Skip to content

Commit

Permalink
feat: add uuid compression storing data as binary (#1458)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
plusiv authored Oct 11, 2023
1 parent 0484cf5 commit 15d180e
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 1 deletion.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Changelog
0.20
====

0.20.1
------
Added
^^^^^
- Add binary compression support for `UUIDField` in `MySQL`. (#1458)

0.20.0
------
Added
Expand Down
1 change: 1 addition & 0 deletions docs/contrib/mysql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Fields
MySQL specific fields.

.. autoclass:: tortoise.contrib.mysql.fields.GeometryField
.. autoclass:: tortoise.contrib.mysql.fields.UUIDField

Search
======
Expand Down
2 changes: 1 addition & 1 deletion docs/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ MySQL
^^^^^

.. automodule:: tortoise.contrib.mysql.fields
:members: GeometryField
:members: GeometryField, UUIDField

Postgres
^^^^^^^^
Expand Down
Empty file added tests/contrib/mysql/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions tests/contrib/mysql/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import uuid

from tests import testmodels_mysql
from tortoise.contrib import test
from tortoise.exceptions import IntegrityError


class TestMySQLUUIDFields(test.TestCase):
async def test_empty(self):
with self.assertRaises(IntegrityError):
await testmodels_mysql.UUIDFields.create()

async def test_create(self):
data = uuid.uuid4()
obj0 = await testmodels_mysql.UUIDFields.create(data=data)
self.assertIsInstance(obj0.data, bytes)
self.assertIsInstance(obj0.data_auto, bytes)
self.assertEqual(obj0.data_null, None)
obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
self.assertIsInstance(obj.data, uuid.UUID)
self.assertIsInstance(obj.data_auto, uuid.UUID)
self.assertEqual(obj.data, data)
self.assertEqual(obj.data_null, None)
await obj.save()
obj2 = await testmodels_mysql.UUIDFields.get(id=obj.id)
self.assertEqual(obj, obj2)

await obj.delete()
obj = await testmodels_mysql.UUIDFields.filter(id=obj0.id).first()
self.assertEqual(obj, None)

async def test_update(self):
data = uuid.uuid4()
data2 = uuid.uuid4()
obj0 = await testmodels_mysql.UUIDFields.create(data=data)
await testmodels_mysql.UUIDFields.filter(id=obj0.id).update(data=data2)
obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
self.assertEqual(obj.data, data2)
self.assertEqual(obj.data_null, None)

async def test_create_not_null(self):
data = uuid.uuid4()
obj0 = await testmodels_mysql.UUIDFields.create(data=data, data_null=data)
obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
self.assertEqual(obj.data, data)
self.assertEqual(obj.data_null, data)
78 changes: 78 additions & 0 deletions tests/testmodels_mysql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from tortoise import fields
from tortoise.contrib.mysql import fields as mysql_fields
from tortoise.models import Model


class UUIDPkModel(Model):
id = mysql_fields.UUIDField(pk=True)

children: fields.ReverseRelation["UUIDFkRelatedModel"]
children_null: fields.ReverseRelation["UUIDFkRelatedNullModel"]
peers: fields.ManyToManyRelation["UUIDM2MRelatedModel"]


class UUIDFkRelatedModel(Model):
id = mysql_fields.UUIDField(pk=True)
name = fields.CharField(max_length=50, null=True)
model: fields.ForeignKeyRelation[UUIDPkModel] = fields.ForeignKeyField(
"models.UUIDPkModel", related_name="children"
)


class UUIDFkRelatedNullModel(Model):
id = mysql_fields.UUIDField(pk=True)
name = fields.CharField(max_length=50, null=True)
model: fields.ForeignKeyNullableRelation[UUIDPkModel] = fields.ForeignKeyField(
"models.UUIDPkModel", related_name=False, null=True
)
parent: fields.OneToOneNullableRelation[UUIDPkModel] = fields.OneToOneField(
"models.UUIDPkModel", related_name=False, null=True, on_delete=fields.NO_ACTION
)


class UUIDM2MRelatedModel(Model):
id = mysql_fields.UUIDField(pk=True)
value = fields.TextField(default="test")
models: fields.ManyToManyRelation[UUIDPkModel] = fields.ManyToManyField(
"models.UUIDPkModel", related_name="peers"
)


class UUIDPkSourceModel(Model):
id = mysql_fields.UUIDField(pk=True, source_field="a")

class Meta:
table = "upsm"


class UUIDFkRelatedSourceModel(Model):
id = mysql_fields.UUIDField(pk=True, source_field="b")
name = fields.CharField(max_length=50, null=True, source_field="c")
model: fields.ForeignKeyRelation[UUIDPkSourceModel] = fields.ForeignKeyField(
"models.UUIDPkSourceModel", related_name="children", source_field="d"
)

class Meta:
table = "ufrsm"


class UUIDFkRelatedNullSourceModel(Model):
id = mysql_fields.UUIDField(pk=True, source_field="i")
name = fields.CharField(max_length=50, null=True, source_field="j")
model: fields.ForeignKeyNullableRelation[UUIDPkSourceModel] = fields.ForeignKeyField(
"models.UUIDPkSourceModel", related_name="children_null", source_field="k", null=True
)

class Meta:
table = "ufrnsm"


class UUIDM2MRelatedSourceModel(Model):
id = mysql_fields.UUIDField(pk=True, source_field="e")
value = fields.TextField(default="test", source_field="f")
models: fields.ManyToManyRelation[UUIDPkSourceModel] = fields.ManyToManyField(
"models.UUIDPkSourceModel", related_name="peers", forward_key="e", backward_key="h"
)

class Meta:
table = "umrsm"
58 changes: 58 additions & 0 deletions tortoise/contrib/mysql/fields.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
from typing import ( # noqa pylint: disable=unused-import
TYPE_CHECKING,
Any,
Optional,
Type,
Union,
)
from uuid import UUID, uuid4

from tortoise.fields import Field
from tortoise.fields import UUIDField as UUIDFieldBase

if TYPE_CHECKING: # pragma: nocoverage
from tortoise.models import Model # noqa pylint: disable=unused-import


class GeometryField(Field):
SQL_TYPE = "GEOMETRY"


class UUIDField(UUIDFieldBase):
"""
UUID Field
This field can store uuid value, but with the option to add binary compression.
If used as a primary key, it will auto-generate a UUID4 by default.
``binary_compression`` (bool):
If True, the UUID will be stored in binary format.
This will save 6 bytes per UUID in the database.
Note: that this is a MySQL-only feature.
See https://dev.mysql.com/blog-archive/mysql-8-0-uuid-support/ for more details.
"""

SQL_TYPE = "CHAR(36)"

def __init__(self, binary_compression: bool = True, **kwargs: Any) -> None:
if kwargs.get("pk", False) and "default" not in kwargs:
kwargs["default"] = uuid4
super().__init__(**kwargs)

if binary_compression:
self.SQL_TYPE = "BINARY(16)"
self._binary_compression = binary_compression

def to_db_value(self, value: Any, instance: "Union[Type[Model], Model]") -> Optional[Union[str, bytes]]: # type: ignore
# Make sure that value is a UUIDv4
# If not, raise an error
# This is to prevent UUIDv1 or any other version from being stored in the database
if self._binary_compression:
if value is not isinstance(value, UUID):
raise ValueError("UUIDField only accepts UUID values")
return value.bytes
return value and str(value)

def to_python_value(self, value: Any) -> Optional[UUID]:
if value is None or isinstance(value, UUID):
return value
elif self._binary_compression and isinstance(value, bytes):
return UUID(bytes=value)
else:
return UUID(value)

0 comments on commit 15d180e

Please sign in to comment.