Skip to content

Commit 15d180e

Browse files
authored
feat: add uuid compression storing data as binary (#1458)
* feat: add uuid compression storing data as `binary` * style: fix trailing whitespaces * docs: add changes to `CHANGELOG.rst` * ref: move changes to `tortoise/contrib/mysql/fields.py` * fix: error about unconsistent methods * fix: inherits from base field instead old `UUIDField` This's becaus the old `UUIDField` class is not a generic class. * style: format code * test: add test for MySQL `UUIDField` class * fix: remove bugs and lint errors * chore: add missing parameter in method overriding * chore: fix linter errors * style: fix style * style: fix types to pass code quality * style: fix types to pass code quality * chore: bypass linter false-positive * docs: update documentation
1 parent 0484cf5 commit 15d180e

File tree

7 files changed

+190
-1
lines changed

7 files changed

+190
-1
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Changelog
99
0.20
1010
====
1111

12+
0.20.1
13+
------
14+
Added
15+
^^^^^
16+
- Add binary compression support for `UUIDField` in `MySQL`. (#1458)
17+
1218
0.20.0
1319
------
1420
Added

docs/contrib/mysql.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Fields
1818
MySQL specific fields.
1919

2020
.. autoclass:: tortoise.contrib.mysql.fields.GeometryField
21+
.. autoclass:: tortoise.contrib.mysql.fields.UUIDField
2122

2223
Search
2324
======

docs/fields.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ MySQL
5656
^^^^^
5757

5858
.. automodule:: tortoise.contrib.mysql.fields
59-
:members: GeometryField
59+
:members: GeometryField, UUIDField
6060

6161
Postgres
6262
^^^^^^^^

tests/contrib/mysql/__init__.py

Whitespace-only changes.

tests/contrib/mysql/fields.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import uuid
2+
3+
from tests import testmodels_mysql
4+
from tortoise.contrib import test
5+
from tortoise.exceptions import IntegrityError
6+
7+
8+
class TestMySQLUUIDFields(test.TestCase):
9+
async def test_empty(self):
10+
with self.assertRaises(IntegrityError):
11+
await testmodels_mysql.UUIDFields.create()
12+
13+
async def test_create(self):
14+
data = uuid.uuid4()
15+
obj0 = await testmodels_mysql.UUIDFields.create(data=data)
16+
self.assertIsInstance(obj0.data, bytes)
17+
self.assertIsInstance(obj0.data_auto, bytes)
18+
self.assertEqual(obj0.data_null, None)
19+
obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
20+
self.assertIsInstance(obj.data, uuid.UUID)
21+
self.assertIsInstance(obj.data_auto, uuid.UUID)
22+
self.assertEqual(obj.data, data)
23+
self.assertEqual(obj.data_null, None)
24+
await obj.save()
25+
obj2 = await testmodels_mysql.UUIDFields.get(id=obj.id)
26+
self.assertEqual(obj, obj2)
27+
28+
await obj.delete()
29+
obj = await testmodels_mysql.UUIDFields.filter(id=obj0.id).first()
30+
self.assertEqual(obj, None)
31+
32+
async def test_update(self):
33+
data = uuid.uuid4()
34+
data2 = uuid.uuid4()
35+
obj0 = await testmodels_mysql.UUIDFields.create(data=data)
36+
await testmodels_mysql.UUIDFields.filter(id=obj0.id).update(data=data2)
37+
obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
38+
self.assertEqual(obj.data, data2)
39+
self.assertEqual(obj.data_null, None)
40+
41+
async def test_create_not_null(self):
42+
data = uuid.uuid4()
43+
obj0 = await testmodels_mysql.UUIDFields.create(data=data, data_null=data)
44+
obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
45+
self.assertEqual(obj.data, data)
46+
self.assertEqual(obj.data_null, data)

tests/testmodels_mysql.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from tortoise import fields
2+
from tortoise.contrib.mysql import fields as mysql_fields
3+
from tortoise.models import Model
4+
5+
6+
class UUIDPkModel(Model):
7+
id = mysql_fields.UUIDField(pk=True)
8+
9+
children: fields.ReverseRelation["UUIDFkRelatedModel"]
10+
children_null: fields.ReverseRelation["UUIDFkRelatedNullModel"]
11+
peers: fields.ManyToManyRelation["UUIDM2MRelatedModel"]
12+
13+
14+
class UUIDFkRelatedModel(Model):
15+
id = mysql_fields.UUIDField(pk=True)
16+
name = fields.CharField(max_length=50, null=True)
17+
model: fields.ForeignKeyRelation[UUIDPkModel] = fields.ForeignKeyField(
18+
"models.UUIDPkModel", related_name="children"
19+
)
20+
21+
22+
class UUIDFkRelatedNullModel(Model):
23+
id = mysql_fields.UUIDField(pk=True)
24+
name = fields.CharField(max_length=50, null=True)
25+
model: fields.ForeignKeyNullableRelation[UUIDPkModel] = fields.ForeignKeyField(
26+
"models.UUIDPkModel", related_name=False, null=True
27+
)
28+
parent: fields.OneToOneNullableRelation[UUIDPkModel] = fields.OneToOneField(
29+
"models.UUIDPkModel", related_name=False, null=True, on_delete=fields.NO_ACTION
30+
)
31+
32+
33+
class UUIDM2MRelatedModel(Model):
34+
id = mysql_fields.UUIDField(pk=True)
35+
value = fields.TextField(default="test")
36+
models: fields.ManyToManyRelation[UUIDPkModel] = fields.ManyToManyField(
37+
"models.UUIDPkModel", related_name="peers"
38+
)
39+
40+
41+
class UUIDPkSourceModel(Model):
42+
id = mysql_fields.UUIDField(pk=True, source_field="a")
43+
44+
class Meta:
45+
table = "upsm"
46+
47+
48+
class UUIDFkRelatedSourceModel(Model):
49+
id = mysql_fields.UUIDField(pk=True, source_field="b")
50+
name = fields.CharField(max_length=50, null=True, source_field="c")
51+
model: fields.ForeignKeyRelation[UUIDPkSourceModel] = fields.ForeignKeyField(
52+
"models.UUIDPkSourceModel", related_name="children", source_field="d"
53+
)
54+
55+
class Meta:
56+
table = "ufrsm"
57+
58+
59+
class UUIDFkRelatedNullSourceModel(Model):
60+
id = mysql_fields.UUIDField(pk=True, source_field="i")
61+
name = fields.CharField(max_length=50, null=True, source_field="j")
62+
model: fields.ForeignKeyNullableRelation[UUIDPkSourceModel] = fields.ForeignKeyField(
63+
"models.UUIDPkSourceModel", related_name="children_null", source_field="k", null=True
64+
)
65+
66+
class Meta:
67+
table = "ufrnsm"
68+
69+
70+
class UUIDM2MRelatedSourceModel(Model):
71+
id = mysql_fields.UUIDField(pk=True, source_field="e")
72+
value = fields.TextField(default="test", source_field="f")
73+
models: fields.ManyToManyRelation[UUIDPkSourceModel] = fields.ManyToManyField(
74+
"models.UUIDPkSourceModel", related_name="peers", forward_key="e", backward_key="h"
75+
)
76+
77+
class Meta:
78+
table = "umrsm"

tortoise/contrib/mysql/fields.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,63 @@
1+
from typing import ( # noqa pylint: disable=unused-import
2+
TYPE_CHECKING,
3+
Any,
4+
Optional,
5+
Type,
6+
Union,
7+
)
8+
from uuid import UUID, uuid4
9+
110
from tortoise.fields import Field
11+
from tortoise.fields import UUIDField as UUIDFieldBase
12+
13+
if TYPE_CHECKING: # pragma: nocoverage
14+
from tortoise.models import Model # noqa pylint: disable=unused-import
215

316

417
class GeometryField(Field):
518
SQL_TYPE = "GEOMETRY"
19+
20+
21+
class UUIDField(UUIDFieldBase):
22+
"""
23+
UUID Field
24+
25+
This field can store uuid value, but with the option to add binary compression.
26+
27+
If used as a primary key, it will auto-generate a UUID4 by default.
28+
29+
``binary_compression`` (bool):
30+
If True, the UUID will be stored in binary format.
31+
This will save 6 bytes per UUID in the database.
32+
Note: that this is a MySQL-only feature.
33+
See https://dev.mysql.com/blog-archive/mysql-8-0-uuid-support/ for more details.
34+
"""
35+
36+
SQL_TYPE = "CHAR(36)"
37+
38+
def __init__(self, binary_compression: bool = True, **kwargs: Any) -> None:
39+
if kwargs.get("pk", False) and "default" not in kwargs:
40+
kwargs["default"] = uuid4
41+
super().__init__(**kwargs)
42+
43+
if binary_compression:
44+
self.SQL_TYPE = "BINARY(16)"
45+
self._binary_compression = binary_compression
46+
47+
def to_db_value(self, value: Any, instance: "Union[Type[Model], Model]") -> Optional[Union[str, bytes]]: # type: ignore
48+
# Make sure that value is a UUIDv4
49+
# If not, raise an error
50+
# This is to prevent UUIDv1 or any other version from being stored in the database
51+
if self._binary_compression:
52+
if value is not isinstance(value, UUID):
53+
raise ValueError("UUIDField only accepts UUID values")
54+
return value.bytes
55+
return value and str(value)
56+
57+
def to_python_value(self, value: Any) -> Optional[UUID]:
58+
if value is None or isinstance(value, UUID):
59+
return value
60+
elif self._binary_compression and isinstance(value, bytes):
61+
return UUID(bytes=value)
62+
else:
63+
return UUID(value)

0 commit comments

Comments
 (0)