Initial commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper
|
||||
|
||||
from .features import DatabaseFeatures
|
||||
from .introspection import MySQLIntrospection
|
||||
from .operations import MySQLOperations
|
||||
from .schema import MySQLGISSchemaEditor
|
||||
|
||||
|
||||
class DatabaseWrapper(MySQLDatabaseWrapper):
|
||||
SchemaEditorClass = MySQLGISSchemaEditor
|
||||
# Classes instantiated in __init__().
|
||||
features_class = DatabaseFeatures
|
||||
introspection_class = MySQLIntrospection
|
||||
ops_class = MySQLOperations
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures
|
||||
from django.db.backends.mysql.features import DatabaseFeatures as MySQLDatabaseFeatures
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
|
||||
empty_intersection_returns_none = False
|
||||
has_spatialrefsys_table = False
|
||||
supports_add_srs_entry = False
|
||||
supports_distance_geodetic = False
|
||||
supports_length_geodetic = False
|
||||
supports_area_geodetic = False
|
||||
supports_transform = False
|
||||
supports_null_geometries = False
|
||||
supports_num_points_poly = False
|
||||
unsupported_geojson_options = {"crs"}
|
||||
|
||||
@cached_property
|
||||
def supports_geometry_field_unique_index(self):
|
||||
# Not supported in MySQL since https://dev.mysql.com/worklog/task/?id=11808
|
||||
return self.connection.mysql_is_mariadb
|
||||
@@ -0,0 +1,33 @@
|
||||
from MySQLdb.constants import FIELD_TYPE
|
||||
|
||||
from django.contrib.gis.gdal import OGRGeomType
|
||||
from django.db.backends.mysql.introspection import DatabaseIntrospection
|
||||
|
||||
|
||||
class MySQLIntrospection(DatabaseIntrospection):
|
||||
# Updating the data_types_reverse dictionary with the appropriate
|
||||
# type for Geometry fields.
|
||||
data_types_reverse = DatabaseIntrospection.data_types_reverse.copy()
|
||||
data_types_reverse[FIELD_TYPE.GEOMETRY] = "GeometryField"
|
||||
|
||||
def get_geometry_type(self, table_name, description):
|
||||
with self.connection.cursor() as cursor:
|
||||
# In order to get the specific geometry type of the field,
|
||||
# we introspect on the table definition using `DESCRIBE`.
|
||||
cursor.execute("DESCRIBE %s" % self.connection.ops.quote_name(table_name))
|
||||
# Increment over description info until we get to the geometry
|
||||
# column.
|
||||
for column, typ, null, key, default, extra in cursor.fetchall():
|
||||
if column == description.name:
|
||||
# Using OGRGeomType to convert from OGC name to Django field.
|
||||
# MySQL does not support 3D or SRIDs, so the field params
|
||||
# are empty.
|
||||
field_type = OGRGeomType(typ).django
|
||||
field_params = {}
|
||||
break
|
||||
return field_type, field_params
|
||||
|
||||
def supports_spatial_index(self, cursor, table_name):
|
||||
# Supported with MyISAM, Aria, or InnoDB.
|
||||
storage_engine = self.get_storage_engine(cursor, table_name)
|
||||
return storage_engine in ("MyISAM", "Aria", "InnoDB")
|
||||
@@ -0,0 +1,146 @@
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
|
||||
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.utils import SpatialOperator
|
||||
from django.contrib.gis.geos.geometry import GEOSGeometryBase
|
||||
from django.contrib.gis.geos.prototypes.io import wkb_r
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.db.backends.mysql.operations import DatabaseOperations
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
|
||||
name = "mysql"
|
||||
geom_func_prefix = "ST_"
|
||||
|
||||
Adapter = WKTAdapter
|
||||
|
||||
@cached_property
|
||||
def mariadb(self):
|
||||
return self.connection.mysql_is_mariadb
|
||||
|
||||
@cached_property
|
||||
def mysql(self):
|
||||
return not self.connection.mysql_is_mariadb
|
||||
|
||||
@cached_property
|
||||
def select(self):
|
||||
return self.geom_func_prefix + "AsBinary(%s)"
|
||||
|
||||
@cached_property
|
||||
def from_text(self):
|
||||
return self.geom_func_prefix + "GeomFromText"
|
||||
|
||||
@cached_property
|
||||
def collect(self):
|
||||
if self.connection.features.supports_collect_aggr:
|
||||
return self.geom_func_prefix + "Collect"
|
||||
|
||||
@cached_property
|
||||
def gis_operators(self):
|
||||
operators = {
|
||||
"bbcontains": SpatialOperator(
|
||||
func="MBRContains"
|
||||
), # For consistency w/PostGIS API
|
||||
"bboverlaps": SpatialOperator(func="MBROverlaps"), # ...
|
||||
"contained": SpatialOperator(func="MBRWithin"), # ...
|
||||
"contains": SpatialOperator(func="ST_Contains"),
|
||||
"crosses": SpatialOperator(func="ST_Crosses"),
|
||||
"disjoint": SpatialOperator(func="ST_Disjoint"),
|
||||
"equals": SpatialOperator(func="ST_Equals"),
|
||||
"exact": SpatialOperator(func="ST_Equals"),
|
||||
"intersects": SpatialOperator(func="ST_Intersects"),
|
||||
"overlaps": SpatialOperator(func="ST_Overlaps"),
|
||||
"same_as": SpatialOperator(func="ST_Equals"),
|
||||
"touches": SpatialOperator(func="ST_Touches"),
|
||||
"within": SpatialOperator(func="ST_Within"),
|
||||
}
|
||||
if self.connection.mysql_is_mariadb:
|
||||
operators["relate"] = SpatialOperator(func="ST_Relate")
|
||||
else:
|
||||
operators["covers"] = SpatialOperator(func="MBRCovers")
|
||||
operators["coveredby"] = SpatialOperator(func="MBRCoveredBy")
|
||||
return operators
|
||||
|
||||
@cached_property
|
||||
def disallowed_aggregates(self):
|
||||
disallowed_aggregates = [
|
||||
models.Extent,
|
||||
models.Extent3D,
|
||||
models.MakeLine,
|
||||
models.Union,
|
||||
]
|
||||
is_mariadb = self.connection.mysql_is_mariadb
|
||||
if is_mariadb or self.connection.mysql_version < (8, 0, 24):
|
||||
disallowed_aggregates.insert(0, models.Collect)
|
||||
return tuple(disallowed_aggregates)
|
||||
|
||||
function_names = {
|
||||
"FromWKB": "ST_GeomFromWKB",
|
||||
"FromWKT": "ST_GeomFromText",
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def unsupported_functions(self):
|
||||
unsupported = {
|
||||
"AsGML",
|
||||
"AsKML",
|
||||
"AsSVG",
|
||||
"Azimuth",
|
||||
"BoundingCircle",
|
||||
"ClosestPoint",
|
||||
"ForcePolygonCW",
|
||||
"GeometryDistance",
|
||||
"IsEmpty",
|
||||
"LineLocatePoint",
|
||||
"MakeValid",
|
||||
"MemSize",
|
||||
"Perimeter",
|
||||
"PointOnSurface",
|
||||
"Reverse",
|
||||
"Scale",
|
||||
"SnapToGrid",
|
||||
"Transform",
|
||||
"Translate",
|
||||
}
|
||||
if self.connection.mysql_is_mariadb:
|
||||
unsupported.remove("PointOnSurface")
|
||||
unsupported.update({"GeoHash", "IsValid"})
|
||||
return unsupported
|
||||
|
||||
def geo_db_type(self, f):
|
||||
return f.geom_type
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
raise ValueError(
|
||||
"Only numeric values of degree units are allowed on "
|
||||
"geodetic distance queries."
|
||||
)
|
||||
dist_param = getattr(
|
||||
value, Distance.unit_attname(f.units_name(self.connection))
|
||||
)
|
||||
else:
|
||||
dist_param = value
|
||||
return [dist_param]
|
||||
|
||||
def get_geometry_converter(self, expression):
|
||||
read = wkb_r().read
|
||||
srid = expression.output_field.srid
|
||||
if srid == -1:
|
||||
srid = None
|
||||
geom_class = expression.output_field.geom_class
|
||||
|
||||
def converter(value, expression, connection):
|
||||
if value is not None:
|
||||
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
|
||||
if srid:
|
||||
geom.srid = srid
|
||||
return geom
|
||||
|
||||
return converter
|
||||
|
||||
def spatial_aggregate_name(self, agg_name):
|
||||
return getattr(self, agg_name.lower())
|
||||
@@ -0,0 +1,112 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.db 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)"
|
||||
|
||||
def skip_default(self, field):
|
||||
# Geometry fields are stored as BLOB/TEXT, for which MySQL < 8.0.13
|
||||
# doesn't support defaults.
|
||||
if (
|
||||
isinstance(field, GeometryField)
|
||||
and not self._supports_limited_data_type_defaults
|
||||
):
|
||||
return True
|
||||
return super().skip_default(field)
|
||||
|
||||
def quote_value(self, value):
|
||||
if isinstance(value, self.connection.ops.Adapter):
|
||||
return super().quote_value(str(value))
|
||||
return super().quote_value(value)
|
||||
|
||||
def _field_indexes_sql(self, model, field):
|
||||
if isinstance(field, GeometryField) and field.spatial_index and not field.null:
|
||||
with self.connection.cursor() as cursor:
|
||||
supports_spatial_index = (
|
||||
self.connection.introspection.supports_spatial_index(
|
||||
cursor, model._meta.db_table
|
||||
)
|
||||
)
|
||||
sql = self._create_spatial_index_sql(model, field)
|
||||
if supports_spatial_index:
|
||||
return [sql]
|
||||
else:
|
||||
logger.error(
|
||||
f"Cannot create SPATIAL INDEX {sql}. Only MyISAM, Aria, and InnoDB "
|
||||
f"support them.",
|
||||
)
|
||||
return []
|
||||
return super()._field_indexes_sql(model, field)
|
||||
|
||||
def remove_field(self, model, field):
|
||||
if isinstance(field, GeometryField) and field.spatial_index and not field.null:
|
||||
sql = self._delete_spatial_index_sql(model, field)
|
||||
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().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)
|
||||
|
||||
def _create_spatial_index_sql(self, model, field):
|
||||
index_name = self._create_spatial_index_name(model, field)
|
||||
qn = self.connection.ops.quote_name
|
||||
return self.sql_add_spatial_index % {
|
||||
"index": qn(index_name),
|
||||
"table": qn(model._meta.db_table),
|
||||
"column": qn(field.column),
|
||||
}
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user