diff --git a/django/contrib/gis/db/backends/oracle/base.py b/django/contrib/gis/db/backends/oracle/base.py index c1b48bccfa..fad281a3f9 100644 --- a/django/contrib/gis/db/backends/oracle/base.py +++ b/django/contrib/gis/db/backends/oracle/base.py @@ -6,6 +6,7 @@ from django.contrib.gis.db.backends.base import BaseSpatialFeatures from django.contrib.gis.db.backends.oracle.creation import OracleCreation from django.contrib.gis.db.backends.oracle.introspection import OracleIntrospection from django.contrib.gis.db.backends.oracle.operations import OracleOperations +from django.contrib.gis.db.backends.oracle.schema import OracleGISSchemaEditor class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures): @@ -20,3 +21,6 @@ class DatabaseWrapper(OracleDatabaseWrapper): self.ops = OracleOperations(self) self.creation = OracleCreation(self) self.introspection = OracleIntrospection(self) + + def schema_editor(self, *args, **kwargs): + return OracleGISSchemaEditor(self, *args, **kwargs) diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index aa06e98433..1c58c335e3 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -142,6 +142,9 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): truncate_params = {'relate': None} + def geo_quote_name(self, name): + return super(OracleOperations, self).geo_quote_name(name).upper() + def get_db_converters(self, internal_type): converters = super(OracleOperations, self).get_db_converters(internal_type) geometry_fields = ( diff --git a/django/contrib/gis/db/backends/oracle/schema.py b/django/contrib/gis/db/backends/oracle/schema.py new file mode 100644 index 0000000000..973a56e766 --- /dev/null +++ b/django/contrib/gis/db/backends/oracle/schema.py @@ -0,0 +1,94 @@ +from django.contrib.gis.db.models.fields import GeometryField +from django.db.backends.oracle.schema import DatabaseSchemaEditor +from django.db.backends.utils import truncate_name + + +class OracleGISSchemaEditor(DatabaseSchemaEditor): + sql_add_geometry_metadata = (""" + INSERT INTO USER_SDO_GEOM_METADATA + ("TABLE_NAME", "COLUMN_NAME", "DIMINFO", "SRID") + VALUES ( + %(table)s, + %(column)s, + MDSYS.SDO_DIM_ARRAY( + MDSYS.SDO_DIM_ELEMENT('LONG', %(dim0)s, %(dim2)s, %(tolerance)s), + MDSYS.SDO_DIM_ELEMENT('LAT', %(dim1)s, %(dim3)s, %(tolerance)s) + ), + %(srid)s + )""") + sql_add_spatial_index = 'CREATE INDEX %(index)s ON %(table)s(%(column)s) INDEXTYPE IS MDSYS.SPATIAL_INDEX' + sql_drop_spatial_index = 'DROP INDEX %(index)s' + sql_clear_geometry_table_metadata = 'DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s' + sql_clear_geometry_field_metadata = ( + 'DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s ' + 'AND COLUMN_NAME = %(column)s' + ) + + def __init__(self, *args, **kwargs): + super(OracleGISSchemaEditor, self).__init__(*args, **kwargs) + self.geometry_sql = [] + + def geo_quote_name(self, name): + return self.connection.ops.geo_quote_name(name) + + def column_sql(self, model, field, include_default=False): + column_sql = super(OracleGISSchemaEditor, self).column_sql(model, field, include_default) + if isinstance(field, GeometryField): + db_table = model._meta.db_table + self.geometry_sql.append( + self.sql_add_geometry_metadata % { + 'table': self.geo_quote_name(db_table), + 'column': self.geo_quote_name(field.column), + 'dim0': field._extent[0], + 'dim1': field._extent[1], + 'dim2': field._extent[2], + 'dim3': field._extent[3], + 'tolerance': field._tolerance, + 'srid': field.srid, + } + ) + if field.spatial_index: + self.geometry_sql.append( + self.sql_add_spatial_index % { + 'index': self.quote_name(self._create_spatial_index_name(model, field)), + 'table': self.quote_name(db_table), + 'column': self.quote_name(field.column), + } + ) + return column_sql + + def create_model(self, model): + super(OracleGISSchemaEditor, self).create_model(model) + self.run_geometry_sql() + + def delete_model(self, model): + super(OracleGISSchemaEditor, self).delete_model(model) + self.execute(self.sql_clear_geometry_table_metadata % { + 'table': self.geo_quote_name(model._meta.db_table), + }) + + def add_field(self, model, field): + super(OracleGISSchemaEditor, self).add_field(model, field) + self.run_geometry_sql() + + def remove_field(self, model, field): + if isinstance(field, GeometryField): + self.execute(self.sql_clear_geometry_field_metadata % { + 'table': self.geo_quote_name(model._meta.db_table), + 'column': self.geo_quote_name(field.column), + }) + if field.spatial_index: + self.execute(self.sql_drop_spatial_index % { + 'index': self.quote_name(self._create_spatial_index_name(model, field)), + }) + super(OracleGISSchemaEditor, self).remove_field(model, field) + + def run_geometry_sql(self): + for sql in self.geometry_sql: + self.execute(sql) + self.geometry_sql = [] + + 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. + return truncate_name('%s_%s_id' % (model._meta.db_table, field.column), 30) diff --git a/django/contrib/gis/tests/gis_migrations/test_operations.py b/django/contrib/gis/tests/gis_migrations/test_operations.py index 32072eec86..c979317630 100644 --- a/django/contrib/gis/tests/gis_migrations/test_operations.py +++ b/django/contrib/gis/tests/gis_migrations/test_operations.py @@ -51,6 +51,17 @@ class OperationTests(TransactionTestCase): )] return self.apply_operations('gis', ProjectState(), operations) + def assertGeometryColumnsCount(self, expected_count): + table_name = "gis_neighborhood" + if connection.features.uppercases_column_names: + table_name = table_name.upper() + self.assertEqual( + GeometryColumns.objects.filter(**{ + GeometryColumns.table_name_col(): table_name, + }).count(), + expected_count + ) + def test_add_gis_field(self): """ Tests the AddField operation with a GIS-enabled column. @@ -70,10 +81,7 @@ class OperationTests(TransactionTestCase): # Test GeometryColumns when available if HAS_GEOMETRY_COLUMNS: - self.assertEqual( - GeometryColumns.objects.filter(**{GeometryColumns.table_name_col(): "gis_neighborhood"}).count(), - 2 - ) + self.assertGeometryColumnsCount(2) if self.has_spatial_indexes: with connection.cursor() as cursor: @@ -95,10 +103,7 @@ class OperationTests(TransactionTestCase): # Test GeometryColumns when available if HAS_GEOMETRY_COLUMNS: - self.assertEqual( - GeometryColumns.objects.filter(**{GeometryColumns.table_name_col(): "gis_neighborhood"}).count(), - 0 - ) + self.assertGeometryColumnsCount(0) def test_create_model_spatial_index(self): self.current_state = self.set_up_test_model() diff --git a/docs/releases/1.7.1.txt b/docs/releases/1.7.1.txt index 3b53534185..a1149d37cd 100644 --- a/docs/releases/1.7.1.txt +++ b/docs/releases/1.7.1.txt @@ -77,6 +77,9 @@ Bugfixes * Added ``SchemaEditor`` for MySQL GIS backend so that spatial indexes will be created for apps with migrations (:ticket:`23538`). +* Added ``SchemaEditor`` for Oracle GIS backend so that spatial metadata and + indexes will be created for apps with migrations (:ticket:`23537`). + * Coerced the ``related_name`` model field option to unicode during migration generation to generate migrations that work with both Python 2 and 3 (:ticket:`23455`).