From 203c57fb2fe2715418803f9abce9441bd408b9de Mon Sep 17 00:00:00 2001 From: collerek Date: Sat, 28 Nov 2020 10:48:49 +0100 Subject: [PATCH 1/4] fix uuid column to char(36) --- docs/releases.md | 6 +++++ ormar/__init__.py | 2 +- ormar/fields/sqlalchemy_uuid.py | 2 +- tests/test_dumping_model_to_dict.py | 33 ++++++++++++----------- tests/test_excluding_fields_in_fastapi.py | 32 +++++++++++++++------- tests/test_models.py | 5 ++++ 6 files changed, 53 insertions(+), 27 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index d27647f..d1eadd6 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,3 +1,9 @@ +# 0.5.4 + +* Fixed bug in `UUID` field that was trimmed to 32 chars instead of 36 (with dashes). +* Since first 32 characters of uuid should still be unique I consider this as non-breaking, but if you + were using UUID field before you might consider re-generating the ids. + # 0.5.3 * Fixed bug in `Model.dict()` method that was ignoring exclude parameter and not include dictionary argument. diff --git a/ormar/__init__.py b/ormar/__init__.py index e910731..e2d78c8 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -30,7 +30,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.5.3" +__version__ = "0.5.4" __all__ = [ "Integer", "BigInteger", diff --git a/ormar/fields/sqlalchemy_uuid.py b/ormar/fields/sqlalchemy_uuid.py index aca381a..3f0fc88 100644 --- a/ormar/fields/sqlalchemy_uuid.py +++ b/ormar/fields/sqlalchemy_uuid.py @@ -28,7 +28,7 @@ class UUID(TypeDecorator): # pragma nocover return ret_value def load_dialect_impl(self, dialect: DefaultDialect) -> Any: - return dialect.type_descriptor(CHAR(32)) + return dialect.type_descriptor(CHAR(36)) def process_bind_param( self, value: Union[str, int, bytes, uuid.UUID, None], dialect: DefaultDialect diff --git a/tests/test_dumping_model_to_dict.py b/tests/test_dumping_model_to_dict.py index 5fd9405..499c5f0 100644 --- a/tests/test_dumping_model_to_dict.py +++ b/tests/test_dumping_model_to_dict.py @@ -73,7 +73,7 @@ def test_dumping_to_dict_no_exclusion(sample_data): dict1 = item1.dict() assert dict1["name"] == "Teddy Bear" assert dict1["category"]["name"] == "Toys" - assert dict1["category"]["tier"]['name'] == "Tier I" + assert dict1["category"]["tier"]["name"] == "Tier I" assert dict1["created_by"]["email"] == "test@test.com" dict2 = item2.dict() @@ -114,26 +114,29 @@ def test_dumping_to_dict_exclude_nested_dict(sample_data): dict1 = item2.dict(exclude={"category": {"tier": {"name"}}, "name": ...}) assert "name" not in dict1 assert "category" in dict1 - assert dict1["category"]['name'] == 'Weapons' + assert dict1["category"]["name"] == "Weapons" assert dict1["created_by"]["email"] == "test@test.com" - assert dict1["category"]["tier"].get('name') is None + assert dict1["category"]["tier"].get("name") is None def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data): item1, item2 = sample_data - dict1 = item2.dict(exclude={"category": {"tier": {"name"}}}, - include={'name', 'category'}) - assert dict1.get('name') == 'M16' + dict1 = item2.dict( + exclude={"category": {"tier": {"name"}}}, include={"name", "category"} + ) + assert dict1.get("name") == "M16" assert "category" in dict1 - assert dict1["category"]['name'] == 'Weapons' + assert dict1["category"]["name"] == "Weapons" assert "created_by" not in dict1 - assert dict1["category"]["tier"].get('name') is None + assert dict1["category"]["tier"].get("name") is None - dict2 = item1.dict(exclude={"id": ...}, - include={'name': ..., 'category': {'name': ..., 'tier': {'id'}}}) - assert dict2.get('name') == 'Teddy Bear' - assert dict2.get('id') is None # models not saved - assert dict2["category"]['name'] == 'Toys' + dict2 = item1.dict( + exclude={"id": ...}, + include={"name": ..., "category": {"name": ..., "tier": {"id"}}}, + ) + assert dict2.get("name") == "Teddy Bear" + assert dict2.get("id") is None # models not saved + assert dict2["category"]["name"] == "Toys" assert "created_by" not in dict1 - assert dict1["category"]["tier"].get('name') is None - assert dict1["category"]["tier"]['id'] is None + assert dict1["category"]["tier"].get("name") is None + assert dict1["category"]["tier"]["id"] is None diff --git a/tests/test_excluding_fields_in_fastapi.py b/tests/test_excluding_fields_in_fastapi.py index 56934f1..c9ebc46 100644 --- a/tests/test_excluding_fields_in_fastapi.py +++ b/tests/test_excluding_fields_in_fastapi.py @@ -66,9 +66,11 @@ class RandomModel(ormar.Model): id: int = ormar.Integer(primary_key=True) password: str = ormar.String(max_length=255, default=gen_pass) - first_name: str = ormar.String(max_length=255, default='John') + first_name: str = ormar.String(max_length=255, default="John") last_name: str = ormar.String(max_length=255) - created_date: datetime.datetime = ormar.DateTime(server_default=sqlalchemy.func.now()) + created_date: datetime.datetime = ormar.DateTime( + server_default=sqlalchemy.func.now() + ) class User(ormar.Model): @@ -115,7 +117,7 @@ async def create_user(user: User): @app.post("/users2/", response_model=User) async def create_user2(user: User): user = await user.save() - return user.dict(exclude={'password'}) + return user.dict(exclude={"password"}) @app.post("/users3/", response_model=UserBase) @@ -126,7 +128,7 @@ async def create_user3(user: User2): @app.post("/users4/") async def create_user4(user: User2): user = await user.save() - return user.dict(exclude={'password'}) + return user.dict(exclude={"password"}) @app.post("/random/", response_model=RandomModel) @@ -166,13 +168,23 @@ def test_all_endpoints(): # response has only 3 fields from UserBase response = client.post("/users3/", json=user) - assert list(response.json().keys()) == ['email', 'first_name', 'last_name'] + assert list(response.json().keys()) == ["email", "first_name", "last_name"] response = client.post("/users4/", json=user) - assert list(response.json().keys()) == ['id', 'email', 'first_name', 'last_name', 'category'] + assert list(response.json().keys()) == [ + "id", + "email", + "first_name", + "last_name", + "category", + ] - user3 = { - 'last_name': 'Test' - } + user3 = {"last_name": "Test"} response = client.post("/random/", json=user3) - assert list(response.json().keys()) == ['id', 'password', 'first_name', 'last_name', 'created_date'] + assert list(response.json().keys()) == [ + "id", + "password", + "first_name", + "last_name", + "created_date", + ] diff --git a/tests/test_models.py b/tests/test_models.py index 490dedd..1c0f2ae 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -145,6 +145,11 @@ async def test_uuid_column(): item = await UUIDSample.objects.filter(id=u1.id).get() assert item.id == u1.id + item2 = await UUIDSample.objects.first() + item3 = await UUIDSample.objects.get(pk=item2.id) + assert item2.id == item3.id + assert isinstance(item3.id, uuid.UUID) + @pytest.mark.asyncio async def test_model_crud(): From 740bb29ea5baed6f67f93ae863bd7f5e965d88f0 Mon Sep 17 00:00:00 2001 From: collerek Date: Sat, 28 Nov 2020 10:59:53 +0100 Subject: [PATCH 2/4] fixed binding uuid column to str with dashes --- docs/releases.md | 7 +++---- ormar/__init__.py | 2 +- ormar/fields/sqlalchemy_uuid.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index d1eadd6..af20441 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,8 +1,7 @@ -# 0.5.4 +# 0.6.0 -* Fixed bug in `UUID` field that was trimmed to 32 chars instead of 36 (with dashes). -* Since first 32 characters of uuid should still be unique I consider this as non-breaking, but if you - were using UUID field before you might consider re-generating the ids. +* **Breaking** Changed `UUID` field that was trimmed to 32 chars (without dashes) instead of 36 (with dashes) + to more in line with other libraries. # 0.5.3 diff --git a/ormar/__init__.py b/ormar/__init__.py index e2d78c8..774dea1 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -30,7 +30,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.5.4" +__version__ = "0.6.0" __all__ = [ "Integer", "BigInteger", diff --git a/ormar/fields/sqlalchemy_uuid.py b/ormar/fields/sqlalchemy_uuid.py index 3f0fc88..dd910c5 100644 --- a/ormar/fields/sqlalchemy_uuid.py +++ b/ormar/fields/sqlalchemy_uuid.py @@ -37,7 +37,7 @@ class UUID(TypeDecorator): # pragma nocover return value if not isinstance(value, uuid.UUID): value = self._cast_to_uuid(value) - return "%.32x" % value.int + return str(value) def process_result_value( self, value: Optional[str], dialect: DefaultDialect From 2350111768d1e3d1cc82861dc1b9d917b8c83565 Mon Sep 17 00:00:00 2001 From: collerek Date: Sat, 28 Nov 2020 11:28:15 +0100 Subject: [PATCH 3/4] allow uuid_format add more tests and update docs --- docs/fields.md | 14 ++++++++++++-- docs/releases.md | 10 +++++++--- ormar/fields/model_fields.py | 17 ++++++++++++++++- ormar/fields/sqlalchemy_uuid.py | 12 ++++++++++-- tests/test_models.py | 20 +++++++++++++++++++- 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/docs/fields.md b/docs/fields.md index 67bdc74..0c09d74 100644 --- a/docs/fields.md +++ b/docs/fields.md @@ -251,11 +251,21 @@ You can use either `length` and `precision` parameters or `max_digits` and `deci ### UUID -`UUID()` has no required parameters. +`UUID(uuid_format: str = 'hex')` has no required parameters. -* Sqlalchemy column: `ormar.UUID` based on `sqlalchemy.CHAR` field +* Sqlalchemy column: `ormar.UUID` based on `sqlalchemy.CHAR(36)` or `sqlalchemy.CHAR(32)` field (for string or hex format respectively) * Type (used for pydantic): `uuid.UUID` +`uuid_format` parameters allow 'hex'(default) or 'string' values. + +Depending on the format either 32 or 36 char is used in the database. + +Sample: +* 'hex' format value = "c616ab438cce49dbbf4380d109251dce" (CHAR(32)) +* 'string' value = "c616ab43-8cce-49db-bf43-80d109251dce" (CHAR(36)) + +When loaded it's always python UUID so you can compare it and compare two formats values between each other. + [relations]: ./relations.md [queries]: ./queries.md [pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types diff --git a/docs/releases.md b/docs/releases.md index af20441..ef4791d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,7 +1,11 @@ -# 0.6.0 +# 0.5.4 -* **Breaking** Changed `UUID` field that was trimmed to 32 chars (without dashes) instead of 36 (with dashes) - to more in line with other libraries. +* Allow to pass `uuid_format` (allowed 'hex'(default) or 'string') to `UUID` field to change the format in which it's saved. + By default field is saved in hex format (trimmed to 32 chars (without dashes)), but you can pass + format='string' to use 36 (with dashes) instead to adjust to existing db or other libraries. + Sample: + hex value = c616ab438cce49dbbf4380d109251dce + string value = c616ab43-8cce-49db-bf43-80d109251dce # 0.5.3 diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index 4e1c0cd..b92da65 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -318,6 +318,21 @@ class Decimal(ModelFieldFactory, decimal.Decimal): class UUID(ModelFieldFactory, uuid.UUID): _type = uuid.UUID + def __new__( # type: ignore # noqa CFQ002 + cls, *, uuid_format: str = "hex", **kwargs: Any + ) -> Type[BaseField]: + kwargs = { + **kwargs, + **{ + k: v + for k, v in locals().items() + if k not in ["cls", "__class__", "kwargs"] + }, + } + + return super().__new__(cls, **kwargs) + @classmethod def get_column_type(cls, **kwargs: Any) -> Any: - return sqlalchemy_uuid.UUID() + uuid_format = kwargs.get("uuid_format", "hex") + return sqlalchemy_uuid.UUID(uuid_format=uuid_format) diff --git a/ormar/fields/sqlalchemy_uuid.py b/ormar/fields/sqlalchemy_uuid.py index dd910c5..ba2ecda 100644 --- a/ormar/fields/sqlalchemy_uuid.py +++ b/ormar/fields/sqlalchemy_uuid.py @@ -15,6 +15,10 @@ class UUID(TypeDecorator): # pragma nocover impl = CHAR + def __init__(self, *args: Any, uuid_format: str = "hex", **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.uuid_format = uuid_format + def _cast_to_uuid(self, value: Union[str, int, bytes]) -> uuid.UUID: if not isinstance(value, uuid.UUID): if isinstance(value, bytes): @@ -28,7 +32,11 @@ class UUID(TypeDecorator): # pragma nocover return ret_value def load_dialect_impl(self, dialect: DefaultDialect) -> Any: - return dialect.type_descriptor(CHAR(36)) + return ( + dialect.type_descriptor(CHAR(36)) + if self.uuid_format == "string" + else dialect.type_descriptor(CHAR(32)) + ) def process_bind_param( self, value: Union[str, int, bytes, uuid.UUID, None], dialect: DefaultDialect @@ -37,7 +45,7 @@ class UUID(TypeDecorator): # pragma nocover return value if not isinstance(value, uuid.UUID): value = self._cast_to_uuid(value) - return str(value) + return str(value) if self.uuid_format == "string" else "%.32x" % value.int def process_result_value( self, value: Optional[str], dialect: DefaultDialect diff --git a/tests/test_models.py b/tests/test_models.py index 1c0f2ae..3471b8a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -36,6 +36,16 @@ class UUIDSample(ormar.Model): test_text: str = ormar.Text() +class UUIDSample2(ormar.Model): + class Meta: + tablename = "uuids2" + metadata = metadata + database = database + + id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4, uuid_format='string') + test_text: str = ormar.Text() + + class User(ormar.Model): class Meta: tablename = "users" @@ -150,6 +160,14 @@ async def test_uuid_column(): assert item2.id == item3.id assert isinstance(item3.id, uuid.UUID) + u3 = await UUIDSample2(**u1.dict()).save() + + u1_2 = await UUIDSample.objects.get(pk=u3.id) + assert u1_2 == u1 + + u4 = await UUIDSample2.objects.get(pk=u3.id) + assert u3 == u4 + @pytest.mark.asyncio async def test_model_crud(): @@ -303,7 +321,7 @@ async def test_model_limit_with_filter(): await User.objects.create(name="Tom") assert ( - len(await User.objects.limit(2).filter(name__iexact="Tom").all()) == 2 + len(await User.objects.limit(2).filter(name__iexact="Tom").all()) == 2 ) From bf86d07c73a6b86cc087cec8b4c25f2ca3e23691 Mon Sep 17 00:00:00 2001 From: collerek Date: Sat, 28 Nov 2020 11:28:46 +0100 Subject: [PATCH 4/4] allow uuid_format add more tests and update docs --- docs/releases.md | 5 +++-- ormar/__init__.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index ef4791d..0bc4a00 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -3,9 +3,10 @@ * Allow to pass `uuid_format` (allowed 'hex'(default) or 'string') to `UUID` field to change the format in which it's saved. By default field is saved in hex format (trimmed to 32 chars (without dashes)), but you can pass format='string' to use 36 (with dashes) instead to adjust to existing db or other libraries. + Sample: - hex value = c616ab438cce49dbbf4380d109251dce - string value = c616ab43-8cce-49db-bf43-80d109251dce + * hex value = c616ab438cce49dbbf4380d109251dce + * string value = c616ab43-8cce-49db-bf43-80d109251dce # 0.5.3 diff --git a/ormar/__init__.py b/ormar/__init__.py index 774dea1..e2d78c8 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -30,7 +30,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.6.0" +__version__ = "0.5.4" __all__ = [ "Integer", "BigInteger",