Skip to content

Add tiny integer fields #1123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 13, 2025
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
Changelog
=========

* Added field classes :class:`~django_mysql.models.TinyIntegerField` and
:class:`~django_mysql.models.PositiveTinyIntegerField` that use MySQL’s one-byte
``TINYINT`` data type.

4.16.0 (2025-02-06)
-------------------

Expand Down
20 changes: 20 additions & 0 deletions docs/exposition.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,26 @@ field class allows you to interact with those fields:

:ref:`Read more <bit1booleanfields>`

Tiny integer fields
-------------------

MySQL’s ``TINYINT`` type stores small integers efficiently, using just one byte.
Django-MySQL provides field classes for the ``TINYINT`` type:

.. code-block:: python

from django.db import models
from django_mysql.models import TinyIntegerField, PositiveTinyIntegerField


class TinyIntModel(models.Model):
# Supports values from -128 to 127:
tiny_value = TinyIntegerField()
# Supports values from 0 to 255:
positive_tiny_value = PositiveTinyIntegerField()

:ref:`Read more <tiny-integer-fields>`

-------------
Field Lookups
-------------
Expand Down
1 change: 1 addition & 0 deletions docs/model_fields/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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_fields

.. currentmodule:: django_mysql.models
50 changes: 50 additions & 0 deletions docs/model_fields/tiny_integer_fields.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.. _tiny-integer-fields:

-------------------
Tiny integer fields
-------------------

.. currentmodule:: django_mysql.models

When working with integers that only take small values, Django’s default integer fields can be a bit wasteful as smallest field class, |SmallIntegerField|__, takes 2 bytes.
MySQL’s smallest integer data type, ``TINYINT``, is 1 byte, half the size!
The below field classes allow you to use the ``TINYINT`` and ``TINYINT UNSIGNED`` types in Django.

.. |SmallIntegerField| replace:: ``SmallIntegerField``
__ https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.SmallIntegerField

Docs:
`MySQL TINYINT <https://dev.mysql.com/doc/refman/en/integer-types.html>`_ /
`MariaDB <https://mariadb.com/kb/en/tinyint/>`_.

.. class:: TinyIntegerField(**kwargs)

A subclass of Django’s :class:`~django.db.models.IntegerField` 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.PositiveIntegerField` 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()
2 changes: 2 additions & 0 deletions src/django_mysql/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
from django_mysql.models.fields import ListCharField
from django_mysql.models.fields import ListTextField
from django_mysql.models.fields import NullBit1BooleanField
from django_mysql.models.fields import PositiveTinyIntegerField
from django_mysql.models.fields import SetCharField
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 import TinyIntegerField
from django_mysql.models.query import ApproximateInt
from django_mysql.models.query import QuerySet
from django_mysql.models.query import QuerySetMixin
Expand Down
4 changes: 4 additions & 0 deletions src/django_mysql/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from django_mysql.models.fields.sets import SetTextField
from django_mysql.models.fields.sizes import SizedBinaryField
from django_mysql.models.fields.sizes import SizedTextField
from django_mysql.models.fields.tiny_integer import PositiveTinyIntegerField
from django_mysql.models.fields.tiny_integer import TinyIntegerField

__all__ = [
"Bit1BooleanField",
Expand All @@ -20,8 +22,10 @@
"ListCharField",
"ListTextField",
"NullBit1BooleanField",
"PositiveTinyIntegerField",
"SetCharField",
"SetTextField",
"SizedBinaryField",
"SizedTextField",
"TinyIntegerField",
]
24 changes: 24 additions & 0 deletions src/django_mysql/models/fields/tiny_integer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

from django.core.validators import MaxValueValidator
from django.core.validators import MinValueValidator
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.models import IntegerField
from django.db.models import PositiveIntegerField
from django.utils.translation import gettext_lazy as _


class TinyIntegerField(IntegerField):
description = _("Small integer")
default_validators = [MinValueValidator(-128), MaxValueValidator(127)]

def db_type(self, connection: BaseDatabaseWrapper) -> str:
return "tinyint"


class PositiveTinyIntegerField(PositiveIntegerField):
description = _("Positive small integer")
default_validators = [MinValueValidator(0), MaxValueValidator(255)]

def db_type(self, connection: BaseDatabaseWrapper) -> str:
return "tinyint unsigned"
7 changes: 7 additions & 0 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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")
Expand Down
101 changes: 101 additions & 0 deletions tests/testapp/test_tiny_integer_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from __future__ import annotations

import pytest
from django.core.management import call_command
from django.core.validators import MaxValueValidator
from django.core.validators import MinValueValidator
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)


class TestFormValidation(TestCase):
def test_signed_validators(self):
validators = TinyIntegerModel._meta.get_field("tiny_signed").validators
assert len(validators) == 2
assert isinstance(validators[0], MinValueValidator)
assert validators[0].limit_value == -128
assert isinstance(validators[1], MaxValueValidator)
assert validators[1].limit_value == 127

def test_unsigned_validators(self):
validators = TinyIntegerModel._meta.get_field("tiny_unsigned").validators
assert len(validators) == 2
assert isinstance(validators[0], MinValueValidator)
assert validators[0].limit_value == 0
assert isinstance(validators[1], MaxValueValidator)
assert validators[1].limit_value == 255
38 changes: 38 additions & 0 deletions tests/testapp/tinyinteger_default_migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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,),
)
]
Empty file.