Skip to content

Commit f53978a

Browse files
Add tiny integer fields (#1123)
Co-authored-by: Adam Johnson <[email protected]>
1 parent c3f9d97 commit f53978a

File tree

11 files changed

+251
-0
lines changed

11 files changed

+251
-0
lines changed

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
Changelog
33
=========
44

5+
* Added field classes :class:`~django_mysql.models.TinyIntegerField` and
6+
:class:`~django_mysql.models.PositiveTinyIntegerField` that use MySQL’s one-byte
7+
``TINYINT`` data type.
8+
59
4.16.0 (2025-02-06)
610
-------------------
711

docs/exposition.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,26 @@ field class allows you to interact with those fields:
196196
197197
:ref:`Read more <bit1booleanfields>`
198198

199+
Tiny integer fields
200+
-------------------
201+
202+
MySQL’s ``TINYINT`` type stores small integers efficiently, using just one byte.
203+
Django-MySQL provides field classes for the ``TINYINT`` type:
204+
205+
.. code-block:: python
206+
207+
from django.db import models
208+
from django_mysql.models import TinyIntegerField, PositiveTinyIntegerField
209+
210+
211+
class TinyIntModel(models.Model):
212+
# Supports values from -128 to 127:
213+
tiny_value = TinyIntegerField()
214+
# Supports values from 0 to 255:
215+
positive_tiny_value = PositiveTinyIntegerField()
216+
217+
:ref:`Read more <tiny-integer-fields>`
218+
199219
-------------
200220
Field Lookups
201221
-------------

docs/model_fields/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ to the home of Django's native fields in ``django.db.models``.
2020
fixedchar_field
2121
resizable_text_binary_fields
2222
null_bit1_boolean_fields
23+
tiny_integer_fields
2324

2425
.. currentmodule:: django_mysql.models
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.. _tiny-integer-fields:
2+
3+
-------------------
4+
Tiny integer fields
5+
-------------------
6+
7+
.. currentmodule:: django_mysql.models
8+
9+
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.
10+
MySQL’s smallest integer data type, ``TINYINT``, is 1 byte, half the size!
11+
The below field classes allow you to use the ``TINYINT`` and ``TINYINT UNSIGNED`` types in Django.
12+
13+
.. |SmallIntegerField| replace:: ``SmallIntegerField``
14+
__ https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.SmallIntegerField
15+
16+
Docs:
17+
`MySQL TINYINT <https://dev.mysql.com/doc/refman/en/integer-types.html>`_ /
18+
`MariaDB <https://mariadb.com/kb/en/tinyint/>`_.
19+
20+
.. class:: TinyIntegerField(**kwargs)
21+
22+
A subclass of Django’s :class:`~django.db.models.IntegerField` that uses a MySQL ``TINYINT`` type for storage.
23+
It supports signed integer values ranging from -128 to 127.
24+
25+
Example:
26+
27+
.. code-block:: python
28+
29+
from django.db import models
30+
from myapp.fields import TinyIntegerField
31+
32+
33+
class ExampleModel(models.Model):
34+
tiny_value = TinyIntegerField()
35+
36+
.. class:: PositiveTinyIntegerField(**kwargs)
37+
38+
A subclass of Django’s :class:`~django.db.models.PositiveIntegerField` that uses a MySQL ``TINYINT UNSIGNED`` type for storage.
39+
It supports unsigned integer values ranging from 0 to 255.
40+
41+
Example:
42+
43+
.. code-block:: python
44+
45+
from django.db import models
46+
from myapp.fields import PositiveTinyIntegerField
47+
48+
49+
class ExampleModel(models.Model):
50+
positive_tiny_value = PositiveTinyIntegerField()

src/django_mysql/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
from django_mysql.models.fields import ListCharField
1515
from django_mysql.models.fields import ListTextField
1616
from django_mysql.models.fields import NullBit1BooleanField
17+
from django_mysql.models.fields import PositiveTinyIntegerField
1718
from django_mysql.models.fields import SetCharField
1819
from django_mysql.models.fields import SetTextField
1920
from django_mysql.models.fields import SizedBinaryField
2021
from django_mysql.models.fields import SizedTextField
22+
from django_mysql.models.fields import TinyIntegerField
2123
from django_mysql.models.query import ApproximateInt
2224
from django_mysql.models.query import QuerySet
2325
from django_mysql.models.query import QuerySetMixin

src/django_mysql/models/fields/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from django_mysql.models.fields.sets import SetTextField
1212
from django_mysql.models.fields.sizes import SizedBinaryField
1313
from django_mysql.models.fields.sizes import SizedTextField
14+
from django_mysql.models.fields.tiny_integer import PositiveTinyIntegerField
15+
from django_mysql.models.fields.tiny_integer import TinyIntegerField
1416

1517
__all__ = [
1618
"Bit1BooleanField",
@@ -20,8 +22,10 @@
2022
"ListCharField",
2123
"ListTextField",
2224
"NullBit1BooleanField",
25+
"PositiveTinyIntegerField",
2326
"SetCharField",
2427
"SetTextField",
2528
"SizedBinaryField",
2629
"SizedTextField",
30+
"TinyIntegerField",
2731
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from django.core.validators import MaxValueValidator
4+
from django.core.validators import MinValueValidator
5+
from django.db.backends.base.base import BaseDatabaseWrapper
6+
from django.db.models import IntegerField
7+
from django.db.models import PositiveIntegerField
8+
from django.utils.translation import gettext_lazy as _
9+
10+
11+
class TinyIntegerField(IntegerField):
12+
description = _("Small integer")
13+
default_validators = [MinValueValidator(-128), MaxValueValidator(127)]
14+
15+
def db_type(self, connection: BaseDatabaseWrapper) -> str:
16+
return "tinyint"
17+
18+
19+
class PositiveTinyIntegerField(PositiveIntegerField):
20+
description = _("Positive small integer")
21+
default_validators = [MinValueValidator(0), MaxValueValidator(255)]
22+
23+
def db_type(self, connection: BaseDatabaseWrapper) -> str:
24+
return "tinyint unsigned"

tests/testapp/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
from django_mysql.models import ListCharField
2727
from django_mysql.models import ListTextField
2828
from django_mysql.models import Model
29+
from django_mysql.models import PositiveTinyIntegerField
2930
from django_mysql.models import SetCharField
3031
from django_mysql.models import SetTextField
3132
from django_mysql.models import SizedBinaryField
3233
from django_mysql.models import SizedTextField
34+
from django_mysql.models import TinyIntegerField
3335
from tests.testapp.utils import conn_is_mysql
3436

3537

@@ -145,6 +147,11 @@ class FixedCharModel(Model):
145147
zip_code = FixedCharField(max_length=10)
146148

147149

150+
class TinyIntegerModel(Model):
151+
tiny_signed = TinyIntegerField(null=True)
152+
tiny_unsigned = PositiveTinyIntegerField(null=True)
153+
154+
148155
class Author(Model):
149156
name = CharField(max_length=32, db_index=True)
150157
tutor = ForeignKey("self", on_delete=CASCADE, null=True, related_name="tutees")
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
from django.core.management import call_command
5+
from django.core.validators import MaxValueValidator
6+
from django.core.validators import MinValueValidator
7+
from django.db import connection
8+
from django.db.utils import DataError
9+
from django.test import TestCase
10+
from django.test import TransactionTestCase
11+
from django.test import override_settings
12+
13+
from tests.testapp.models import TinyIntegerModel
14+
15+
16+
class TestSaveLoad(TestCase):
17+
def test_success(self):
18+
TinyIntegerModel.objects.create(tiny_signed=-128, tiny_unsigned=0)
19+
TinyIntegerModel.objects.create(tiny_signed=127, tiny_unsigned=255)
20+
21+
def test_invalid_too_long_signed(self):
22+
with pytest.raises(DataError) as excinfo:
23+
TinyIntegerModel.objects.create(tiny_signed=128)
24+
25+
assert excinfo.value.args == (
26+
1264,
27+
"Out of range value for column 'tiny_signed' at row 1",
28+
)
29+
30+
def test_invalid_too_long_unsigned(self):
31+
with pytest.raises(DataError) as excinfo:
32+
TinyIntegerModel.objects.create(tiny_unsigned=256)
33+
34+
assert excinfo.value.args == (
35+
1264,
36+
"Out of range value for column 'tiny_unsigned' at row 1",
37+
)
38+
39+
def test_invalid_too_short_signed(self):
40+
with pytest.raises(DataError) as excinfo:
41+
TinyIntegerModel.objects.create(tiny_signed=-129)
42+
43+
assert excinfo.value.args == (
44+
1264,
45+
"Out of range value for column 'tiny_signed' at row 1",
46+
)
47+
48+
def test_invalid_too_short_unsigned(self):
49+
with pytest.raises(DataError) as excinfo:
50+
TinyIntegerModel.objects.create(tiny_unsigned=-1)
51+
52+
assert excinfo.value.args == (
53+
1264,
54+
"Out of range value for column 'tiny_unsigned' at row 1",
55+
)
56+
57+
58+
class TestMigrations(TransactionTestCase):
59+
@override_settings(
60+
MIGRATION_MODULES={"testapp": "tests.testapp.tinyinteger_default_migrations"}
61+
)
62+
def test_adding_field_with_default(self):
63+
table_name = "testapp_tinyintegerdefaultmodel"
64+
table_names = connection.introspection.table_names
65+
with connection.cursor() as cursor:
66+
assert table_name not in table_names(cursor)
67+
68+
call_command(
69+
"migrate", "testapp", verbosity=0, skip_checks=True, interactive=False
70+
)
71+
with connection.cursor() as cursor:
72+
assert table_name in table_names(cursor)
73+
74+
call_command(
75+
"migrate",
76+
"testapp",
77+
"zero",
78+
verbosity=0,
79+
skip_checks=True,
80+
interactive=False,
81+
)
82+
with connection.cursor() as cursor:
83+
assert table_name not in table_names(cursor)
84+
85+
86+
class TestFormValidation(TestCase):
87+
def test_signed_validators(self):
88+
validators = TinyIntegerModel._meta.get_field("tiny_signed").validators
89+
assert len(validators) == 2
90+
assert isinstance(validators[0], MinValueValidator)
91+
assert validators[0].limit_value == -128
92+
assert isinstance(validators[1], MaxValueValidator)
93+
assert validators[1].limit_value == 127
94+
95+
def test_unsigned_validators(self):
96+
validators = TinyIntegerModel._meta.get_field("tiny_unsigned").validators
97+
assert len(validators) == 2
98+
assert isinstance(validators[0], MinValueValidator)
99+
assert validators[0].limit_value == 0
100+
assert isinstance(validators[1], MaxValueValidator)
101+
assert validators[1].limit_value == 255
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
from django.db import migrations
4+
from django.db import models
5+
6+
from django_mysql.models import PositiveTinyIntegerField
7+
from django_mysql.models import TinyIntegerField
8+
9+
10+
class Migration(migrations.Migration):
11+
dependencies: list[tuple[str, str]] = []
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="TinyIntegerDefaultModel",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
verbose_name="ID",
21+
serialize=False,
22+
auto_created=True,
23+
primary_key=True,
24+
),
25+
),
26+
(
27+
"tiny_signed",
28+
PositiveTinyIntegerField(),
29+
),
30+
(
31+
"tiny_unsigned",
32+
TinyIntegerField(),
33+
),
34+
],
35+
options={},
36+
bases=(models.Model,),
37+
)
38+
]

tests/testapp/tinyinteger_default_migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)