summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Tao <daniel.tao@gmail.com>2019-01-18 22:17:26 -0600
committerTim Graham <timograham@gmail.com>2019-01-29 15:42:57 -0500
commit738faf9da2a5cd03148a36375db80746c99c9623 (patch)
treef6b31d0e5c571693f772fe07ef224ebf5a217a98
parent9a0cc54524422dbdd9213e83a8ad7e8a4c13bd3e (diff)
Fixed #30108 -- Allowed adding foreign key constraints in the same statement that adds a field.
-rw-r--r--django/db/backends/base/features.py3
-rw-r--r--django/db/backends/base/schema.py45
-rw-r--r--django/db/backends/mysql/schema.py5
-rw-r--r--django/db/backends/oracle/schema.py1
-rw-r--r--django/db/backends/postgresql/schema.py1
-rw-r--r--django/db/backends/sqlite3/features.py1
-rw-r--r--docs/releases/3.0.txt4
-rw-r--r--tests/indexes/tests.py8
-rw-r--r--tests/schema/tests.py21
9 files changed, 69 insertions, 20 deletions
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