Fixed #30108 -- Allowed adding foreign key constraints in the same statement that adds a field.
This commit is contained in:
parent
9a0cc54524
commit
738faf9da2
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
-------------
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue