Fixed #23538 -- Added SchemaEditor for MySQL GIS.

Thanks Claude Paroz for suggestions and review.
This commit is contained in:
Tim Graham 2014-09-23 14:13:59 -04:00
parent 215aa4f53b
commit 74e7f91e6d
8 changed files with 132 additions and 4 deletions

View File

@ -6,6 +6,7 @@ from django.contrib.gis.db.backends.base import BaseSpatialFeatures
from django.contrib.gis.db.backends.mysql.creation import MySQLCreation from django.contrib.gis.db.backends.mysql.creation import MySQLCreation
from django.contrib.gis.db.backends.mysql.introspection import MySQLIntrospection from django.contrib.gis.db.backends.mysql.introspection import MySQLIntrospection
from django.contrib.gis.db.backends.mysql.operations import MySQLOperations from django.contrib.gis.db.backends.mysql.operations import MySQLOperations
from django.contrib.gis.db.backends.mysql.schema import MySQLGISSchemaEditor
class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
@ -25,3 +26,7 @@ class DatabaseWrapper(MySQLDatabaseWrapper):
self.creation = MySQLCreation(self) self.creation = MySQLCreation(self)
self.ops = MySQLOperations(self) self.ops = MySQLOperations(self)
self.introspection = MySQLIntrospection(self) self.introspection = MySQLIntrospection(self)
def schema_editor(self, *args, **kwargs):
"Returns a new instance of this backend's SchemaEditor"
return MySQLGISSchemaEditor(self, *args, **kwargs)

View File

@ -31,3 +31,11 @@ class MySQLIntrospection(DatabaseIntrospection):
cursor.close() cursor.close()
return field_type, field_params return field_type, field_params
def supports_spatial_index(self, cursor, table_name):
# Supported with MyISAM, or InnoDB on MySQL 5.7.5+
storage_engine = self.get_storage_engine(cursor, table_name)
return (
(storage_engine == 'InnoDB' and self.connection.mysql_version >= (5, 7, 5)) or
storage_engine == 'MyISAM'
)

View File

@ -0,0 +1,70 @@
import logging
from django.contrib.gis.db.models.fields import GeometryField
from django.db.utils import OperationalError
from django.db.backends.mysql.schema import DatabaseSchemaEditor
logger = logging.getLogger('django.contrib.gis')
class MySQLGISSchemaEditor(DatabaseSchemaEditor):
sql_add_spatial_index = 'CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)'
sql_drop_spatial_index = 'DROP INDEX %(index)s ON %(table)s'
def __init__(self, *args, **kwargs):
super(MySQLGISSchemaEditor, self).__init__(*args, **kwargs)
self.geometry_sql = []
def column_sql(self, model, field, include_default=False):
column_sql = super(MySQLGISSchemaEditor, self).column_sql(model, field, include_default)
# MySQL doesn't support spatial indexes on NULL columns
if isinstance(field, GeometryField) and field.spatial_index and not field.null:
qn = self.connection.ops.quote_name
db_table = model._meta.db_table
self.geometry_sql.append(
self.sql_add_spatial_index % {
'index': qn(self._create_spatial_index_name(model, field)),
'table': qn(db_table),
'column': qn(field.column),
}
)
return column_sql
def create_model(self, model):
super(MySQLGISSchemaEditor, self).create_model(model)
self.create_spatial_indexes()
def add_field(self, model, field):
super(MySQLGISSchemaEditor, self).add_field(model, field)
self.create_spatial_indexes()
def remove_field(self, model, field):
if isinstance(field, GeometryField) and field.spatial_index:
qn = self.connection.ops.quote_name
sql = self.sql_drop_spatial_index % {
'index': qn(self._create_spatial_index_name(model, field)),
'table': qn(model._meta.db_table),
}
try:
self.execute(sql)
except OperationalError:
logger.error(
"Couldn't remove spatial index: %s (may be expected "
"if your storage engine doesn't support them)." % sql
)
super(MySQLGISSchemaEditor, self).remove_field(model, field)
def _create_spatial_index_name(self, model, field):
return '%s_%s_id' % (model._meta.db_table, field.column)
def create_spatial_indexes(self):
for sql in self.geometry_sql:
try:
self.execute(sql)
except OperationalError:
logger.error(
"Cannot create SPATIAL INDEX %s. Only MyISAM and (as of "
"MySQL 5.7.5) InnoDB support them." % sql
)
self.geometry_sql = []

View File

@ -53,3 +53,12 @@ class SpatiaLiteIntrospection(DatabaseIntrospection):
cursor.close() cursor.close()
return field_type, field_params return field_type, field_params
def get_indexes(self, cursor, table_name):
indexes = super(SpatiaLiteIntrospection, self).get_indexes(cursor, table_name)
cursor.execute('SELECT f_geometry_column '
'FROM geometry_columns '
'WHERE f_table_name=%s AND spatial_index_enabled=1', (table_name,))
for row in cursor.fetchall():
indexes[row[0]] = {'primary_key': False, 'unique': False}
return indexes

View File

@ -12,7 +12,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=100, unique=True)), ('name', models.CharField(max_length=100, unique=True)),
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326, null=True)), ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326)),
], ],
options={ options={
}, },
@ -25,7 +25,7 @@ class Migration(migrations.Migration):
('neighborhood', models.ForeignKey(to='gis.Neighborhood', to_field='id', null=True)), ('neighborhood', models.ForeignKey(to='gis.Neighborhood', to_field='id', null=True)),
('address', models.CharField(max_length=100)), ('address', models.CharField(max_length=100)),
('zip_code', models.IntegerField(null=True, blank=True)), ('zip_code', models.IntegerField(null=True, blank=True)),
('geom', django.contrib.gis.db.models.fields.PointField(srid=4326, null=True, geography=True)), ('geom', django.contrib.gis.db.models.fields.PointField(srid=4326, geography=True)),
], ],
options={ options={
}, },

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.gis.tests.utils import mysql
from django.db import connection, migrations, models from django.db import connection, migrations, models
from django.db.migrations.migration import Migration from django.db.migrations.migration import Migration
from django.db.migrations.state import ProjectState from django.db.migrations.state import ProjectState
@ -45,7 +46,7 @@ class OperationTests(TransactionTestCase):
[ [
("id", models.AutoField(primary_key=True)), ("id", models.AutoField(primary_key=True)),
('name', models.CharField(max_length=100, unique=True)), ('name', models.CharField(max_length=100, unique=True)),
('geom', fields.MultiPolygonField(srid=4326, null=True)), ('geom', fields.MultiPolygonField(srid=4326)),
], ],
)] )]
return self.apply_operations('gis', ProjectState(), operations) return self.apply_operations('gis', ProjectState(), operations)
@ -58,7 +59,7 @@ class OperationTests(TransactionTestCase):
operation = migrations.AddField( operation = migrations.AddField(
"Neighborhood", "Neighborhood",
"path", "path",
fields.LineStringField(srid=4326, null=True, blank=True), fields.LineStringField(srid=4326),
) )
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("gis", new_state) operation.state_forwards("gis", new_state)
@ -74,6 +75,11 @@ class OperationTests(TransactionTestCase):
2 2
) )
if self.has_spatial_indexes:
with connection.cursor() as cursor:
indexes = connection.introspection.get_indexes(cursor, "gis_neighborhood")
self.assertIn('path', indexes)
def test_remove_gis_field(self): def test_remove_gis_field(self):
""" """
Tests the RemoveField operation with a GIS-enabled column. Tests the RemoveField operation with a GIS-enabled column.
@ -93,3 +99,20 @@ class OperationTests(TransactionTestCase):
GeometryColumns.objects.filter(**{GeometryColumns.table_name_col(): "gis_neighborhood"}).count(), GeometryColumns.objects.filter(**{GeometryColumns.table_name_col(): "gis_neighborhood"}).count(),
0 0
) )
def test_create_model_spatial_index(self):
self.current_state = self.set_up_test_model()
if not self.has_spatial_indexes:
self.skipTest("No support for Spatial indexes")
with connection.cursor() as cursor:
indexes = connection.introspection.get_indexes(cursor, "gis_neighborhood")
self.assertIn('geom', indexes)
@property
def has_spatial_indexes(self):
if mysql:
with connection.cursor() as cursor:
return connection.introspection.supports_spatial_index(cursor, "gis_neighborhood")
return True

View File

@ -127,6 +127,16 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
indexes[row[4]]['unique'] = True indexes[row[4]]['unique'] = True
return indexes return indexes
def get_storage_engine(self, cursor, table_name):
"""
Retrieves the storage engine for a given table.
"""
cursor.execute(
"SELECT engine "
"FROM information_schema.tables "
"WHERE table_name = %s", [table_name])
return cursor.fetchone()[0]
def get_constraints(self, cursor, table_name): def get_constraints(self, cursor, table_name):
""" """
Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns. Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns.

View File

@ -73,3 +73,6 @@ Bugfixes
* Fixed bug in migrations that prevented foreign key constraints to unmanaged * Fixed bug in migrations that prevented foreign key constraints to unmanaged
models with a custom primary key (:ticket:`23415`). models with a custom primary key (:ticket:`23415`).
* Added ``SchemaEditor`` for MySQL GIS backend so that spatial indexes will be
created for apps with migrations (:ticket:`23538`).