Commit a8f07530 authored by Tim Graham's avatar Tim Graham
Browse files

Fixed #23537 -- Added Oracle GIS SchemaEditor.

Thanks Shai Berger for review.
parent 45bd7b3b
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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)
+3 −0
Original line number Diff line number Diff line
@@ -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 = (
+94 −0
Original line number Diff line number Diff line
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)
+13 −8
Original line number Diff line number Diff line
@@ -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()
+3 −0
Original line number Diff line number Diff line
@@ -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`).