From 2a7ce34600d0f879e93c9a5e02215948ed3bb6ac Mon Sep 17 00:00:00 2001 From: Alexander Sosnovskiy Date: Thu, 2 Jul 2015 11:43:15 +0300 Subject: [PATCH] Fixed #14286 -- Added models.BigAutoField. --- django/contrib/gis/utils/layermapping.py | 4 +- django/db/backends/base/schema.py | 2 +- django/db/backends/mysql/base.py | 1 + django/db/backends/mysql/introspection.py | 8 +- django/db/backends/oracle/base.py | 1 + django/db/backends/postgresql/base.py | 1 + .../db/backends/postgresql/introspection.py | 7 +- django/db/backends/postgresql/schema.py | 5 +- django/db/backends/sqlite3/base.py | 2 + django/db/models/fields/__init__.py | 26 +++- docs/ref/models/fields.txt | 12 +- docs/releases/1.10.txt | 4 + docs/topics/forms/modelforms.txt | 2 + tests/introspection/models.py | 18 +++ tests/introspection/tests.py | 8 +- tests/many_to_many/models.py | 10 ++ tests/many_to_one/models.py | 18 +++ tests/many_to_one/tests.py | 13 +- tests/migrations/test_operations.py | 138 ++++++++++++++++++ 19 files changed, 260 insertions(+), 20 deletions(-) diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 665d2e3cc6..22d7dd2579 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -15,7 +15,8 @@ from django.contrib.gis.gdal import ( SpatialReference, ) from django.contrib.gis.gdal.field import ( - OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime, + OFTDate, OFTDateTime, OFTInteger, OFTInteger64, OFTReal, OFTString, + OFTTime, ) from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist from django.db import connections, models, router, transaction @@ -60,6 +61,7 @@ class LayerMapping(object): # counterparts. FIELD_TYPES = { models.AutoField: OFTInteger, + models.BigAutoField: OFTInteger64, models.IntegerField: (OFTInteger, OFTReal, OFTString), models.FloatField: (OFTInteger, OFTReal), models.DateField: OFTDate, diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 2aa486c91e..191478a6a3 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -261,7 +261,7 @@ class BaseDatabaseSchemaEditor(object): definition, )) # Autoincrement SQL (for backends with post table definition variant) - if field.get_internal_type() == "AutoField": + if field.get_internal_type() in ("AutoField", "BigAutoField"): 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 6fbf15e42e..f674232ab3 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -153,6 +153,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): # If a column type is set to None, it won't be included in the output. _data_types = { 'AutoField': 'integer AUTO_INCREMENT', + 'BigAutoField': 'bigint AUTO_INCREMENT', 'BinaryField': 'longblob', 'BooleanField': 'bool', 'CharField': 'varchar(%(max_length)s)', diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index 0a22357cf9..3d0f6f3689 100644 --- a/django/db/backends/mysql/introspection.py +++ b/django/db/backends/mysql/introspection.py @@ -38,8 +38,12 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_field_type(self, data_type, description): field_type = super(DatabaseIntrospection, self).get_field_type(data_type, description) - if field_type == 'IntegerField' and 'auto_increment' in description.extra: - return 'AutoField' + if 'auto_increment' in description.extra: + if field_type == 'IntegerField': + return 'AutoField' + elif field_type == 'BigIntegerField': + return 'BigAutoField' + return field_type def get_table_list(self, cursor): diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index d286ad0fff..ef93300737 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -92,6 +92,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): # output (the "qn_" prefix is stripped before the lookup is performed. data_types = { 'AutoField': 'NUMBER(11)', + 'BigAutoField': 'NUMBER(19)', 'BinaryField': 'BLOB', 'BooleanField': 'NUMBER(1)', 'CharField': 'NVARCHAR2(%(max_length)s)', diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 383601e477..7822c66327 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -72,6 +72,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): # If a column type is set to None, it won't be included in the output. data_types = { 'AutoField': 'serial', + 'BigAutoField': 'bigserial', 'BinaryField': 'bytea', 'BooleanField': 'boolean', 'CharField': 'varchar(%(max_length)s)', diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py index 9b3e9074b2..90b7090464 100644 --- a/django/db/backends/postgresql/introspection.py +++ b/django/db/backends/postgresql/introspection.py @@ -46,8 +46,11 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_field_type(self, data_type, description): field_type = super(DatabaseIntrospection, self).get_field_type(data_type, description) - if field_type == 'IntegerField' and description.default and 'nextval' in description.default: - return 'AutoField' + if description.default and 'nextval' in description.default: + if field_type == 'IntegerField': + return 'AutoField' + elif field_type == 'BigIntegerField': + return 'BigAutoField' return field_type def get_table_list(self, cursor): diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py index cc31aadf0a..42d01f002d 100644 --- a/django/db/backends/postgresql/schema.py +++ b/django/db/backends/postgresql/schema.py @@ -54,14 +54,15 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): """ Makes ALTER TYPE with SERIAL make sense. """ - if new_type.lower() == "serial": + if new_type.lower() in ("serial", "bigserial"): column = 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": "integer", + "type": col_type, }, [], ), diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index da9a21cbe9..55b97e8bd2 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -93,6 +93,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): # schema inspection is more useful. data_types = { 'AutoField': 'integer', + 'BigAutoField': 'integer', 'BinaryField': 'BLOB', 'BooleanField': 'bool', 'CharField': 'varchar(%(max_length)s)', @@ -120,6 +121,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): } data_types_suffix = { 'AutoField': 'AUTOINCREMENT', + 'BigAutoField': '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/models/fields/__init__.py b/django/db/models/fields/__init__.py index d16c58d29b..7a2dfdbd7d 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -42,14 +42,14 @@ from django.utils.translation import ugettext_lazy as _ # Avoid "TypeError: Item in ``from list'' not a string" -- unicode_literals # makes these strings unicode __all__ = [str(x) for x in ( - 'AutoField', 'BLANK_CHOICE_DASH', 'BigIntegerField', 'BinaryField', - 'BooleanField', 'CharField', 'CommaSeparatedIntegerField', 'DateField', - 'DateTimeField', 'DecimalField', 'DurationField', 'EmailField', 'Empty', - 'Field', 'FieldDoesNotExist', 'FilePathField', 'FloatField', - 'GenericIPAddressField', 'IPAddressField', 'IntegerField', 'NOT_PROVIDED', - 'NullBooleanField', 'PositiveIntegerField', 'PositiveSmallIntegerField', - 'SlugField', 'SmallIntegerField', 'TextField', 'TimeField', 'URLField', - 'UUIDField', + 'AutoField', 'BLANK_CHOICE_DASH', 'BigAutoField', 'BigIntegerField', + 'BinaryField', 'BooleanField', 'CharField', 'CommaSeparatedIntegerField', + 'DateField', 'DateTimeField', 'DecimalField', 'DurationField', + 'EmailField', 'Empty', 'Field', 'FieldDoesNotExist', 'FilePathField', + 'FloatField', 'GenericIPAddressField', 'IPAddressField', 'IntegerField', + 'NOT_PROVIDED', 'NullBooleanField', 'PositiveIntegerField', + 'PositiveSmallIntegerField', 'SlugField', 'SmallIntegerField', 'TextField', + 'TimeField', 'URLField', 'UUIDField', )] @@ -997,6 +997,16 @@ class AutoField(Field): return None +class BigAutoField(AutoField): + description = _("Big (8 byte) integer") + + def get_internal_type(self): + return "BigAutoField" + + def rel_db_type(self, connection): + return BigIntegerField().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 6320f20e24..0a33e96122 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -388,12 +388,22 @@ according to available IDs. You usually won't need to use this directly; a primary key field will automatically be added to your model if you don't specify otherwise. See :ref:`automatic-primary-key-fields`. +``BigAutoField`` +---------------- + +.. class:: BigAutoField(**options) + +.. versionadded:: 1.10 + +A 64-bit integer, much like an :class:`AutoField` except that it is +guaranteed to fit numbers from ``1`` to ``9223372036854775807``. + ``BigIntegerField`` ------------------- .. class:: BigIntegerField(**options) -A 64 bit integer, much like an :class:`IntegerField` except that it is +A 64-bit integer, much like an :class:`IntegerField` except that it is guaranteed to fit numbers from ``-9223372036854775808`` to ``9223372036854775807``. The default form widget for this field is a :class:`~django.forms.TextInput`. diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index 8564c8286d..077a27cea1 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -242,6 +242,10 @@ Models :class:`~django.db.models.Func`. This attribute can be used to set the number of arguments the function accepts. +* Added :class:`~django.db.models.BigAutoField` which acts much like an + :class:`~django.db.models.AutoField` except that it is guaranteed + to fit numbers from ``1`` to ``9223372036854775807``. + Requests and Responses ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 42594e7002..46a1cbe774 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -56,6 +56,8 @@ Model field Form field =================================== ================================================== :class:`AutoField` Not represented in the form +:class:`BigAutoField` Not represented in the form + :class:`BigIntegerField` :class:`~django.forms.IntegerField` with ``min_value`` set to -9223372036854775808 and ``max_value`` set to 9223372036854775807. diff --git a/tests/introspection/models.py b/tests/introspection/models.py index 9ad52b8f8d..4d961b20be 100644 --- a/tests/introspection/models.py +++ b/tests/introspection/models.py @@ -4,6 +4,24 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible +class City(models.Model): + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + +@python_2_unicode_compatible +class District(models.Model): + city = models.ForeignKey(City, models.CASCADE) + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + @python_2_unicode_compatible class Reporter(models.Model): first_name = models.CharField(max_length=30) diff --git a/tests/introspection/tests.py b/tests/introspection/tests.py index 3cd00060db..9ce5a3f0f6 100644 --- a/tests/introspection/tests.py +++ b/tests/introspection/tests.py @@ -6,7 +6,7 @@ from django.db import connection from django.db.utils import DatabaseError from django.test import TransactionTestCase, mock, skipUnlessDBFeature -from .models import Article, Reporter +from .models import Article, City, Reporter class IntrospectionTests(TransactionTestCase): @@ -103,6 +103,12 @@ class IntrospectionTests(TransactionTestCase): [False, nullable_by_backend, nullable_by_backend, nullable_by_backend, True, True, False] ) + @skipUnlessDBFeature('can_introspect_autofield') + def test_bigautofield(self): + with connection.cursor() as cursor: + desc = connection.introspection.get_table_description(cursor, City._meta.db_table) + self.assertIn('BigAutoField', [datatype(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_many/models.py b/tests/many_to_many/models.py index 5688c853d6..3e4cdd2e70 100644 --- a/tests/many_to_many/models.py +++ b/tests/many_to_many/models.py @@ -23,12 +23,22 @@ class Publication(models.Model): ordering = ('title',) +@python_2_unicode_compatible +class Tag(models.Model): + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + @python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) # Assign a unicode string as name to make sure the intermediary model is # correctly created. Refs #20207 publications = models.ManyToManyField(Publication, name='publications') + tags = models.ManyToManyField(Tag, related_name='tags') def __str__(self): return self.headline diff --git a/tests/many_to_one/models.py b/tests/many_to_one/models.py index 57fc7a617b..05491e99f1 100644 --- a/tests/many_to_one/models.py +++ b/tests/many_to_one/models.py @@ -32,6 +32,24 @@ class Article(models.Model): ordering = ('headline',) +@python_2_unicode_compatible +class City(models.Model): + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + +@python_2_unicode_compatible +class District(models.Model): + city = models.ForeignKey(City, models.CASCADE) + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + # If ticket #1578 ever slips back in, these models will not be able to be # created (the field names being lower-cased versions of their opposite # classes is important here). diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py index 69bba0993d..79caec7177 100644 --- a/tests/many_to_one/tests.py +++ b/tests/many_to_one/tests.py @@ -9,8 +9,8 @@ from django.utils.deprecation import RemovedInDjango20Warning from django.utils.translation import ugettext_lazy from .models import ( - Article, Category, Child, First, Parent, Record, Relation, Reporter, - School, Student, Third, ToFieldChild, + Article, Category, Child, City, District, First, Parent, Record, Relation, + Reporter, School, Student, Third, ToFieldChild, ) @@ -569,6 +569,15 @@ class ManyToOneTests(TestCase): self.assertIsNot(c.parent, p) self.assertEqual(c.parent, p) + def test_fk_to_bigautofield(self): + ch = City.objects.create(name='Chicago') + District.objects.create(city=ch, name='Far South') + District.objects.create(city=ch, name='North') + + ny = City.objects.create(name='New York', id=2 ** 33) + District.objects.create(city=ny, name='Brooklyn') + District.objects.create(city=ny, name='Manhattan') + 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 b624a73392..de32353aa4 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -1806,6 +1806,144 @@ class OperationTests(OperationTestBase): create_old_man.state_forwards("test_books", new_state) create_old_man.database_forwards("test_books", editor, project_state, new_state) + def test_model_with_bigautofield(self): + """ + A model with BigAutoField can be created. + """ + def create_data(models, schema_editor): + Author = models.get_model("test_author", "Author") + Book = models.get_model("test_book", "Book") + author1 = Author.objects.create(name="Hemingway") + Book.objects.create(title="Old Man and The Sea", author=author1) + Book.objects.create(id=2 ** 33, title="A farewell to arms", author=author1) + + author2 = Author.objects.create(id=2 ** 33, name="Remarque") + Book.objects.create(title="All quiet on the western front", author=author2) + Book.objects.create(title="Arc de Triomphe", author=author2) + + create_author = migrations.CreateModel( + "Author", + [ + ("id", models.BigAutoField(primary_key=True)), + ("name", models.CharField(max_length=100)), + ], + options={}, + ) + create_book = migrations.CreateModel( + "Book", + [ + ("id", models.BigAutoField(primary_key=True)), + ("title", models.CharField(max_length=100)), + ("author", models.ForeignKey(to="test_author.Author", on_delete=models.CASCADE)) + ], + options={}, + ) + fill_data = migrations.RunPython(create_data) + + project_state = ProjectState() + new_state = project_state.clone() + with connection.schema_editor() as editor: + create_author.state_forwards("test_author", new_state) + create_author.database_forwards("test_author", editor, project_state, new_state) + + project_state = new_state + new_state = new_state.clone() + with connection.schema_editor() as editor: + create_book.state_forwards("test_book", new_state) + create_book.database_forwards("test_book", editor, project_state, new_state) + + project_state = new_state + new_state = new_state.clone() + with connection.schema_editor() as editor: + 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): + """ + A field may be migrated from AutoField to BigAutoField. + """ + def create_initial_data(models, schema_editor): + Article = models.get_model("test_article", "Article") + Blog = models.get_model("test_blog", "Blog") + blog = Blog.objects.create(name="web development done right") + Article.objects.create(name="Frameworks", blog=blog) + Article.objects.create(name="Programming Languages", blog=blog) + + 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) + Article.objects.create(name="Django", blog=blog2) + Article.objects.create(id=2 ** 33, name="Django2", blog=blog2) + + create_blog = migrations.CreateModel( + "Blog", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=100)), + ], + options={}, + ) + create_article = migrations.CreateModel( + "Article", + [ + ("id", models.AutoField(primary_key=True)), + ("blog", models.ForeignKey(to="test_blog.Blog", on_delete=models.CASCADE)), + ("name", models.CharField(max_length=100)), + ("data", models.TextField(default="")), + ], + options={}, + ) + 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)) + + project_state = ProjectState() + new_state = project_state.clone() + with connection.schema_editor() as editor: + create_blog.state_forwards("test_blog", new_state) + create_blog.database_forwards("test_blog", editor, project_state, new_state) + + project_state = new_state + new_state = new_state.clone() + with connection.schema_editor() as editor: + create_article.state_forwards("test_article", new_state) + create_article.database_forwards("test_article", editor, project_state, new_state) + + project_state = new_state + new_state = new_state.clone() + with connection.schema_editor() as editor: + fill_initial_data.state_forwards("fill_initial_data", new_state) + fill_initial_data.database_forwards("fill_initial_data", editor, project_state, new_state) + + project_state = new_state + new_state = new_state.clone() + with connection.schema_editor() as editor: + grow_article_id.state_forwards("test_article", new_state) + grow_article_id.database_forwards("test_article", editor, project_state, new_state) + + state = new_state.clone() + article = state.apps.get_model("test_article.Article") + self.assertIsInstance(article._meta.pk, models.BigAutoField) + + project_state = new_state + new_state = new_state.clone() + with connection.schema_editor() as editor: + grow_blog_id.state_forwards("test_blog", new_state) + grow_blog_id.database_forwards("test_blog", editor, project_state, new_state) + + state = new_state.clone() + blog = state.apps.get_model("test_blog.Blog") + self.assertTrue(isinstance(blog._meta.pk, models.BigAutoField)) + + project_state = new_state + new_state = new_state.clone() + with connection.schema_editor() as editor: + 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_run_python_noop(self): """ #24098 - Tests no-op RunPython operations.