diff --git a/django/contrib/gis/db/backends/mysql/schema.py b/django/contrib/gis/db/backends/mysql/schema.py index 1cc3186cb0..e485c671e5 100644 --- a/django/contrib/gis/db/backends/mysql/schema.py +++ b/django/contrib/gis/db/backends/mysql/schema.py @@ -58,6 +58,43 @@ class MySQLGISSchemaEditor(DatabaseSchemaEditor): super().remove_field(model, field) + def _alter_field( + self, + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=False, + ): + super()._alter_field( + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=strict, + ) + + old_field_spatial_index = ( + isinstance(old_field, GeometryField) + and old_field.spatial_index + and not old_field.null + ) + new_field_spatial_index = ( + isinstance(new_field, GeometryField) + and new_field.spatial_index + and not new_field.null + ) + if not old_field_spatial_index and new_field_spatial_index: + self.execute(self._create_spatial_index_sql(model, new_field)) + elif old_field_spatial_index and not new_field_spatial_index: + self.execute(self._delete_spatial_index_sql(model, old_field)) + def _create_spatial_index_name(self, model, field): return "%s_%s_id" % (model._meta.db_table, field.column) diff --git a/django/contrib/gis/db/backends/oracle/schema.py b/django/contrib/gis/db/backends/oracle/schema.py index ef9b1aa942..ce03ee8892 100644 --- a/django/contrib/gis/db/backends/oracle/schema.py +++ b/django/contrib/gis/db/backends/oracle/schema.py @@ -98,6 +98,39 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor): self.execute(sql) self.geometry_sql = [] + def _alter_field( + self, + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=False, + ): + super()._alter_field( + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=strict, + ) + + old_field_spatial_index = ( + isinstance(old_field, GeometryField) and old_field.spatial_index + ) + new_field_spatial_index = ( + isinstance(new_field, GeometryField) and new_field.spatial_index + ) + if not old_field_spatial_index and new_field_spatial_index: + self.execute(self._create_spatial_index_sql(model, new_field)) + elif old_field_spatial_index and not new_field_spatial_index: + self.execute(self._delete_spatial_index_sql(model, old_field)) + def _create_spatial_index_name(self, model, field): # Oracle doesn't allow object names > 30 characters. Use this scheme # instead of self._create_index_name() for backwards compatibility. diff --git a/django/contrib/gis/db/backends/postgis/schema.py b/django/contrib/gis/db/backends/postgis/schema.py index 5c0cd25364..c74b574c4c 100644 --- a/django/contrib/gis/db/backends/postgis/schema.py +++ b/django/contrib/gis/db/backends/postgis/schema.py @@ -1,3 +1,4 @@ +from django.contrib.gis.db.models import GeometryField from django.db.backends.postgresql.schema import DatabaseSchemaEditor from django.db.models.expressions import Col, Func @@ -58,6 +59,39 @@ class PostGISSchemaEditor(DatabaseSchemaEditor): [], ) + def _alter_field( + self, + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=False, + ): + super()._alter_field( + model, + old_field, + new_field, + old_type, + new_type, + old_db_params, + new_db_params, + strict=strict, + ) + + old_field_spatial_index = ( + isinstance(old_field, GeometryField) and old_field.spatial_index + ) + new_field_spatial_index = ( + isinstance(new_field, GeometryField) and new_field.spatial_index + ) + if not old_field_spatial_index and new_field_spatial_index: + self.execute(self._create_spatial_index_sql(model, new_field)) + elif old_field_spatial_index and not new_field_spatial_index: + self.execute(self._delete_spatial_index_sql(model, old_field)) + def _create_spatial_index_name(self, model, field): return self._create_index_name(model._meta.db_table, [field.column], "_id") @@ -84,3 +118,7 @@ class PostGISSchemaEditor(DatabaseSchemaEditor): opclasses=opclasses, expressions=expressions, ) + + def _delete_spatial_index_sql(self, model, field): + index_name = self._create_spatial_index_name(model, field) + return self._delete_index_sql(model, index_name) diff --git a/tests/gis_tests/gis_migrations/test_operations.py b/tests/gis_tests/gis_migrations/test_operations.py index 3ecde2025e..98201ed3f7 100644 --- a/tests/gis_tests/gis_migrations/test_operations.py +++ b/tests/gis_tests/gis_migrations/test_operations.py @@ -92,6 +92,20 @@ class OperationTestCase(TransactionTestCase): else: self.assertIn([column], [c["columns"] for c in constraints.values()]) + def assertSpatialIndexNotExists(self, table, column, raster=False): + with connection.cursor() as cursor: + constraints = connection.introspection.get_constraints(cursor, table) + if raster: + self.assertFalse( + any( + "st_convexhull(%s)" % column in c["definition"] + for c in constraints.values() + if c["definition"] is not None + ) + ) + else: + self.assertNotIn([column], [c["columns"] for c in constraints.values()]) + def alter_gis_model( self, migration_class, @@ -239,6 +253,102 @@ class OperationTests(OperationTestCase): if connection.features.supports_raster: self.assertSpatialIndexExists("gis_neighborhood", "rast", raster=True) + @skipUnlessDBFeature("can_alter_geometry_field") + def test_alter_field_add_spatial_index(self): + if not self.has_spatial_indexes: + self.skipTest("No support for Spatial indexes") + + self.alter_gis_model( + migrations.AddField, + "Neighborhood", + "point", + fields.PointField, + field_class_kwargs={"spatial_index": False}, + ) + self.assertSpatialIndexNotExists("gis_neighborhood", "point") + + self.alter_gis_model( + migrations.AlterField, + "Neighborhood", + "point", + fields.PointField, + field_class_kwargs={"spatial_index": True}, + ) + self.assertSpatialIndexExists("gis_neighborhood", "point") + + @skipUnlessDBFeature("can_alter_geometry_field") + def test_alter_field_remove_spatial_index(self): + if not self.has_spatial_indexes: + self.skipTest("No support for Spatial indexes") + + self.assertSpatialIndexExists("gis_neighborhood", "geom") + + self.alter_gis_model( + migrations.AlterField, + "Neighborhood", + "geom", + fields.MultiPolygonField, + field_class_kwargs={"spatial_index": False}, + ) + self.assertSpatialIndexNotExists("gis_neighborhood", "geom") + + @skipUnlessDBFeature("can_alter_geometry_field") + @skipUnless(connection.vendor == "mysql", "MySQL specific test") + def test_alter_field_nullable_with_spatial_index(self): + if not self.has_spatial_indexes: + self.skipTest("No support for Spatial indexes") + + self.alter_gis_model( + migrations.AddField, + "Neighborhood", + "point", + fields.PointField, + field_class_kwargs={"spatial_index": False, "null": True}, + ) + # MySQL doesn't support spatial indexes on NULL columns. + self.assertSpatialIndexNotExists("gis_neighborhood", "point") + + self.alter_gis_model( + migrations.AlterField, + "Neighborhood", + "point", + fields.PointField, + field_class_kwargs={"spatial_index": True, "null": True}, + ) + self.assertSpatialIndexNotExists("gis_neighborhood", "point") + + self.alter_gis_model( + migrations.AlterField, + "Neighborhood", + "point", + fields.PointField, + field_class_kwargs={"spatial_index": False, "null": True}, + ) + self.assertSpatialIndexNotExists("gis_neighborhood", "point") + + @skipUnlessDBFeature("can_alter_geometry_field") + def test_alter_field_with_spatial_index(self): + if not self.has_spatial_indexes: + self.skipTest("No support for Spatial indexes") + + self.alter_gis_model( + migrations.AddField, + "Neighborhood", + "point", + fields.PointField, + field_class_kwargs={"spatial_index": True}, + ) + self.assertSpatialIndexExists("gis_neighborhood", "point") + + self.alter_gis_model( + migrations.AlterField, + "Neighborhood", + "point", + fields.PointField, + field_class_kwargs={"spatial_index": True, "srid": 3086}, + ) + self.assertSpatialIndexExists("gis_neighborhood", "point") + @skipUnlessDBFeature("supports_3d_storage") def test_add_3d_field_opclass(self): if not connection.ops.postgis: