Skip to content
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.