mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #30108 -- Allowed adding foreign key constraints in the same statement that adds a field.
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user