[1.7.x] Fixed #23030 -- Properly handled geometry columns metadata during migrations

Thanks kunitoki for the report and initial patches.
Backport of 8c30df15f1 from master.
This commit is contained in:
Claude Paroz 2014-07-16 11:06:33 +02:00
parent 86655111c9
commit ddb5674945
5 changed files with 63 additions and 18 deletions

View File

@ -10,7 +10,8 @@ from django.utils.encoding import python_2_unicode_compatible
class PostGISGeometryColumns(models.Model): class PostGISGeometryColumns(models.Model):
""" """
The 'geometry_columns' table from the PostGIS. See the PostGIS The 'geometry_columns' table from the PostGIS. See the PostGIS
documentation at Ch. 4.2.2. documentation at Ch. 4.3.2.
On PostGIS 2, this is a view.
""" """
f_table_catalog = models.CharField(max_length=256) f_table_catalog = models.CharField(max_length=256)
f_table_schema = models.CharField(max_length=256) f_table_schema = models.CharField(max_length=256)

View File

@ -62,13 +62,13 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
self.execute(sql) self.execute(sql)
self.geometry_sql = [] self.geometry_sql = []
def delete_model(self, model): def delete_model(self, model, **kwargs):
from django.contrib.gis.db.models.fields import GeometryField from django.contrib.gis.db.models.fields import GeometryField
# Drop spatial metadata (dropping the table does not automatically remove them) # Drop spatial metadata (dropping the table does not automatically remove them)
for field in model._meta.local_fields: for field in model._meta.local_fields:
if isinstance(field, GeometryField): if isinstance(field, GeometryField):
self.remove_geometry_metadata(model, field) self.remove_geometry_metadata(model, field)
super(SpatialiteSchemaEditor, self).delete_model(model) super(SpatialiteSchemaEditor, self).delete_model(model, **kwargs)
def add_field(self, model, field): def add_field(self, model, field):
from django.contrib.gis.db.models.fields import GeometryField from django.contrib.gis.db.models.fields import GeometryField
@ -81,12 +81,6 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
else: else:
super(SpatialiteSchemaEditor, self).add_field(model, field) super(SpatialiteSchemaEditor, self).add_field(model, field)
def remove_field(self, model, field):
from django.contrib.gis.db.models.fields import GeometryField
if isinstance(field, GeometryField):
self.remove_geometry_metadata(model, field)
super(SpatialiteSchemaEditor, self).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):
super(SpatialiteSchemaEditor, self).alter_db_table(model, old_db_table, new_db_table) super(SpatialiteSchemaEditor, self).alter_db_table(model, old_db_table, new_db_table)
self.execute( self.execute(
@ -95,3 +89,10 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
"new_table": self.quote_name(new_db_table), "new_table": self.quote_name(new_db_table),
} }
) )
# Also rename spatial index tables
for field in model._meta.local_fields:
if getattr(field, 'spatial_index', False):
self.execute(self.sql_rename_table % {
"old_table": self.quote_name("idx_%s_%s" % (old_db_table, field.column)),
"new_table": self.quote_name("idx_%s_%s" % (new_db_table, field.column)),
})

View File

@ -2,9 +2,10 @@ from django.db import models, migrations
import django.contrib.gis.db.models.fields import django.contrib.gis.db.models.fields
# Used for regression test of ticket #22001: https://code.djangoproject.com/ticket/22001
class Migration(migrations.Migration): class Migration(migrations.Migration):
"""
Used for gis.specific migration tests.
"""
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Neighborhood', name='Neighborhood',
@ -29,5 +30,21 @@ class Migration(migrations.Migration):
options={ options={
}, },
bases=(models.Model,), bases=(models.Model,),
) ),
migrations.CreateModel(
name='Family',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=100, unique=True)),
],
options={
},
bases=(models.Model,),
),
migrations.AddField(
model_name='household',
name='family',
field=models.ForeignKey(blank=True, to='gis.Family', null=True),
preserve_default=True,
),
] ]

View File

@ -34,17 +34,37 @@ class MigrateTests(TransactionTestCase):
Tests basic usage of the migrate command when a model uses Geodjango Tests basic usage of the migrate command when a model uses Geodjango
fields. Regression test for ticket #22001: fields. Regression test for ticket #22001:
https://code.djangoproject.com/ticket/22001 https://code.djangoproject.com/ticket/22001
It's also used to showcase an error in migrations where spatialite is
enabled and geo tables are renamed resulting in unique constraint
failure on geometry_columns. Regression for ticket #23030:
https://code.djangoproject.com/ticket/23030
""" """
# Make sure no tables are created # Make sure no tables are created
self.assertTableNotExists("migrations_neighborhood") self.assertTableNotExists("migrations_neighborhood")
self.assertTableNotExists("migrations_household") self.assertTableNotExists("migrations_household")
self.assertTableNotExists("migrations_family")
# Run the migrations to 0001 only # Run the migrations to 0001 only
call_command("migrate", "gis", "0001", verbosity=0) call_command("migrate", "gis", "0001", verbosity=0)
# Make sure the right tables exist # Make sure the right tables exist
self.assertTableExists("gis_neighborhood") self.assertTableExists("gis_neighborhood")
self.assertTableExists("gis_household") self.assertTableExists("gis_household")
self.assertTableExists("gis_family")
# Unmigrate everything # Unmigrate everything
call_command("migrate", "gis", "zero", verbosity=0) call_command("migrate", "gis", "zero", verbosity=0)
# Make sure it's all gone # Make sure it's all gone
self.assertTableNotExists("gis_neighborhood") self.assertTableNotExists("gis_neighborhood")
self.assertTableNotExists("gis_household") self.assertTableNotExists("gis_household")
self.assertTableNotExists("gis_family")
# Even geometry columns metadata
try:
GeoColumn = connection.ops.geometry_columns()
except NotImplementedError:
# Not all GIS backends have geometry columns model
pass
else:
self.assertEqual(
GeoColumn.objects.filter(
**{'%s__in' % GeoColumn.table_name_col(): ["gis_neighborhood", "gis_household"]}
).count(),
0)

View File

@ -127,13 +127,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
', '.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(model._meta.db_table),
)) ))
# Delete the old table (not using self.delete_model to avoid deleting # Delete the old table
# all implicit M2M tables) self.delete_model(model, handle_autom2m=False)
self.execute(self.sql_delete_table % {
"table": self.quote_name(model._meta.db_table),
})
# Rename the new to the old # Rename the new to the old
self.alter_db_table(model, temp_model._meta.db_table, model._meta.db_table) 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.replace(temp_model._meta.db_table, model._meta.db_table))
@ -142,6 +139,15 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
if restore_pk_field: if restore_pk_field:
restore_pk_field.primary_key = True restore_pk_field.primary_key = True
def delete_model(self, model, handle_autom2m=True):
if handle_autom2m:
super(DatabaseSchemaEditor, self).delete_model(model)
else:
# Delete the table (and only that)
self.execute(self.sql_delete_table % {
"table": self.quote_name(model._meta.db_table),
})
def add_field(self, model, field): def add_field(self, model, field):
""" """
Creates a field on a model. Creates a field on a model.