Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit a33ea36

Browse files
authored
add UUID field (#105)
1 parent 2649ede commit a33ea36

File tree

11 files changed

+96
-9
lines changed

11 files changed

+96
-9
lines changed

.github/workflows/test-suite.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,20 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10.0-rc.1"]
17+
python-version: ["3.6", "3.7", "3.8", "3.9"]
1818

1919
services:
20+
mysql:
21+
image: mysql:5.7
22+
env:
23+
MYSQL_USER: username
24+
MYSQL_PASSWORD: password
25+
MYSQL_ROOT_PASSWORD: password
26+
MYSQL_DATABASE: testsuite
27+
ports:
28+
- 3306:3306
29+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
30+
2031
postgres:
2132
image: postgres:10.8
2233
env:
@@ -38,9 +49,13 @@ jobs:
3849
run: "scripts/check"
3950
- name: "Build package & docs"
4051
run: "scripts/build"
41-
- name: "Run tests"
52+
- name: "Run tests with PostgreSQL"
4253
env:
4354
TEST_DATABASE_URL: "postgresql://username:password@localhost:5432/testsuite"
4455
run: "scripts/test"
56+
- name: "Run tests with MySQL"
57+
env:
58+
TEST_DATABASE_URL: "mysql://username:password@localhost:3306/testsuite"
59+
run: "scripts/test"
4560
- name: "Enforce coverage"
4661
run: "scripts/coverage"

docs/declaring_models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ See `TypeSystem` for [type-specific validation keyword arguments][typesystem-fie
6969
* `orm.String(max_length)`
7070
* `orm.Text()`
7171
* `orm.Time()`
72+
* `orm.UUID()`
7273
* `orm.JSON()`
7374

7475
[psycopg2]: https://www.psycopg.org/

orm/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from orm.exceptions import MultipleMatches, NoMatch
22
from orm.fields import (
33
JSON,
4+
UUID,
45
BigInteger,
56
Boolean,
67
Date,
@@ -28,10 +29,11 @@
2829
"Enum",
2930
"Float",
3031
"Integer",
32+
"JSON",
3133
"String",
3234
"Text",
3335
"Time",
34-
"JSON",
36+
"UUID",
3537
"ForeignKey",
3638
"Model",
3739
]

orm/fields.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import sqlalchemy
44
import typesystem
55

6+
from orm.sqlalchemy_fields import GUID
7+
68

79
class ModelField:
810
def __init__(
@@ -138,3 +140,8 @@ def __init__(self, max_digits: int, decimal_places: int, **kwargs):
138140

139141
def get_column_type(self):
140142
return sqlalchemy.Numeric(precision=self.max_digits, scale=self.decimal_places)
143+
144+
145+
class UUID(ModelField, typesystem.UUID):
146+
def get_column_type(self):
147+
return GUID()

orm/sqlalchemy_fields.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import uuid
2+
3+
import sqlalchemy
4+
5+
6+
class GUID(sqlalchemy.TypeDecorator):
7+
"""Platform-independent GUID type.
8+
9+
Uses PostgreSQL's UUID type, otherwise uses
10+
CHAR(32), storing as stringified hex values.
11+
"""
12+
13+
impl = sqlalchemy.CHAR
14+
cache_ok = True
15+
16+
def load_dialect_impl(self, dialect):
17+
if dialect.name == "postgresql":
18+
return dialect.type_descriptor(sqlalchemy.dialects.postgresql.UUID())
19+
else:
20+
return dialect.type_descriptor(sqlalchemy.CHAR(32))
21+
22+
def process_bind_param(self, value, dialect):
23+
if value is None:
24+
return value
25+
elif dialect.name == "postgresql":
26+
return str(value)
27+
else:
28+
return "%.32x" % value.int
29+
30+
def process_result_value(self, value, dialect):
31+
if value is None:
32+
return value
33+
else:
34+
if not isinstance(value, uuid.UUID):
35+
value = uuid.UUID(value)
36+
return value

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
databases[postgresql]
2-
psycopg2
1+
databases[postgresql, mysql]
2+
psycopg2-binary
3+
pymysql
34
typesystem
45

56
# Packaging

scripts/test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ if [ -z $GITHUB_ACTIONS ]; then
1111
scripts/check
1212
fi
1313

14-
${PREFIX}coverage run -m pytest $@
14+
${PREFIX}coverage run -a -m pytest $@
1515

1616
if [ -z $GITHUB_ACTIONS ]; then
1717
scripts/coverage

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ xfail_strict=True
1919
filterwarnings=
2020
# Turn warnings that aren't filtered into exceptions
2121
error
22+
ignore::DeprecationWarning
2223

2324
[coverage:run]
2425
source_pkgs = orm, tests

tests/test_columns.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import decimal
3+
import uuid
34
from enum import Enum
45

56
import databases
@@ -30,6 +31,7 @@ class Example(orm.Model):
3031
__database__ = database
3132

3233
id = orm.Integer(primary_key=True)
34+
uuid = orm.UUID(allow_null=True)
3335
huge_number = orm.BigInteger(default=9223372036854775807)
3436
created = orm.DateTime(default=datetime.datetime.now)
3537
created_day = orm.Date(default=datetime.date.today)
@@ -43,7 +45,13 @@ class Example(orm.Model):
4345

4446
@pytest.fixture(autouse=True, scope="module")
4547
def create_test_database():
46-
engine = sqlalchemy.create_engine(DATABASE_URL)
48+
database_url = databases.DatabaseURL(DATABASE_URL)
49+
if database_url.scheme == "mysql":
50+
url = str(database_url.replace(driver="pymysql"))
51+
else:
52+
url = str(database_url)
53+
54+
engine = sqlalchemy.create_engine(url)
4755
metadata.create_all(engine)
4856
yield
4957
metadata.drop_all(engine)
@@ -62,15 +70,19 @@ async def test_model_crud():
6270
assert example.price is None
6371
assert example.data == {}
6472
assert example.status == StatusEnum.DRAFT
73+
assert example.uuid is None
6574

6675
await example.update(
6776
data={"foo": 123},
6877
value=123.456,
6978
status=StatusEnum.RELEASED,
7079
price=decimal.Decimal("999.99"),
80+
uuid=uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b"),
7181
)
82+
7283
example = await Example.objects.get()
7384
assert example.value == 123.456
7485
assert example.data == {"foo": 123}
7586
assert example.status == StatusEnum.RELEASED
7687
assert example.price == decimal.Decimal("999.99")
88+
assert example.uuid == uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b")

tests/test_foreignkey.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ class Member(orm.Model):
6262

6363
@pytest.fixture(autouse=True, scope="module")
6464
def create_test_database():
65-
engine = sqlalchemy.create_engine(DATABASE_URL)
65+
database_url = databases.DatabaseURL(DATABASE_URL)
66+
if database_url.scheme == "mysql":
67+
url = str(database_url.replace(driver="pymysql"))
68+
else:
69+
url = str(database_url)
70+
71+
engine = sqlalchemy.create_engine(url)
6672
metadata.create_all(engine)
6773
yield
6874
metadata.drop_all(engine)

0 commit comments

Comments
 (0)