diff options
| author | Simon Charette <charette.s@gmail.com> | 2017-11-26 22:39:43 -0500 |
|---|---|---|
| committer | Simon Charette <charette.s@gmail.com> | 2017-12-01 22:15:48 -0500 |
| commit | 31d318d19c90ed1dcc441b6b87a1f82dc2fc2d28 (patch) | |
| tree | 7a79c736cc691fdaa3adc78f4f6dac6de45331c6 /django | |
| parent | 50b35eef0b55a796bf936aaef83f11e18fdadace (diff) | |
[2.0.x] Fixed #28849 -- Fixed referenced table and column rename on SQLite.
Thanks Ramiro for the input and Tim for the review.
Backport of 095c1aaa898bed40568009db836aa8434f1b983d from master
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/gis/db/backends/spatialite/schema.py | 4 | ||||
| -rw-r--r-- | django/db/backends/base/features.py | 3 | ||||
| -rw-r--r-- | django/db/backends/sqlite3/features.py | 1 | ||||
| -rw-r--r-- | django/db/backends/sqlite3/schema.py | 60 |
4 files changed, 64 insertions, 4 deletions
diff --git a/django/contrib/gis/db/backends/spatialite/schema.py b/django/contrib/gis/db/backends/spatialite/schema.py index 6f4e3380db..93ea754488 100644 --- a/django/contrib/gis/db/backends/spatialite/schema.py +++ b/django/contrib/gis/db/backends/spatialite/schema.py @@ -123,7 +123,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor): else: super().remove_field(model, field) - def alter_db_table(self, model, old_db_table, new_db_table): + def alter_db_table(self, model, old_db_table, new_db_table, disable_constraints=True): from django.contrib.gis.db.models.fields import GeometryField # Remove geometry-ness from temp table for field in model._meta.local_fields: @@ -135,7 +135,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor): } ) # Alter table - super().alter_db_table(model, old_db_table, new_db_table) + super().alter_db_table(model, old_db_table, new_db_table, disable_constraints) # Repoint any straggler names for geom_table in self.geometry_tables: try: diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 4ef7cc24a6..1d431b5e99 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -167,6 +167,9 @@ class BaseDatabaseFeatures: # Can we roll back DDL in a transaction? can_rollback_ddl = False + # Does it support operations requiring references rename in a transaction? + supports_atomic_references_rename = True + # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE? supports_combined_alters = False diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index fcebec063e..82c1a34d89 100644 --- a/django/db/backends/sqlite3/features.py +++ b/django/db/backends/sqlite3/features.py @@ -24,6 +24,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_transactions = True atomic_transactions = False can_rollback_ddl = True + supports_atomic_references_rename = False supports_paramstyle_pyformat = False supports_sequence_reset = False can_clone_databases = True diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index 93cf077dd2..8c427bee45 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -6,6 +6,8 @@ from decimal import Decimal from django.apps.registry import Apps from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.ddl_references import Statement +from django.db.transaction import atomic +from django.db.utils import NotSupportedError class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): @@ -59,6 +61,58 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): else: raise ValueError("Cannot quote parameter value %r of type %s" % (value, type(value))) + def alter_db_table(self, model, old_db_table, new_db_table, disable_constraints=True): + if model._meta.related_objects and disable_constraints: + if self.connection.in_atomic_block: + raise NotSupportedError(( + 'Renaming the %r table while in a transaction is not ' + 'supported on SQLite because it would break referential ' + 'integrity. Try adding `atomic = False` to the Migration class.' + ) % old_db_table) + self.connection.enable_constraint_checking() + super().alter_db_table(model, old_db_table, new_db_table) + self.connection.disable_constraint_checking() + else: + super().alter_db_table(model, old_db_table, new_db_table) + + def alter_field(self, model, old_field, new_field, strict=False): + old_field_name = old_field.name + if (new_field.name != old_field_name and + any(r.field_name == old_field.name for r in model._meta.related_objects)): + if self.connection.in_atomic_block: + raise NotSupportedError(( + 'Renaming the %r.%r column while in a transaction is not ' + 'supported on SQLite because it would break referential ' + 'integrity. Try adding `atomic = False` to the Migration class.' + ) % (model._meta.db_table, old_field_name)) + with atomic(self.connection.alias): + super().alter_field(model, old_field, new_field, strict=strict) + # Follow SQLite's documented procedure for performing changes + # that don't affect the on-disk content. + # https://sqlite.org/lang_altertable.html#otheralter + 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 + cursor.execute('UPDATE sqlite_master SET sql = replace(sql, %s, %s)', (search, replacement)) + cursor.execute('PRAGMA schema_version = %d' % (schema_version + 1)) + cursor.execute('PRAGMA writable_schema = 0') + # The integrity check will raise an exception and rollback + # the transaction if the sqlite_master updates corrupt the + # database. + cursor.execute('PRAGMA integrity_check') + # Perform a VACUUM to refresh the database representation from + # the sqlite_master table. + with self.connection.cursor() as cursor: + cursor.execute('VACUUM') + else: + super().alter_field(model, old_field, new_field, strict=strict) + def _remake_table(self, model, create_field=None, delete_field=None, alter_field=None): """ Shortcut to transform a model from old_model into new_model @@ -182,8 +236,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): with altered_table_name(model, model._meta.db_table + "__old"): # Rename the old table to make way for the new - self.alter_db_table(model, temp_model._meta.db_table, model._meta.db_table) - + self.alter_db_table( + model, temp_model._meta.db_table, model._meta.db_table, + disable_constraints=False, + ) # Create a new table with the updated schema. self.create_model(temp_model) |
