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

Add tiny integer field #1123

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Changelog
=========

* Added field class ``PositiveTinyIntegerField`` and ``TinyIntegerField`` that uses MySQL's ``TINYINT`` data type.

4.15.0 (2024-10-29)
-------------------

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

:ref:`Read more <bit1booleanfields>`

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 <tinyintegerfields>`


-------------
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_field

.. currentmodule:: django_mysql.models
54 changes: 54 additions & 0 deletions docs/model_fields/tiny_integer_field.rst
Original file line number Diff line number Diff line change
@@ -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 <https://dev.mysql.com/doc/refman/en/numeric-types.html>`_ /
`MariaDB <https://mariadb.com/kb/en/tinyint/>`_.

.. 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.
2 changes: 2 additions & 0 deletions src/django_mysql/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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
Expand All @@ -20,8 +22,10 @@
"ListCharField",
"ListTextField",
"NullBit1BooleanField",
"PositiveTinyIntegerField",
"SetCharField",
"SetTextField",
"SizedBinaryField",
"SizedTextField",
"TinyIntegerField",
]
20 changes: 20 additions & 0 deletions src/django_mysql/models/fields/integer.py
Original file line number Diff line number Diff line change
@@ -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"
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
81 changes: 81 additions & 0 deletions tests/testapp/test_tinyintegerfield.py
Original file line number Diff line number Diff line change
@@ -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)
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.