Fixed #30661 -- Added models.SmallAutoField.

This commit is contained in:
Nick Pope 2019-07-26 22:05:22 +01:00 committed by Mariusz Felisiak
parent 955b382600
commit 194d1dfc18
28 changed files with 177 additions and 26 deletions

View File

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

View File

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

View File

@ -182,7 +182,7 @@ class BaseDatabaseSchemaEditor:
)) ))
# Autoincrement SQL (for backends with post table definition # Autoincrement SQL (for backends with post table definition
# variant). # 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) autoinc_sql = self.connection.ops.autoinc_sql(model._meta.db_table, field.column)
if autoinc_sql: if autoinc_sql:
self.deferred_sql.extend(autoinc_sql) self.deferred_sql.extend(autoinc_sql)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,7 +90,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
# Make a new field that's like the new one but with a temporary # Make a new field that's like the new one but with a temporary
# column name. # column name.
new_temp_field = copy.deepcopy(new_field) 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) new_temp_field.column = self._generate_temp_name(new_field.column)
# Add it # Add it
self.add_field(model, new_temp_field) self.add_field(model, new_temp_field)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,8 +38,8 @@ __all__ = [
'EmailField', 'Empty', 'Field', 'FieldDoesNotExist', 'FilePathField', 'EmailField', 'Empty', 'Field', 'FieldDoesNotExist', 'FilePathField',
'FloatField', 'GenericIPAddressField', 'IPAddressField', 'IntegerField', 'FloatField', 'GenericIPAddressField', 'IPAddressField', 'IntegerField',
'NOT_PROVIDED', 'NullBooleanField', 'PositiveIntegerField', 'NOT_PROVIDED', 'NullBooleanField', 'PositiveIntegerField',
'PositiveSmallIntegerField', 'SlugField', 'SmallIntegerField', 'TextField', 'PositiveSmallIntegerField', 'SlugField', 'SmallAutoField',
'TimeField', 'URLField', 'UUIDField', 'SmallIntegerField', 'TextField', 'TimeField', 'URLField', 'UUIDField',
] ]
@ -985,6 +985,16 @@ class BigAutoField(AutoField):
return BigIntegerField().db_type(connection=connection) 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): class BooleanField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { default_error_messages = {

View File

@ -1085,6 +1085,17 @@ It uses :class:`~django.core.validators.validate_slug` or
If ``True``, the field accepts Unicode letters in addition to ASCII If ``True``, the field accepts Unicode letters in addition to ASCII
letters. Defaults to ``False``. 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`` ``SmallIntegerField``
--------------------- ---------------------

View File

@ -294,6 +294,11 @@ Models
* :class:`~django.db.models.Avg` and :class:`~django.db.models.Sum` now support * :class:`~django.db.models.Avg` and :class:`~django.db.models.Sum` now support
the ``distinct`` argument. 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 Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

View File

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

View File

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

View File

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

View File

@ -5,7 +5,9 @@ from django.db.models import Index
from django.db.utils import DatabaseError from django.db.utils import DatabaseError
from django.test import TransactionTestCase, skipUnlessDBFeature 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): class IntrospectionTests(TransactionTestCase):
@ -115,6 +117,15 @@ class IntrospectionTests(TransactionTestCase):
[connection.introspection.get_field_type(r[1], r) for r in desc], [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 # Regression test for #9991 - 'real' types in postgres
@skipUnlessDBFeature('has_real_datatype') @skipUnlessDBFeature('has_real_datatype')
def test_postgresql_real_type(self): def test_postgresql_real_type(self):

View File

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

View File

@ -8,8 +8,9 @@ from django.test import TestCase
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
from .models import ( from .models import (
Article, Category, Child, ChildNullableParent, City, District, First, Article, Category, Child, ChildNullableParent, City, Country, District,
Parent, Record, Relation, Reporter, School, Student, Third, ToFieldChild, 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='Brooklyn')
District.objects.create(city=ny, name='Manhattan') 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): def test_multiple_foreignkeys(self):
# Test of multiple ForeignKeys to the same model (bug #7125). # Test of multiple ForeignKeys to the same model (bug #7125).
c1 = Category.objects.create(name='First') c1 = Category.objects.create(name='First')

View File

@ -2651,9 +2651,13 @@ class OperationTests(OperationTestBase):
fill_data.state_forwards("fill_data", new_state) fill_data.state_forwards("fill_data", new_state)
fill_data.database_forwards("fill_data", editor, project_state, 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): def create_initial_data(models, schema_editor):
Article = models.get_model("test_article", "Article") Article = models.get_model("test_article", "Article")
@ -2665,14 +2669,14 @@ class OperationTests(OperationTestBase):
def create_big_data(models, schema_editor): def create_big_data(models, schema_editor):
Article = models.get_model("test_article", "Article") Article = models.get_model("test_article", "Article")
Blog = models.get_model("test_blog", "Blog") 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(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( create_blog = migrations.CreateModel(
"Blog", "Blog",
[ [
("id", models.AutoField(primary_key=True)), ("id", source_field(primary_key=True)),
("name", models.CharField(max_length=100)), ("name", models.CharField(max_length=100)),
], ],
options={}, options={},
@ -2680,7 +2684,7 @@ class OperationTests(OperationTestBase):
create_article = migrations.CreateModel( create_article = migrations.CreateModel(
"Article", "Article",
[ [
("id", models.AutoField(primary_key=True)), ("id", source_field(primary_key=True)),
("blog", models.ForeignKey(to="test_blog.Blog", on_delete=models.CASCADE)), ("blog", models.ForeignKey(to="test_blog.Blog", on_delete=models.CASCADE)),
("name", models.CharField(max_length=100)), ("name", models.CharField(max_length=100)),
("data", models.TextField(default="")), ("data", models.TextField(default="")),
@ -2690,8 +2694,8 @@ class OperationTests(OperationTestBase):
fill_initial_data = migrations.RunPython(create_initial_data, create_initial_data) fill_initial_data = migrations.RunPython(create_initial_data, create_initial_data)
fill_big_data = migrations.RunPython(create_big_data, create_big_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_article_id = migrations.AlterField('Article', 'id', target_field(primary_key=True))
grow_blog_id = migrations.AlterField("Blog", "id", models.BigAutoField(primary_key=True)) grow_blog_id = migrations.AlterField('Blog', 'id', target_field(primary_key=True))
project_state = ProjectState() project_state = ProjectState()
new_state = project_state.clone() new_state = project_state.clone()
@ -2719,7 +2723,7 @@ class OperationTests(OperationTestBase):
state = new_state.clone() state = new_state.clone()
article = state.apps.get_model("test_article.Article") 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 project_state = new_state
new_state = new_state.clone() new_state = new_state.clone()
@ -2729,7 +2733,7 @@ class OperationTests(OperationTestBase):
state = new_state.clone() state = new_state.clone()
blog = state.apps.get_model("test_blog.Blog") 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 project_state = new_state
new_state = new_state.clone() 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.state_forwards("fill_big_data", new_state)
fill_big_data.database_forwards("fill_big_data", editor, project_state, 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): def test_run_python_noop(self):
""" """
#24098 - Tests no-op RunPython operations. #24098 - Tests no-op RunPython operations.

View File

@ -14,7 +14,8 @@ from django.db.models.deletion import CASCADE, PROTECT
from django.db.models.fields import ( from django.db.models.fields import (
AutoField, BigAutoField, BigIntegerField, BinaryField, BooleanField, AutoField, BigAutoField, BigIntegerField, BinaryField, BooleanField,
CharField, DateField, DateTimeField, IntegerField, PositiveIntegerField, CharField, DateField, DateTimeField, IntegerField, PositiveIntegerField,
SlugField, TextField, TimeField, UUIDField, SlugField, SmallAutoField, SmallIntegerField, TextField, TimeField,
UUIDField,
) )
from django.db.models.fields.related import ( from django.db.models.fields.related import (
ForeignKey, ForeignObject, ManyToManyField, OneToOneField, ForeignKey, ForeignObject, ManyToManyField, OneToOneField,
@ -1179,6 +1180,28 @@ class SchemaTests(TransactionTestCase):
# Fail on PostgreSQL if sequence is missing an owner. # Fail on PostgreSQL if sequence is missing an owner.
self.assertIsNotNone(Author.objects.create(name='Bar')) 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): def test_alter_int_pk_to_autofield_pk(self):
""" """
Should be able to rename an IntegerField(primary_key=True) to Should be able to rename an IntegerField(primary_key=True) to
@ -1211,6 +1234,28 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field(IntegerPK, old_field, new_field, strict=True) 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): def test_alter_int_pk_to_int_unique(self):
""" """
Should be able to rename an IntegerField(primary_key=True) to Should be able to rename an IntegerField(primary_key=True) to