Browse Source

Fixed #30661 -- Added models.SmallAutoField.

Nick Pope 5 years ago
parent
commit
194d1dfc18

+ 1 - 0
django/contrib/gis/utils/layermapping.py

@@ -61,6 +61,7 @@ class LayerMapping:
     FIELD_TYPES = {
         models.AutoField: OFTInteger,
         models.BigAutoField: OFTInteger64,
+        models.SmallAutoField: OFTInteger,
         models.BooleanField: (OFTInteger, OFTReal, OFTString),
         models.IntegerField: (OFTInteger, OFTReal, OFTString),
         models.FloatField: (OFTInteger, OFTReal),

+ 3 - 2
django/db/backends/base/features.py

@@ -143,9 +143,10 @@ class BaseDatabaseFeatures:
     # Can the backend introspect a TimeField, instead of a DateTimeField?
     can_introspect_time_field = True
 
-    # Some backends may not be able to differentiate BigAutoField from other
-    # fields such as AutoField.
+    # Some backends may not be able to differentiate BigAutoField or
+    # SmallAutoField from other fields such as AutoField.
     introspected_big_auto_field_type = 'BigAutoField'
+    introspected_small_auto_field_type = 'SmallAutoField'
 
     # Some backends may not be able to differentiate BooleanField from other
     # fields such as IntegerField.

+ 1 - 1
django/db/backends/base/schema.py

@@ -182,7 +182,7 @@ class BaseDatabaseSchemaEditor:
             ))
             # Autoincrement SQL (for backends with post table definition
             # variant).
-            if field.get_internal_type() in ('AutoField', 'BigAutoField'):
+            if field.get_internal_type() in ('AutoField', 'BigAutoField', 'SmallAutoField'):
                 autoinc_sql = self.connection.ops.autoinc_sql(model._meta.db_table, field.column)
                 if autoinc_sql:
                     self.deferred_sql.extend(autoinc_sql)

+ 1 - 0
django/db/backends/mysql/base.py

@@ -123,6 +123,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         'PositiveIntegerField': 'integer UNSIGNED',
         'PositiveSmallIntegerField': 'smallint UNSIGNED',
         'SlugField': 'varchar(%(max_length)s)',
+        'SmallAutoField': 'smallint AUTO_INCREMENT',
         'SmallIntegerField': 'smallint',
         'TextField': 'longtext',
         'TimeField': 'time(6)',

+ 2 - 0
django/db/backends/mysql/introspection.py

@@ -44,6 +44,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
                 return 'AutoField'
             elif field_type == 'BigIntegerField':
                 return 'BigAutoField'
+            elif field_type == 'SmallIntegerField':
+                return 'SmallAutoField'
         if description.is_unsigned:
             if field_type == 'IntegerField':
                 return 'PositiveIntegerField'

+ 1 - 0
django/db/backends/mysql/operations.py

@@ -19,6 +19,7 @@ class DatabaseOperations(BaseDatabaseOperations):
     cast_data_types = {
         'AutoField': 'signed integer',
         'BigAutoField': 'signed integer',
+        'SmallAutoField': 'signed integer',
         'CharField': 'char(%(max_length)s)',
         'DecimalField': 'decimal(%(max_digits)s, %(decimal_places)s)',
         'TextField': 'char',

+ 1 - 0
django/db/backends/oracle/base.py

@@ -123,6 +123,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         'PositiveIntegerField': 'NUMBER(11)',
         'PositiveSmallIntegerField': 'NUMBER(11)',
         'SlugField': 'NVARCHAR2(%(max_length)s)',
+        'SmallAutoField': 'NUMBER(5) GENERATED BY DEFAULT ON NULL AS IDENTITY',
         'SmallIntegerField': 'NUMBER(11)',
         'TextField': 'NCLOB',
         'TimeField': 'TIMESTAMP',

+ 2 - 0
django/db/backends/oracle/introspection.py

@@ -35,6 +35,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
             if scale == 0:
                 if precision > 11:
                     return 'BigAutoField' if description.is_autofield else 'BigIntegerField'
+                elif 1 < precision < 6 and description.is_autofield:
+                    return 'SmallAutoField'
                 elif precision == 1:
                     return 'BooleanField'
                 elif description.is_autofield:

+ 1 - 0
django/db/backends/oracle/operations.py

@@ -56,6 +56,7 @@ END;
     cast_data_types = {
         'AutoField': 'NUMBER(11)',
         'BigAutoField': 'NUMBER(19)',
+        'SmallAutoField': 'NUMBER(5)',
         'TextField': cast_char_field_without_max_length,
     }
 

+ 1 - 1
django/db/backends/oracle/schema.py

@@ -90,7 +90,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
         # Make a new field that's like the new one but with a temporary
         # column name.
         new_temp_field = copy.deepcopy(new_field)
-        new_temp_field.null = (new_field.get_internal_type() not in ('AutoField', 'BigAutoField'))
+        new_temp_field.null = (new_field.get_internal_type() not in ('AutoField', 'BigAutoField', 'SmallAutoField'))
         new_temp_field.column = self._generate_temp_name(new_field.column)
         # Add it
         self.add_field(model, new_temp_field)

+ 1 - 0
django/db/backends/postgresql/base.py

@@ -92,6 +92,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         'PositiveIntegerField': 'integer',
         'PositiveSmallIntegerField': 'smallint',
         'SlugField': 'varchar(%(max_length)s)',
+        'SmallAutoField': 'smallserial',
         'SmallIntegerField': 'smallint',
         'TextField': 'text',
         'TimeField': 'time',

+ 2 - 0
django/db/backends/postgresql/introspection.py

@@ -37,6 +37,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
                 return 'AutoField'
             elif field_type == 'BigIntegerField':
                 return 'BigAutoField'
+            elif field_type == 'SmallIntegerField':
+                return 'SmallAutoField'
         return field_type
 
     def get_table_list(self, cursor):

+ 1 - 0
django/db/backends/postgresql/operations.py

@@ -11,6 +11,7 @@ class DatabaseOperations(BaseDatabaseOperations):
     cast_data_types = {
         'AutoField': 'integer',
         'BigAutoField': 'bigint',
+        'SmallAutoField': 'smallint',
     }
 
     def unification_cast_sql(self, output_field):

+ 3 - 3
django/db/backends/postgresql/schema.py

@@ -69,15 +69,15 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
             self.sql_alter_column_type += ' USING %(column)s::%(type)s'
         # Make ALTER TYPE with SERIAL make sense.
         table = strip_quotes(model._meta.db_table)
-        if new_type.lower() in ("serial", "bigserial"):
+        serial_fields_map = {'bigserial': 'bigint', 'serial': 'integer', 'smallserial': 'smallint'}
+        if new_type.lower() in serial_fields_map:
             column = strip_quotes(new_field.column)
             sequence_name = "%s_%s_seq" % (table, column)
-            col_type = "integer" if new_type.lower() == "serial" else "bigint"
             return (
                 (
                     self.sql_alter_column_type % {
                         "column": self.quote_name(column),
-                        "type": col_type,
+                        "type": serial_fields_map[new_type.lower()],
                     },
                     [],
                 ),

+ 2 - 0
django/db/backends/sqlite3/base.py

@@ -104,6 +104,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         'PositiveIntegerField': 'integer unsigned',
         'PositiveSmallIntegerField': 'smallint unsigned',
         'SlugField': 'varchar(%(max_length)s)',
+        'SmallAutoField': 'integer',
         'SmallIntegerField': 'smallint',
         'TextField': 'text',
         'TimeField': 'time',
@@ -116,6 +117,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
     data_types_suffix = {
         'AutoField': 'AUTOINCREMENT',
         'BigAutoField': 'AUTOINCREMENT',
+        'SmallAutoField': 'AUTOINCREMENT',
     }
     # SQLite requires LIKE statements to include an ESCAPE clause if the value
     # being escaped has a percent or underscore in it.

+ 1 - 0
django/db/backends/sqlite3/features.py

@@ -19,6 +19,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     can_introspect_positive_integer_field = True
     can_introspect_small_integer_field = True
     introspected_big_auto_field_type = 'AutoField'
+    introspected_small_auto_field_type = 'AutoField'
     supports_transactions = True
     atomic_transactions = False
     can_rollback_ddl = True

+ 3 - 3
django/db/backends/sqlite3/introspection.py

@@ -57,9 +57,9 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
 
     def get_field_type(self, data_type, description):
         field_type = super().get_field_type(data_type, description)
-        if description.pk and field_type in {'BigIntegerField', 'IntegerField'}:
-            # No support for BigAutoField as SQLite treats all integer primary
-            # keys as signed 64-bit integers.
+        if description.pk and field_type in {'BigIntegerField', 'IntegerField', 'SmallIntegerField'}:
+            # No support for BigAutoField or SmallAutoField as SQLite treats
+            # all integer primary keys as signed 64-bit integers.
             return 'AutoField'
         return field_type
 

+ 12 - 2
django/db/models/fields/__init__.py

@@ -38,8 +38,8 @@ __all__ = [
     'EmailField', 'Empty', 'Field', 'FieldDoesNotExist', 'FilePathField',
     'FloatField', 'GenericIPAddressField', 'IPAddressField', 'IntegerField',
     'NOT_PROVIDED', 'NullBooleanField', 'PositiveIntegerField',
-    'PositiveSmallIntegerField', 'SlugField', 'SmallIntegerField', 'TextField',
-    'TimeField', 'URLField', 'UUIDField',
+    'PositiveSmallIntegerField', 'SlugField', 'SmallAutoField',
+    'SmallIntegerField', 'TextField', 'TimeField', 'URLField', 'UUIDField',
 ]
 
 
@@ -985,6 +985,16 @@ class BigAutoField(AutoField):
         return BigIntegerField().db_type(connection=connection)
 
 
+class SmallAutoField(AutoField):
+    description = _('Small integer')
+
+    def get_internal_type(self):
+        return 'SmallAutoField'
+
+    def rel_db_type(self, connection):
+        return SmallIntegerField().db_type(connection=connection)
+
+
 class BooleanField(Field):
     empty_strings_allowed = False
     default_error_messages = {

+ 11 - 0
docs/ref/models/fields.txt

@@ -1085,6 +1085,17 @@ It uses :class:`~django.core.validators.validate_slug` or
     If ``True``, the field accepts Unicode letters in addition to ASCII
     letters. Defaults to ``False``.
 
+``SmallAutoField``
+------------------
+
+.. class:: SmallAutoField(**options)
+
+.. versionadded:: 3.0
+
+Like an :class:`AutoField`, but only allows values under a certain
+(database-dependent) limit. Values from ``1`` to ``32767`` are safe in all
+databases supported by Django.
+
 ``SmallIntegerField``
 ---------------------
 

+ 5 - 0
docs/releases/3.0.txt

@@ -294,6 +294,11 @@ Models
 * :class:`~django.db.models.Avg` and :class:`~django.db.models.Sum` now support
   the ``distinct`` argument.
 
+* Added :class:`~django.db.models.SmallAutoField` which acts much like an
+  :class:`~django.db.models.AutoField` except that it only allows values under
+  a certain (database-dependent) limit. Values from ``1`` to ``32767`` are safe
+  in all databases supported by Django.
+
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 2 - 0
docs/topics/forms/modelforms.txt

@@ -110,6 +110,8 @@ Model field                         Form field
 
 :class:`SlugField`                  :class:`~django.forms.SlugField`
 
+:class:`SmallAutoField`             Not represented in the form
+
 :class:`SmallIntegerField`          :class:`~django.forms.IntegerField`
 
 :class:`TextField`                  :class:`~django.forms.CharField` with

+ 1 - 0
tests/db_functions/comparison/test_cast.py

@@ -55,6 +55,7 @@ class CastTests(TestCase):
         for field_class in (
             models.AutoField,
             models.BigAutoField,
+            models.SmallAutoField,
             models.IntegerField,
             models.BigIntegerField,
             models.SmallIntegerField,

+ 5 - 0
tests/introspection/models.py

@@ -9,6 +9,11 @@ class City(models.Model):
         return self.name
 
 
+class Country(models.Model):
+    id = models.SmallAutoField(primary_key=True)
+    name = models.CharField(max_length=50)
+
+
 class District(models.Model):
     city = models.ForeignKey(City, models.CASCADE, primary_key=True)
     name = models.CharField(max_length=50)

+ 12 - 1
tests/introspection/tests.py

@@ -5,7 +5,9 @@ from django.db.models import Index
 from django.db.utils import DatabaseError
 from django.test import TransactionTestCase, skipUnlessDBFeature
 
-from .models import Article, ArticleReporter, City, Comment, District, Reporter
+from .models import (
+    Article, ArticleReporter, City, Comment, Country, District, Reporter,
+)
 
 
 class IntrospectionTests(TransactionTestCase):
@@ -115,6 +117,15 @@ class IntrospectionTests(TransactionTestCase):
             [connection.introspection.get_field_type(r[1], r) for r in desc],
         )
 
+    @skipUnlessDBFeature('can_introspect_autofield')
+    def test_smallautofield(self):
+        with connection.cursor() as cursor:
+            desc = connection.introspection.get_table_description(cursor, Country._meta.db_table)
+        self.assertIn(
+            connection.features.introspected_small_auto_field_type,
+            [connection.introspection.get_field_type(r[1], r) for r in desc],
+        )
+
     # Regression test for #9991 - 'real' types in postgres
     @skipUnlessDBFeature('has_real_datatype')
     def test_postgresql_real_type(self):

+ 6 - 0
tests/many_to_one/models.py

@@ -27,8 +27,14 @@ class Article(models.Model):
         return self.headline
 
 
+class Country(models.Model):
+    id = models.SmallAutoField(primary_key=True)
+    name = models.CharField(max_length=50)
+
+
 class City(models.Model):
     id = models.BigAutoField(primary_key=True)
+    country = models.ForeignKey(Country, models.CASCADE, related_name='cities', null=True)
     name = models.CharField(max_length=50)
 
     def __str__(self):

+ 12 - 2
tests/many_to_one/tests.py

@@ -8,8 +8,9 @@ from django.test import TestCase
 from django.utils.translation import gettext_lazy
 
 from .models import (
-    Article, Category, Child, ChildNullableParent, City, District, First,
-    Parent, Record, Relation, Reporter, School, Student, Third, ToFieldChild,
+    Article, Category, Child, ChildNullableParent, City, Country, District,
+    First, Parent, Record, Relation, Reporter, School, Student, Third,
+    ToFieldChild,
 )
 
 
@@ -576,6 +577,15 @@ class ManyToOneTests(TestCase):
         District.objects.create(city=ny, name='Brooklyn')
         District.objects.create(city=ny, name='Manhattan')
 
+    def test_fk_to_smallautofield(self):
+        us = Country.objects.create(name='United States')
+        City.objects.create(country=us, name='Chicago')
+        City.objects.create(country=us, name='New York')
+
+        uk = Country.objects.create(name='United Kingdom', id=2 ** 11)
+        City.objects.create(country=uk, name='London')
+        City.objects.create(country=uk, name='Edinburgh')
+
     def test_multiple_foreignkeys(self):
         # Test of multiple ForeignKeys to the same model (bug #7125).
         c1 = Category.objects.create(name='First')

+ 38 - 10
tests/migrations/test_operations.py

@@ -2651,9 +2651,13 @@ class OperationTests(OperationTestBase):
             fill_data.state_forwards("fill_data", new_state)
             fill_data.database_forwards("fill_data", editor, project_state, new_state)
 
-    def test_autofield_foreignfield_growth(self):
+    def _test_autofield_foreignfield_growth(self, source_field, target_field, target_value):
         """
-        A field may be migrated from AutoField to BigAutoField.
+        A field may be migrated in the following ways:
+
+        - AutoField to BigAutoField
+        - SmallAutoField to AutoField
+        - SmallAutoField to BigAutoField
         """
         def create_initial_data(models, schema_editor):
             Article = models.get_model("test_article", "Article")
@@ -2665,14 +2669,14 @@ class OperationTests(OperationTestBase):
         def create_big_data(models, schema_editor):
             Article = models.get_model("test_article", "Article")
             Blog = models.get_model("test_blog", "Blog")
-            blog2 = Blog.objects.create(name="Frameworks", id=2 ** 33)
+            blog2 = Blog.objects.create(name="Frameworks", id=target_value)
             Article.objects.create(name="Django", blog=blog2)
-            Article.objects.create(id=2 ** 33, name="Django2", blog=blog2)
+            Article.objects.create(id=target_value, name="Django2", blog=blog2)
 
         create_blog = migrations.CreateModel(
             "Blog",
             [
-                ("id", models.AutoField(primary_key=True)),
+                ("id", source_field(primary_key=True)),
                 ("name", models.CharField(max_length=100)),
             ],
             options={},
@@ -2680,7 +2684,7 @@ class OperationTests(OperationTestBase):
         create_article = migrations.CreateModel(
             "Article",
             [
-                ("id", models.AutoField(primary_key=True)),
+                ("id", source_field(primary_key=True)),
                 ("blog", models.ForeignKey(to="test_blog.Blog", on_delete=models.CASCADE)),
                 ("name", models.CharField(max_length=100)),
                 ("data", models.TextField(default="")),
@@ -2690,8 +2694,8 @@ class OperationTests(OperationTestBase):
         fill_initial_data = migrations.RunPython(create_initial_data, create_initial_data)
         fill_big_data = migrations.RunPython(create_big_data, create_big_data)
 
-        grow_article_id = migrations.AlterField("Article", "id", models.BigAutoField(primary_key=True))
-        grow_blog_id = migrations.AlterField("Blog", "id", models.BigAutoField(primary_key=True))
+        grow_article_id = migrations.AlterField('Article', 'id', target_field(primary_key=True))
+        grow_blog_id = migrations.AlterField('Blog', 'id', target_field(primary_key=True))
 
         project_state = ProjectState()
         new_state = project_state.clone()
@@ -2719,7 +2723,7 @@ class OperationTests(OperationTestBase):
 
         state = new_state.clone()
         article = state.apps.get_model("test_article.Article")
-        self.assertIsInstance(article._meta.pk, models.BigAutoField)
+        self.assertIsInstance(article._meta.pk, target_field)
 
         project_state = new_state
         new_state = new_state.clone()
@@ -2729,7 +2733,7 @@ class OperationTests(OperationTestBase):
 
         state = new_state.clone()
         blog = state.apps.get_model("test_blog.Blog")
-        self.assertIsInstance(blog._meta.pk, models.BigAutoField)
+        self.assertIsInstance(blog._meta.pk, target_field)
 
         project_state = new_state
         new_state = new_state.clone()
@@ -2737,6 +2741,30 @@ class OperationTests(OperationTestBase):
             fill_big_data.state_forwards("fill_big_data", new_state)
             fill_big_data.database_forwards("fill_big_data", editor, project_state, new_state)
 
+    def test_autofield__bigautofield_foreignfield_growth(self):
+        """A field may be migrated from AutoField to BigAutoField."""
+        self._test_autofield_foreignfield_growth(
+            models.AutoField,
+            models.BigAutoField,
+            2 ** 33,
+        )
+
+    def test_smallfield_autofield_foreignfield_growth(self):
+        """A field may be migrated from SmallAutoField to AutoField."""
+        self._test_autofield_foreignfield_growth(
+            models.SmallAutoField,
+            models.AutoField,
+            2 ** 22,
+        )
+
+    def test_smallfield_bigautofield_foreignfield_growth(self):
+        """A field may be migrated from SmallAutoField to BigAutoField."""
+        self._test_autofield_foreignfield_growth(
+            models.SmallAutoField,
+            models.BigAutoField,
+            2 ** 33,
+        )
+
     def test_run_python_noop(self):
         """
         #24098 - Tests no-op RunPython operations.

+ 46 - 1
tests/schema/tests.py

@@ -14,7 +14,8 @@ from django.db.models.deletion import CASCADE, PROTECT
 from django.db.models.fields import (
     AutoField, BigAutoField, BigIntegerField, BinaryField, BooleanField,
     CharField, DateField, DateTimeField, IntegerField, PositiveIntegerField,
-    SlugField, TextField, TimeField, UUIDField,
+    SlugField, SmallAutoField, SmallIntegerField, TextField, TimeField,
+    UUIDField,
 )
 from django.db.models.fields.related import (
     ForeignKey, ForeignObject, ManyToManyField, OneToOneField,
@@ -1179,6 +1180,28 @@ class SchemaTests(TransactionTestCase):
         # Fail on PostgreSQL if sequence is missing an owner.
         self.assertIsNotNone(Author.objects.create(name='Bar'))
 
+    def test_alter_autofield_pk_to_smallautofield_pk_sequence_owner(self):
+        """
+        Converting an implicit PK to SmallAutoField(primary_key=True) should
+        keep a sequence owner on PostgreSQL.
+        """
+        with connection.schema_editor() as editor:
+            editor.create_model(Author)
+        old_field = Author._meta.get_field('id')
+        new_field = SmallAutoField(primary_key=True)
+        new_field.set_attributes_from_name('id')
+        new_field.model = Author
+        with connection.schema_editor() as editor:
+            editor.alter_field(Author, old_field, new_field, strict=True)
+
+        Author.objects.create(name='Foo', pk=1)
+        with connection.cursor() as cursor:
+            sequence_reset_sqls = connection.ops.sequence_reset_sql(no_style(), [Author])
+            if sequence_reset_sqls:
+                cursor.execute(sequence_reset_sqls[0])
+        # Fail on PostgreSQL if sequence is missing an owner.
+        self.assertIsNotNone(Author.objects.create(name='Bar'))
+
     def test_alter_int_pk_to_autofield_pk(self):
         """
         Should be able to rename an IntegerField(primary_key=True) to
@@ -1211,6 +1234,28 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(IntegerPK, old_field, new_field, strict=True)
 
+    @isolate_apps('schema')
+    def test_alter_smallint_pk_to_smallautofield_pk(self):
+        """
+        Should be able to rename an SmallIntegerField(primary_key=True) to
+        SmallAutoField(primary_key=True).
+        """
+        class SmallIntegerPK(Model):
+            i = SmallIntegerField(primary_key=True)
+
+            class Meta:
+                app_label = 'schema'
+
+        with connection.schema_editor() as editor:
+            editor.create_model(SmallIntegerPK)
+        self.isolated_local_models = [SmallIntegerPK]
+        old_field = SmallIntegerPK._meta.get_field('i')
+        new_field = SmallAutoField(primary_key=True)
+        new_field.model = SmallIntegerPK
+        new_field.set_attributes_from_name('i')
+        with connection.schema_editor() as editor:
+            editor.alter_field(SmallIntegerPK, old_field, new_field, strict=True)
+
     def test_alter_int_pk_to_int_unique(self):
         """
         Should be able to rename an IntegerField(primary_key=True) to