From b40a1d774d6e7dc0a3663a69c3a8a9b8793d31ff Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Mon, 4 Dec 2017 20:35:33 -0500 Subject: [2.0.x] Fixed #28884 -- Fixed crash on SQLite when renaming a field in a model referenced by a ManyToManyField. Introspected database constraints instead of relying on _meta.related_objects to determine whether or not a table or a column is referenced on rename operations. This has the side effect of ignoring both db_constraint=False and virtual fields such as GenericRelation which aren't backend by database level constraints and thus shouldn't prevent the rename operations from being performed in a transaction. Regression in 095c1aaa898bed40568009db836aa8434f1b983d. Thanks Tim for the additional tests and edits, and Mariusz for the review. Backport of 9f7772e098439f9edea3d25ab127539fc514eeb2 from master --- django/db/backends/sqlite3/introspection.py | 30 ++++++++++++++++------------- django/db/backends/sqlite3/schema.py | 27 ++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 17 deletions(-) (limited to 'django/db/backends/sqlite3') diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py index 0518641344..de6d6da465 100644 --- a/django/db/backends/sqlite3/introspection.py +++ b/django/db/backends/sqlite3/introspection.py @@ -239,6 +239,22 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): 'pk': field[5], # undocumented } for field in cursor.fetchall()] + def _get_foreign_key_constraints(self, cursor, table_name): + constraints = {} + cursor.execute('PRAGMA foreign_key_list(%s)' % self.connection.ops.quote_name(table_name)) + for row in cursor.fetchall(): + # Remaining on_update/on_delete/match values are of no interest. + id_, _, table, from_, to = row[:5] + constraints['fk_%d' % id_] = { + 'columns': [from_], + 'primary_key': False, + 'unique': False, + 'foreign_key': (table, to), + 'check': False, + 'index': False, + } + return constraints + def get_constraints(self, cursor, table_name): """ Retrieve any constraints or keys (unique, pk, fk, check, index) across @@ -293,17 +309,5 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): "check": False, "index": False, } - # Get foreign keys - cursor.execute('PRAGMA foreign_key_list(%s)' % self.connection.ops.quote_name(table_name)) - for row in cursor.fetchall(): - # Remaining on_update/on_delete/match values are of no interest here - id_, seq, table, from_, to = row[:5] - constraints['fk_%d' % id_] = { - 'columns': [from_], - 'primary_key': False, - 'unique': False, - 'foreign_key': (table, to), - 'check': False, - 'index': False, - } + constraints.update(self._get_foreign_key_constraints(cursor, table_name)) return constraints diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index 8c427bee45..add05843c3 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -61,8 +61,27 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): else: raise ValueError("Cannot quote parameter value %r of type %s" % (value, type(value))) + def _is_referenced_by_fk_constraint(self, table_name, column_name=None, ignore_self=False): + """ + Return whether or not the provided table name is referenced by another + one. If `column_name` is specified, only references pointing to that + column are considered. If `ignore_self` is True, self-referential + constraints are ignored. + """ + with self.connection.cursor() as cursor: + for other_table in self.connection.introspection.get_table_list(cursor): + if ignore_self and other_table.name == table_name: + continue + constraints = self.connection.introspection._get_foreign_key_constraints(cursor, other_table.name) + for constraint in constraints.values(): + constraint_table, constraint_column = constraint['foreign_key'] + if (constraint_table == table_name and + (column_name is None or constraint_column == column_name)): + return True + return False + def alter_db_table(self, model, old_db_table, new_db_table, disable_constraints=True): - if model._meta.related_objects and disable_constraints: + if disable_constraints and self._is_referenced_by_fk_constraint(old_db_table): if self.connection.in_atomic_block: raise NotSupportedError(( 'Renaming the %r table while in a transaction is not ' @@ -77,8 +96,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): def alter_field(self, model, old_field, new_field, strict=False): old_field_name = old_field.name + table_name = model._meta.db_table + _, old_column_name = old_field.get_attname_column() if (new_field.name != old_field_name and - any(r.field_name == old_field.name for r in model._meta.related_objects)): + self._is_referenced_by_fk_constraint(table_name, old_column_name, ignore_self=True)): if self.connection.in_atomic_block: raise NotSupportedError(( 'Renaming the %r.%r column while in a transaction is not ' @@ -93,9 +114,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): with self.connection.cursor() as cursor: schema_version = cursor.execute('PRAGMA schema_version').fetchone()[0] cursor.execute('PRAGMA writable_schema = 1') - table_name = model._meta.db_table references_template = ' REFERENCES "%s" ("%%s") ' % table_name - old_column_name = old_field.get_attname_column()[1] new_column_name = new_field.get_attname_column()[1] search = references_template % old_column_name replacement = references_template % new_column_name -- cgit v1.3