From f8cc9285e14c491d8b8d51313c763808b7d1b115 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sun, 18 Aug 2024 07:12:51 +0200 Subject: [PATCH] Fixed #35074 -- Fixed adding/removing indexes when spatial_index is changed on MySQL, PostgreSQL, and Oracle. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mário Falcão --- .../contrib/gis/db/backends/mysql/schema.py | 37 ++++++ .../contrib/gis/db/backends/oracle/schema.py | 33 ++++++ .../contrib/gis/db/backends/postgis/schema.py | 38 ++++++ .../gis_migrations/test_operations.py | 110 ++++++++++++++++++ 4 files changed, 218 insertions(+) diff --git a/django/contrib/gis/db/backends/mysql/schema.py b/django/contrib/gis/db/backends/mysql/schema.py index 1cc3186cb05..e485c671e5d 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 ef9b1aa9425..ce03ee8892e 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 5c0cd25364c..c74b574c4cb 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 3ecde2025e0..98201ed3f78 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: