mirror of https://github.com/django/django.git
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?
|
# Does it support foreign keys?
|
||||||
supports_foreign_keys = True
|
supports_foreign_keys = True
|
||||||
|
|
||||||
|
# Can it create foreign key constraints inline when adding columns?
|
||||||
|
can_create_inline_fk = True
|
||||||
|
|
||||||
# Does it support CHECK constraints?
|
# Does it support CHECK constraints?
|
||||||
supports_column_check_constraints = True
|
supports_column_check_constraints = True
|
||||||
supports_table_check_constraints = True
|
supports_table_check_constraints = True
|
||||||
|
|
|
@ -77,6 +77,7 @@ class BaseDatabaseSchemaEditor:
|
||||||
"REFERENCES %(to_table)s (%(to_column)s)%(deferrable)s"
|
"REFERENCES %(to_table)s (%(to_column)s)%(deferrable)s"
|
||||||
)
|
)
|
||||||
sql_create_inline_fk = None
|
sql_create_inline_fk = None
|
||||||
|
sql_create_column_inline_fk = None
|
||||||
sql_delete_fk = sql_delete_constraint
|
sql_delete_fk = sql_delete_constraint
|
||||||
|
|
||||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s%(condition)s"
|
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)
|
db_params = field.db_parameters(connection=self.connection)
|
||||||
if db_params['check']:
|
if db_params['check']:
|
||||||
definition += " " + self.sql_check_constraint % db_params
|
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
|
# Build the SQL and run it
|
||||||
sql = self.sql_create_column % {
|
sql = self.sql_create_column % {
|
||||||
"table": self.quote_name(model._meta.db_table),
|
"table": self.quote_name(model._meta.db_table),
|
||||||
|
@ -451,9 +468,6 @@ class BaseDatabaseSchemaEditor:
|
||||||
self.execute(sql, params)
|
self.execute(sql, params)
|
||||||
# Add an index, if required
|
# Add an index, if required
|
||||||
self.deferred_sql.extend(self._field_indexes_sql(model, field))
|
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
|
# Reset connection if required
|
||||||
if self.connection.features.connection_persists_old_columns:
|
if self.connection.features.connection_persists_old_columns:
|
||||||
self.connection.close()
|
self.connection.close()
|
||||||
|
@ -984,18 +998,8 @@ class BaseDatabaseSchemaEditor:
|
||||||
}
|
}
|
||||||
|
|
||||||
def _create_fk_sql(self, model, field, suffix):
|
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)
|
table = Table(model._meta.db_table, self.quote_name)
|
||||||
name = ForeignKeyName(
|
name = self._fk_constraint_name(model, field, suffix)
|
||||||
model._meta.db_table,
|
|
||||||
[field.column],
|
|
||||||
split_identifier(field.target_field.model._meta.db_table)[1],
|
|
||||||
[field.target_field.column],
|
|
||||||
suffix,
|
|
||||||
create_fk_name,
|
|
||||||
)
|
|
||||||
column = Columns(model._meta.db_table, [field.column], self.quote_name)
|
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_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)
|
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,
|
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):
|
def _delete_fk_sql(self, model, name):
|
||||||
return self._delete_constraint_sql(self.sql_delete_fk, 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_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_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_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s"
|
||||||
|
|
||||||
sql_delete_index = "DROP INDEX %(name)s ON %(table)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_default = "MODIFY %(column)s DEFAULT %(default)s"
|
||||||
sql_alter_column_no_default = "MODIFY %(column)s DEFAULT NULL"
|
sql_alter_column_no_default = "MODIFY %(column)s DEFAULT NULL"
|
||||||
sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s"
|
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_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
|
||||||
sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s"
|
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_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_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
|
# Setting the constraint to IMMEDIATE runs any deferred checks to allow
|
||||||
# dropping it in the same transaction.
|
# dropping it in the same transaction.
|
||||||
sql_delete_fk = "SET CONSTRAINTS %(name)s IMMEDIATE; ALTER TABLE %(table)s DROP CONSTRAINT %(name)s"
|
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
|
atomic_transactions = False
|
||||||
can_rollback_ddl = True
|
can_rollback_ddl = True
|
||||||
supports_atomic_references_rename = Database.sqlite_version_info >= (3, 26, 0)
|
supports_atomic_references_rename = Database.sqlite_version_info >= (3, 26, 0)
|
||||||
|
can_create_inline_fk = False
|
||||||
supports_paramstyle_pyformat = False
|
supports_paramstyle_pyformat = False
|
||||||
supports_sequence_reset = False
|
supports_sequence_reset = False
|
||||||
can_clone_databases = True
|
can_clone_databases = True
|
||||||
|
|
|
@ -214,6 +214,10 @@ backends.
|
||||||
|
|
||||||
* ``DatabaseIntrospection.get_field_type()`` may no longer return tuples.
|
* ``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
|
Miscellaneous
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
|
@ -226,11 +226,9 @@ class SchemaIndexesMySQLTests(TransactionTestCase):
|
||||||
new_field.set_attributes_from_name('new_foreign_key')
|
new_field.set_attributes_from_name('new_foreign_key')
|
||||||
editor.add_field(ArticleTranslation, new_field)
|
editor.add_field(ArticleTranslation, new_field)
|
||||||
field_created = True
|
field_created = True
|
||||||
self.assertEqual([str(statement) for statement in editor.deferred_sql], [
|
# No deferred SQL. The FK constraint is included in the
|
||||||
'ALTER TABLE `indexes_articletranslation` '
|
# statement to add the field.
|
||||||
'ADD CONSTRAINT `indexes_articletrans_new_foreign_key_id_d27a9146_fk_indexes_a` '
|
self.assertFalse(editor.deferred_sql)
|
||||||
'FOREIGN KEY (`new_foreign_key_id`) REFERENCES `indexes_article` (`id`)'
|
|
||||||
])
|
|
||||||
finally:
|
finally:
|
||||||
if field_created:
|
if field_created:
|
||||||
with connection.schema_editor() as editor:
|
with connection.schema_editor() as editor:
|
||||||
|
|
|
@ -241,6 +241,27 @@ class SchemaTests(TransactionTestCase):
|
||||||
editor.alter_field(Book, old_field, new_field, strict=True)
|
editor.alter_field(Book, old_field, new_field, strict=True)
|
||||||
self.assertForeignKeyExists(Book, 'author_id', 'schema_tag')
|
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')
|
@skipUnlessDBFeature('supports_foreign_keys')
|
||||||
def test_char_field_with_db_index_to_fk(self):
|
def test_char_field_with_db_index_to_fk(self):
|
||||||
# Create the table
|
# Create the table
|
||||||
|
|
Loading…
Reference in New Issue