From 0a3face067854ef75e17709c32a809964e60c3fd Mon Sep 17 00:00:00 2001 From: Thibaut Decombe Date: Sun, 1 Dec 2024 16:46:52 +0100 Subject: [PATCH 1/4] Add `PositiveTinyIntegerField` and `TinyIntegerField` minimal version --- src/django_mysql/models/__init__.py | 2 ++ src/django_mysql/models/fields/__init__.py | 4 ++++ src/django_mysql/models/fields/integer.py | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 src/django_mysql/models/fields/integer.py diff --git a/src/django_mysql/models/__init__.py b/src/django_mysql/models/__init__.py index 5b074b13..9fb39597 100644 --- a/src/django_mysql/models/__init__.py +++ b/src/django_mysql/models/__init__.py @@ -18,6 +18,8 @@ from django_mysql.models.fields import SetTextField from django_mysql.models.fields import SizedBinaryField from django_mysql.models.fields import SizedTextField +from django_mysql.models.fields.integer import PositiveTinyIntegerField +from django_mysql.models.fields.integer import TinyIntegerField from django_mysql.models.query import ApproximateInt from django_mysql.models.query import QuerySet from django_mysql.models.query import QuerySetMixin diff --git a/src/django_mysql/models/fields/__init__.py b/src/django_mysql/models/fields/__init__.py index f413947d..02785813 100644 --- a/src/django_mysql/models/fields/__init__.py +++ b/src/django_mysql/models/fields/__init__.py @@ -5,6 +5,8 @@ from django_mysql.models.fields.dynamic import DynamicField from django_mysql.models.fields.enum import EnumField from django_mysql.models.fields.fixedchar import FixedCharField +from django_mysql.models.fields.integer import PositiveTinyIntegerField +from django_mysql.models.fields.integer import TinyIntegerField from django_mysql.models.fields.lists import ListCharField from django_mysql.models.fields.lists import ListTextField from django_mysql.models.fields.sets import SetCharField @@ -20,8 +22,10 @@ "ListCharField", "ListTextField", "NullBit1BooleanField", + "PositiveTinyIntegerField", "SetCharField", "SetTextField", "SizedBinaryField", "SizedTextField", + "TinyIntegerField", ] diff --git a/src/django_mysql/models/fields/integer.py b/src/django_mysql/models/fields/integer.py new file mode 100644 index 00000000..8b666756 --- /dev/null +++ b/src/django_mysql/models/fields/integer.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.models import PositiveSmallIntegerField +from django.db.models import SmallIntegerField +from django.utils.translation import gettext_lazy as _ + + +class TinyIntegerField(SmallIntegerField): + description = _("Small integer") + + def db_type(self, connection: BaseDatabaseWrapper) -> str: + return "tinyint" + + +class PositiveTinyIntegerField(PositiveSmallIntegerField): + description = _("Positive small integer") + + def db_type(self, connection: BaseDatabaseWrapper) -> str: + return "tinyint unsigned" From 75cbd78ba4512d3e7b7b8da9cc23838670eeb66f Mon Sep 17 00:00:00 2001 From: Thibaut Decombe Date: Sun, 1 Dec 2024 17:30:36 +0100 Subject: [PATCH 2/4] Add tests --- tests/testapp/models.py | 7 ++ tests/testapp/test_tinyintegerfield.py | 81 +++++++++++++++++++ .../0001_initial.py | 38 +++++++++ .../__init__.py | 0 4 files changed, 126 insertions(+) create mode 100644 tests/testapp/test_tinyintegerfield.py create mode 100644 tests/testapp/tinyinteger_default_migrations/0001_initial.py create mode 100644 tests/testapp/tinyinteger_default_migrations/__init__.py diff --git a/tests/testapp/models.py b/tests/testapp/models.py index e6c5649c..12e64794 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -26,10 +26,12 @@ from django_mysql.models import ListCharField from django_mysql.models import ListTextField from django_mysql.models import Model +from django_mysql.models import PositiveTinyIntegerField from django_mysql.models import SetCharField from django_mysql.models import SetTextField from django_mysql.models import SizedBinaryField from django_mysql.models import SizedTextField +from django_mysql.models import TinyIntegerField from tests.testapp.utils import conn_is_mysql @@ -145,6 +147,11 @@ class FixedCharModel(Model): zip_code = FixedCharField(max_length=10) +class TinyIntegerModel(Model): + tiny_signed = TinyIntegerField(null=True) + tiny_unsigned = PositiveTinyIntegerField(null=True) + + class Author(Model): name = CharField(max_length=32, db_index=True) tutor = ForeignKey("self", on_delete=CASCADE, null=True, related_name="tutees") diff --git a/tests/testapp/test_tinyintegerfield.py b/tests/testapp/test_tinyintegerfield.py new file mode 100644 index 00000000..273a69e5 --- /dev/null +++ b/tests/testapp/test_tinyintegerfield.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import pytest +from django.core.management import call_command +from django.db import connection +from django.db.utils import DataError +from django.test import TestCase +from django.test import TransactionTestCase +from django.test import override_settings + +from tests.testapp.models import TinyIntegerModel + + +class TestSaveLoad(TestCase): + def test_success(self): + TinyIntegerModel.objects.create(tiny_signed=-128, tiny_unsigned=0) + TinyIntegerModel.objects.create(tiny_signed=127, tiny_unsigned=255) + + def test_invalid_too_long_signed(self): + with pytest.raises(DataError) as excinfo: + TinyIntegerModel.objects.create(tiny_signed=128) + + assert excinfo.value.args == ( + 1264, + "Out of range value for column 'tiny_signed' at row 1", + ) + + def test_invalid_too_long_unsigned(self): + with pytest.raises(DataError) as excinfo: + TinyIntegerModel.objects.create(tiny_unsigned=256) + + assert excinfo.value.args == ( + 1264, + "Out of range value for column 'tiny_unsigned' at row 1", + ) + + def test_invalid_too_short_signed(self): + with pytest.raises(DataError) as excinfo: + TinyIntegerModel.objects.create(tiny_signed=-129) + + assert excinfo.value.args == ( + 1264, + "Out of range value for column 'tiny_signed' at row 1", + ) + + def test_invalid_too_short_unsigned(self): + with pytest.raises(DataError) as excinfo: + TinyIntegerModel.objects.create(tiny_unsigned=-1) + + assert excinfo.value.args == ( + 1264, + "Out of range value for column 'tiny_unsigned' at row 1", + ) + + +class TestMigrations(TransactionTestCase): + @override_settings( + MIGRATION_MODULES={"testapp": "tests.testapp.tinyinteger_default_migrations"} + ) + def test_adding_field_with_default(self): + table_name = "testapp_tinyintegerdefaultmodel" + table_names = connection.introspection.table_names + with connection.cursor() as cursor: + assert table_name not in table_names(cursor) + + call_command( + "migrate", "testapp", verbosity=0, skip_checks=True, interactive=False + ) + with connection.cursor() as cursor: + assert table_name in table_names(cursor) + + call_command( + "migrate", + "testapp", + "zero", + verbosity=0, + skip_checks=True, + interactive=False, + ) + with connection.cursor() as cursor: + assert table_name not in table_names(cursor) diff --git a/tests/testapp/tinyinteger_default_migrations/0001_initial.py b/tests/testapp/tinyinteger_default_migrations/0001_initial.py new file mode 100644 index 00000000..63045b24 --- /dev/null +++ b/tests/testapp/tinyinteger_default_migrations/0001_initial.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from django.db import migrations +from django.db import models + +from django_mysql.models import PositiveTinyIntegerField +from django_mysql.models import TinyIntegerField + + +class Migration(migrations.Migration): + dependencies: list[tuple[str, str]] = [] + + operations = [ + migrations.CreateModel( + name="TinyIntegerDefaultModel", + fields=[ + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "tiny_signed", + PositiveTinyIntegerField(), + ), + ( + "tiny_unsigned", + TinyIntegerField(), + ), + ], + options={}, + bases=(models.Model,), + ) + ] diff --git a/tests/testapp/tinyinteger_default_migrations/__init__.py b/tests/testapp/tinyinteger_default_migrations/__init__.py new file mode 100644 index 00000000..e69de29b From cc4eeaf1119a037e76f4fccd1e03fceaab172bbd Mon Sep 17 00:00:00 2001 From: Thibaut Decombe Date: Sun, 1 Dec 2024 18:02:11 +0100 Subject: [PATCH 3/4] update documentation and changelog --- docs/changelog.rst | 2 + docs/exposition.rst | 15 +++++++ docs/model_fields/tiny_integer_field.rst | 54 ++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 docs/model_fields/tiny_integer_field.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index 0bc4c480..da195fa3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* Added field class ``PositiveTinyIntegerField`` and ``TinyIntegerField`` that uses MySQL's ``TINYINT`` data type. + 4.15.0 (2024-10-29) ------------------- diff --git a/docs/exposition.rst b/docs/exposition.rst index 10d48eec..15a2f5ca 100644 --- a/docs/exposition.rst +++ b/docs/exposition.rst @@ -196,6 +196,21 @@ field class allows you to interact with those fields: :ref:`Read more ` +TinyInteger Fields +------------------ + +MySQL’s ``TINYINT`` type efficiently stores small integers in just one byte. +These fields allow you to use it seamlessly in Django models: + +.. code-block:: python + + class TinyIntModel(Model): + tiny_value = TinyIntegerField() # Supports values from -128 to 127. + positive_tiny_value = PositiveTinyIntegerField() # Supports values from 0 to 255. + +:ref:`Read more ` + + ------------- Field Lookups ------------- diff --git a/docs/model_fields/tiny_integer_field.rst b/docs/model_fields/tiny_integer_field.rst new file mode 100644 index 00000000..5329a11a --- /dev/null +++ b/docs/model_fields/tiny_integer_field.rst @@ -0,0 +1,54 @@ +.. _tinyintegerfields: + +---------------- +TinyIntegerField +---------------- + +.. currentmodule:: django_mysql.models + +When working with integers that fit within small ranges, the default integer +fields can lead to excessive storage usage. MySQL’s ``TINYINT`` type allows +efficient storage by limiting the size to one byte. +The `TinyIntegerField` and `PositiveTinyIntegerField` make it easy to use +the ``TINYINT`` and ``TINYINT UNSIGNED`` types in Django. + +Docs: +`MySQL TINYINT `_ / +`MariaDB `_. + +.. class:: TinyIntegerField(**kwargs) + + A subclass of Django’s :class:`~django.db.models.SmallIntegerField` that uses a MySQL + ``TINYINT`` type for storage. It supports signed integer values ranging from -128 to 127. + + Example: + + .. code-block:: python + + from django.db import models + from myapp.fields import TinyIntegerField + + + class ExampleModel(models.Model): + tiny_value = TinyIntegerField() + +.. class:: PositiveTinyIntegerField(**kwargs) + + A subclass of Django’s :class:`~django.db.models.PositiveSmallIntegerField` that uses a + MySQL ``TINYINT UNSIGNED`` type for storage. It supports unsigned integer values ranging + from 0 to 255. + + Example: + + .. code-block:: python + + from django.db import models + from myapp.fields import PositiveTinyIntegerField + + + class ExampleModel(models.Model): + positive_tiny_value = PositiveTinyIntegerField() + +.. note:: + Ensure that existing data values fall within the specified ranges before migrating + to this field, as values outside these ranges will cause migration operations to fail. From 30d74f32cbcf2a42c85d8e835376e5bc74a20196 Mon Sep 17 00:00:00 2001 From: Thibaut Decombe Date: Sun, 1 Dec 2024 20:09:30 +0100 Subject: [PATCH 4/4] Add new field to `Model Fields` toctree --- docs/model_fields/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/model_fields/index.rst b/docs/model_fields/index.rst index 094232b1..1ab95d6e 100644 --- a/docs/model_fields/index.rst +++ b/docs/model_fields/index.rst @@ -20,5 +20,6 @@ to the home of Django's native fields in ``django.db.models``. fixedchar_field resizable_text_binary_fields null_bit1_boolean_fields + tiny_integer_field .. currentmodule:: django_mysql.models