diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 8afc7eb516..e0b7372eb1 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -179,6 +179,9 @@ class BaseDatabaseFeatures: # Does it support foreign keys? supports_foreign_keys = True + # Can it create foreign key constraints inline when adding columns? + can_create_inline_fk = True + # Does it support CHECK constraints? supports_column_check_constraints = True supports_table_check_constraints = True diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 113d1b7f67..0522abcaad 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -77,6 +77,7 @@ class BaseDatabaseSchemaEditor: "REFERENCES %(to_table)s (%(to_column)s)%(deferrable)s" ) sql_create_inline_fk = None + sql_create_column_inline_fk = None sql_delete_fk = sql_delete_constraint sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s%(condition)s" @@ -433,6 +434,22 @@ class BaseDatabaseSchemaEditor: db_params = field.db_parameters(connection=self.connection) if db_params['check']: definition += " " + self.sql_check_constraint % db_params + if field.remote_field and self.connection.features.supports_foreign_keys and field.db_constraint: + constraint_suffix = '_fk_%(to_table)s_%(to_column)s' + # Add FK constraint inline, if supported. + if self.sql_create_column_inline_fk: + to_table = field.remote_field.model._meta.db_table + to_column = field.remote_field.model._meta.get_field(field.remote_field.field_name).column + definition += " " + self.sql_create_column_inline_fk % { + 'name': self._fk_constraint_name(model, field, constraint_suffix), + 'column': self.quote_name(field.column), + 'to_table': self.quote_name(to_table), + 'to_column': self.quote_name(to_column), + 'deferrable': self.connection.ops.deferrable_sql() + } + # Otherwise, add FK constraints later. + else: + self.deferred_sql.append(self._create_fk_sql(model, field, constraint_suffix)) # Build the SQL and run it sql = self.sql_create_column % { "table": self.quote_name(model._meta.db_table), @@ -451,9 +468,6 @@ class BaseDatabaseSchemaEditor: self.execute(sql, params) # Add an index, if required self.deferred_sql.extend(self._field_indexes_sql(model, field)) - # Add any FK constraints later - if field.remote_field and self.connection.features.supports_foreign_keys and field.db_constraint: - self.deferred_sql.append(self._create_fk_sql(model, field, "_fk_%(to_table)s_%(to_column)s")) # Reset connection if required if self.connection.features.connection_persists_old_columns: self.connection.close() @@ -984,18 +998,8 @@ class BaseDatabaseSchemaEditor: } def _create_fk_sql(self, model, field, suffix): - def create_fk_name(*args, **kwargs): - return self.quote_name(self._create_index_name(*args, **kwargs)) - table = Table(model._meta.db_table, self.quote_name) - name = ForeignKeyName( - model._meta.db_table, - [field.column], - split_identifier(field.target_field.model._meta.db_table)[1], - [field.target_field.column], - suffix, - create_fk_name, - ) + name = self._fk_constraint_name(model, field, suffix) column = Columns(model._meta.db_table, [field.column], self.quote_name) to_table = Table(field.target_field.model._meta.db_table, self.quote_name) to_column = Columns(field.target_field.model._meta.db_table, [field.target_field.column], self.quote_name) @@ -1010,6 +1014,19 @@ class BaseDatabaseSchemaEditor: deferrable=deferrable, ) + def _fk_constraint_name(self, model, field, suffix): + def create_fk_name(*args, **kwargs): + return self.quote_name(self._create_index_name(*args, **kwargs)) + + return ForeignKeyName( + model._meta.db_table, + [field.column], + split_identifier(field.target_field.model._meta.db_table)[1], + [field.target_field.column], + suffix, + create_fk_name, + ) + def _delete_fk_sql(self, model, name): return self._delete_constraint_sql(self.sql_delete_fk, model, name) diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index 65d1f24429..172e9b9ac8 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -16,7 +16,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_rename_column = "ALTER TABLE %(table)s CHANGE %(old_column)s %(new_column)s %(type)s" sql_delete_unique = "ALTER TABLE %(table)s DROP INDEX %(name)s" - + sql_create_column_inline_fk = ( + ', ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) ' + 'REFERENCES %(to_table)s(%(to_column)s)' + ) sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s" sql_delete_index = "DROP INDEX %(name)s ON %(table)s" diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py index 4355402c52..9ac71b927e 100644 --- a/django/db/backends/oracle/schema.py +++ b/django/db/backends/oracle/schema.py @@ -15,6 +15,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_alter_column_default = "MODIFY %(column)s DEFAULT %(default)s" sql_alter_column_no_default = "MODIFY %(column)s DEFAULT NULL" sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s" + sql_create_column_inline_fk = 'CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s' sql_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS" sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s" diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py index 9eecaac19a..2dbd9e28a1 100644 --- a/django/db/backends/postgresql/schema.py +++ b/django/db/backends/postgresql/schema.py @@ -15,6 +15,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_create_index = "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s%(condition)s" sql_delete_index = "DROP INDEX IF EXISTS %(name)s" + sql_create_column_inline_fk = 'REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s' # Setting the constraint to IMMEDIATE runs any deferred checks to allow # dropping it in the same transaction. sql_delete_fk = "SET CONSTRAINTS %(name)s IMMEDIATE; ALTER TABLE %(table)s DROP CONSTRAINT %(name)s" diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index 19e949899f..40b99a07b9 100644 --- a/django/db/backends/sqlite3/features.py +++ b/django/db/backends/sqlite3/features.py @@ -26,6 +26,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): atomic_transactions = False can_rollback_ddl = True supports_atomic_references_rename = Database.sqlite_version_info >= (3, 26, 0) + can_create_inline_fk = False supports_paramstyle_pyformat = False supports_sequence_reset = False can_clone_databases = True diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index bf5733ed52..0ebbb7518f 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -214,6 +214,10 @@ backends. * ``DatabaseIntrospection.get_field_type()`` may no longer return tuples. +* If the database can create foreign keys in the same SQL statement that adds a + field, add ``SchemaEditor.sql_create_column_inline_fk`` with the appropriate + SQL; otherwise, set ``DatabaseFeatures.can_create_inline_fk = False``. + Miscellaneous ------------- diff --git a/tests/indexes/tests.py b/tests/indexes/tests.py index 7eb5dd89a9..ecc449ed4a 100644 --- a/tests/indexes/tests.py +++ b/tests/indexes/tests.py @@ -226,11 +226,9 @@ class SchemaIndexesMySQLTests(TransactionTestCase): new_field.set_attributes_from_name('new_foreign_key') editor.add_field(ArticleTranslation, new_field) field_created = True - self.assertEqual([str(statement) for statement in editor.deferred_sql], [ - 'ALTER TABLE `indexes_articletranslation` ' - 'ADD CONSTRAINT `indexes_articletrans_new_foreign_key_id_d27a9146_fk_indexes_a` ' - 'FOREIGN KEY (`new_foreign_key_id`) REFERENCES `indexes_article` (`id`)' - ]) + # No deferred SQL. The FK constraint is included in the + # statement to add the field. + self.assertFalse(editor.deferred_sql) finally: if field_created: with connection.schema_editor() as editor: diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 410a52b646..9b40a43523 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -241,6 +241,27 @@ class SchemaTests(TransactionTestCase): editor.alter_field(Book, old_field, new_field, strict=True) self.assertForeignKeyExists(Book, 'author_id', 'schema_tag') + @skipUnlessDBFeature('can_create_inline_fk') + def test_inline_fk(self): + # Create some tables. + with connection.schema_editor() as editor: + editor.create_model(Author) + editor.create_model(Book) + editor.create_model(Note) + self.assertForeignKeyNotExists(Note, 'book_id', 'schema_book') + # Add a foreign key from one to the other. + with connection.schema_editor() as editor: + new_field = ForeignKey(Book, CASCADE) + new_field.set_attributes_from_name('book') + editor.add_field(Note, new_field) + self.assertForeignKeyExists(Note, 'book_id', 'schema_book') + # Creating a FK field with a constraint uses a single statement without + # a deferred ALTER TABLE. + self.assertFalse([ + sql for sql in (str(statement) for statement in editor.deferred_sql) + if sql.startswith('ALTER TABLE') and 'ADD CONSTRAINT' in sql + ]) + @skipUnlessDBFeature('supports_foreign_keys') def test_char_field_with_db_index_to_fk(self): # Create the table