Safer table alterations under SQLite
Table alterations in SQLite require creating a new table and copying data over from the old one. This change ensures that no Django model ever exists with the temporary table name as its db_table attribute.
This commit is contained in:
parent
9e83f30cd3
commit
6c58e53d5d
|
@ -1,5 +1,6 @@
|
||||||
import _sqlite3 # isort:skip
|
import _sqlite3 # isort:skip
|
||||||
import codecs
|
import codecs
|
||||||
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
@ -46,6 +47,12 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
override_indexes=None):
|
override_indexes=None):
|
||||||
"""
|
"""
|
||||||
Shortcut to transform a model from old_model into new_model
|
Shortcut to transform a model from old_model into new_model
|
||||||
|
|
||||||
|
The essential steps are:
|
||||||
|
1. rename the model's existing table, e.g. "app_model" to "app_model__old"
|
||||||
|
2. create a table with the updated definition called "app_model"
|
||||||
|
3. copy the data from the old renamed table to the new table
|
||||||
|
4. delete the "app_model__old" table
|
||||||
"""
|
"""
|
||||||
# Work out the new fields dict / mapping
|
# Work out the new fields dict / mapping
|
||||||
body = {f.name: f for f in model._meta.local_fields}
|
body = {f.name: f for f in model._meta.local_fields}
|
||||||
|
@ -97,9 +104,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
# Work inside a new app registry
|
# Work inside a new app registry
|
||||||
apps = Apps()
|
apps = Apps()
|
||||||
|
|
||||||
# Provide isolated instances of the fields to the new model body
|
# Provide isolated instances of the fields to the new model body so
|
||||||
# Instantiating the new model with an alternate db_table will alter
|
# that the existing model's internals aren't interfered with when
|
||||||
# the internal references of some of the provided fields.
|
# the dummy model is constructed.
|
||||||
body = copy.deepcopy(body)
|
body = copy.deepcopy(body)
|
||||||
|
|
||||||
# Work out the new value of unique_together, taking renames into
|
# Work out the new value of unique_together, taking renames into
|
||||||
|
@ -121,7 +128,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
# Construct a new model for the new state
|
# Construct a new model for the new state
|
||||||
meta_contents = {
|
meta_contents = {
|
||||||
'app_label': model._meta.app_label,
|
'app_label': model._meta.app_label,
|
||||||
'db_table': model._meta.db_table + "__new",
|
'db_table': model._meta.db_table,
|
||||||
'unique_together': override_uniques,
|
'unique_together': override_uniques,
|
||||||
'index_together': override_indexes,
|
'index_together': override_indexes,
|
||||||
'apps': apps,
|
'apps': apps,
|
||||||
|
@ -131,25 +138,43 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
body['__module__'] = model.__module__
|
body['__module__'] = model.__module__
|
||||||
|
|
||||||
temp_model = type(model._meta.object_name, model.__bases__, body)
|
temp_model = type(model._meta.object_name, model.__bases__, body)
|
||||||
|
|
||||||
|
# We need to modify model._meta.db_table, but everything explodes
|
||||||
|
# if the change isn't reversed before the end of this method. This
|
||||||
|
# context manager helps us avoid that situation.
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def altered_table_name(model, temporary_table_name):
|
||||||
|
original_table_name = model._meta.db_table
|
||||||
|
model._meta.db_table = temporary_table_name
|
||||||
|
yield
|
||||||
|
model._meta.db_table = original_table_name
|
||||||
|
|
||||||
|
# Rename the old table to something temporary
|
||||||
|
old_table_name = model._meta.db_table + "__old"
|
||||||
|
with altered_table_name(model, old_table_name):
|
||||||
|
self.alter_db_table(model, temp_model._meta.db_table, model._meta.db_table)
|
||||||
|
|
||||||
# Create a new table with that format. We remove things from the
|
# Create a new table with that format. We remove things from the
|
||||||
# deferred SQL that match our table name, too
|
# deferred SQL that match our table name, too
|
||||||
self.deferred_sql = [x for x in self.deferred_sql if model._meta.db_table not in x]
|
self.deferred_sql = [x for x in self.deferred_sql if temp_model._meta.db_table not in x]
|
||||||
self.create_model(temp_model)
|
self.create_model(temp_model)
|
||||||
|
|
||||||
# Copy data from the old table
|
# Copy data from the old table
|
||||||
field_maps = list(mapping.items())
|
field_maps = list(mapping.items())
|
||||||
self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % (
|
self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % (
|
||||||
self.quote_name(temp_model._meta.db_table),
|
self.quote_name(temp_model._meta.db_table),
|
||||||
', '.join(self.quote_name(x) for x, y in field_maps),
|
', '.join(self.quote_name(x) for x, y in field_maps),
|
||||||
', '.join(y for x, y in field_maps),
|
', '.join(y for x, y in field_maps),
|
||||||
self.quote_name(model._meta.db_table),
|
self.quote_name(old_table_name),
|
||||||
))
|
))
|
||||||
|
|
||||||
# Delete the old table
|
# Delete the old table
|
||||||
|
with altered_table_name(model, old_table_name):
|
||||||
self.delete_model(model, handle_autom2m=False)
|
self.delete_model(model, handle_autom2m=False)
|
||||||
# Rename the new to the old
|
|
||||||
self.alter_db_table(temp_model, temp_model._meta.db_table, model._meta.db_table)
|
|
||||||
# Run deferred SQL on correct table
|
# Run deferred SQL on correct table
|
||||||
for sql in self.deferred_sql:
|
for sql in self.deferred_sql:
|
||||||
self.execute(sql.replace(temp_model._meta.db_table, model._meta.db_table))
|
self.execute(sql)
|
||||||
self.deferred_sql = []
|
self.deferred_sql = []
|
||||||
# Fix any PK-removed field
|
# Fix any PK-removed field
|
||||||
if restore_pk_field:
|
if restore_pk_field:
|
||||||
|
|
Loading…
Reference in New Issue