From 194d1dfc186cc8d2b35dabf64f3ed38b757cbd98 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Fri, 26 Jul 2019 22:05:22 +0100 Subject: [PATCH] Fixed #30661 -- Added models.SmallAutoField. --- django/contrib/gis/utils/layermapping.py | 1 + django/db/backends/base/features.py | 5 +- django/db/backends/base/schema.py | 2 +- django/db/backends/mysql/base.py | 1 + django/db/backends/mysql/introspection.py | 2 + django/db/backends/mysql/operations.py | 1 + django/db/backends/oracle/base.py | 1 + django/db/backends/oracle/introspection.py | 2 + django/db/backends/oracle/operations.py | 1 + django/db/backends/oracle/schema.py | 2 +- django/db/backends/postgresql/base.py | 1 + .../db/backends/postgresql/introspection.py | 2 + django/db/backends/postgresql/operations.py | 1 + django/db/backends/postgresql/schema.py | 6 +-- django/db/backends/sqlite3/base.py | 2 + django/db/backends/sqlite3/features.py | 1 + django/db/backends/sqlite3/introspection.py | 6 +-- django/db/models/fields/__init__.py | 14 +++++- docs/ref/models/fields.txt | 11 +++++ docs/releases/3.0.txt | 5 ++ docs/topics/forms/modelforms.txt | 2 + tests/db_functions/comparison/test_cast.py | 1 + tests/introspection/models.py | 5 ++ tests/introspection/tests.py | 13 ++++- tests/many_to_one/models.py | 6 +++ tests/many_to_one/tests.py | 14 +++++- tests/migrations/test_operations.py | 48 +++++++++++++++---- tests/schema/tests.py | 47 +++++++++++++++++- 28 files changed, 177 insertions(+), 26 deletions(-) diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 0d65d0fe40..9e211184bb 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/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), diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index bb4b59e7c8..70878f92a2 100644 --- a/django/db/backends/base/features.py +++ b/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. diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index a5a4163c19..3540fd5d0d 100644 --- a/django/db/backends/base/schema.py +++ b/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) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 821f24fdf1..9a0ede0ac8 100644 --- a/django/db/backends/mysql/base.py +++ b/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)', diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index db1a387679..05e73c78dc 100644 --- a/django/db/backends/mysql/introspection.py +++ b/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' diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index ef908ad8ae..e940286720 100644 --- a/django/db/backends/mysql/operations.py +++ b/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', diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 0fbe96ab9b..ef33d9fad7 100644 --- a/django/db/backends/oracle/base.py +++ b/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', diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index 1fcba05510..2322ae0b5d 100644 --- a/django/db/backends/oracle/introspection.py +++ b/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: diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 77c568e84b..016d4c9a59 100644 --- a/django/db/backends/oracle/operations.py +++ b/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, } diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py index 3356788a82..d0f664a6ff 100644 --- a/django/db/backends/oracle/schema.py +++ b/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) diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 7e34a3a177..eea8aeb135 100644 --- a/django/db/backends/postgresql/base.py +++ b/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', diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py index 1c9c7e63a5..b7bf18691c 100644 --- a/django/db/backends/postgresql/introspection.py +++ b/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): diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index 0c497edf71..61bac5e55a 100644 --- a/django/db/backends/postgresql/operations.py +++ b/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): diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py index b7f52ccf25..eb5b182680 100644 --- a/django/db/backends/postgresql/schema.py +++ b/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()], }, [], ), diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index fff65197f9..7a4439ae88 100644 --- a/django/db/backends/sqlite3/base.py +++ b/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. diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index 3d52a8036f..18b76f8c86 100644 --- a/django/db/backends/sqlite3/features.py +++ b/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 diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py index faf9328415..5e33d2a7e2 100644 --- a/django/db/backends/sqlite3/introspection.py +++ b/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 diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 1388dffc58..ff686e4f62 100644 --- a/django/db/models/fields/__init__.py +++ b/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 = { diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index ead39f0572..c267501bf4 100644 --- a/docs/ref/models/fields.txt +++ b/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`` --------------------- diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index da1578c0eb..9de85ab770 100644 --- a/docs/releases/3.0.txt +++ b/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 ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 2a13c2d1df..d8b137f624 100644 --- a/docs/topics/forms/modelforms.txt +++ b/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 diff --git a/tests/db_functions/comparison/test_cast.py b/tests/db_functions/comparison/test_cast.py index c5698d6d0e..988eaa4dad 100644 --- a/tests/db_functions/comparison/test_cast.py +++ b/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, diff --git a/tests/introspection/models.py b/tests/introspection/models.py index fa663de2fd..d069c5820e 100644 --- a/tests/introspection/models.py +++ b/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) diff --git a/tests/introspection/tests.py b/tests/introspection/tests.py index 7edb01a4b7..4a7ce11e7a 100644 --- a/tests/introspection/tests.py +++ b/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): diff --git a/tests/many_to_one/models.py b/tests/many_to_one/models.py index 6230129bbd..6e00fb18fa 100644 --- a/tests/many_to_one/models.py +++ b/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): diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py index e3121a7701..a926574b15 100644 --- a/tests/many_to_one/tests.py +++ b/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') diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 3b2129a933..3c321d4cb4 100644 --- a/tests/migrations/test_operations.py +++ b/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. diff --git a/tests/schema/tests.py b/tests/schema/tests.py index a765528d50..7276912c71 100644 --- a/tests/schema/tests.py +++ b/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