Fixed #22423 -- Added support for MySQL operators on real geometries.

Thanks Viswanathan Mahalingam for the report and initial patch, and
Nicke Pope and Tim Graham for the review.
This commit is contained in:
Claude Paroz 2019-01-19 15:28:42 +01:00 committed by Tim Graham
parent 8cf9dbee6a
commit 15715bf2a2
8 changed files with 85 additions and 55 deletions

View File

@ -21,8 +21,6 @@ class BaseSpatialFeatures:
supports_3d_functions = False
# Does the database support SRID transform operations?
supports_transform = True
# Do geometric relationship operations operate on real shapes (or only on bounding boxes)?
supports_real_shape_operations = True
# Can geometry fields be null?
supports_null_geometries = True
# Are empty geometries supported?

View File

@ -12,7 +12,6 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
supports_length_geodetic = False
supports_area_geodetic = False
supports_transform = False
supports_real_shape_operations = False
supports_null_geometries = False
supports_num_points_poly = False

View File

@ -29,22 +29,20 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
@cached_property
def gis_operators(self):
MBREquals = 'MBREqual' if (
self.connection.mysql_is_mariadb or self.connection.mysql_version < (5, 7, 6)
) else 'MBREquals'
return {
'bbcontains': SpatialOperator(func='MBRContains'), # For consistency w/PostGIS API
'bboverlaps': SpatialOperator(func='MBROverlaps'), # ...
'contained': SpatialOperator(func='MBRWithin'), # ...
'contains': SpatialOperator(func='MBRContains'),
'disjoint': SpatialOperator(func='MBRDisjoint'),
'equals': SpatialOperator(func=MBREquals),
'exact': SpatialOperator(func=MBREquals),
'intersects': SpatialOperator(func='MBRIntersects'),
'overlaps': SpatialOperator(func='MBROverlaps'),
'same_as': SpatialOperator(func=MBREquals),
'touches': SpatialOperator(func='MBRTouches'),
'within': 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'),
}
disallowed_aggregates = (

View File

@ -25,21 +25,15 @@ GeoDjango currently provides the following spatial database backends:
MySQL Spatial Limitations
-------------------------
MySQL's spatial extensions only support bounding box operations
(what MySQL calls minimum bounding rectangles, or MBR). Specifically,
`MySQL does not conform to the OGC standard
<https://dev.mysql.com/doc/refman/en/spatial-relation-functions.html>`_:
Before MySQL 5.6.1, spatial extensions only support bounding box operations
(what MySQL calls minimum bounding rectangles, or MBR). Specifically, MySQL did
not conform to the OGC standard. Django supports spatial functions operating on
real geometries available in modern MySQL versions. However, the spatial
functions are not as rich as other backends like PostGIS.
Currently, MySQL does not implement these functions
[``Contains``, ``Crosses``, ``Disjoint``, ``Intersects``, ``Overlaps``,
``Touches``, ``Within``]
according to the specification. Those that are implemented return
the same result as the corresponding MBR-based functions.
.. versionchanged:: 3.0
In other words, while spatial lookups such as :lookup:`contains <gis-contains>`
are available in GeoDjango when using MySQL, the results returned are really
equivalent to what would be returned when using :lookup:`bbcontains`
on a different spatial backend.
Support for spatial functions operating on real geometries was added.
.. warning::

View File

@ -148,10 +148,15 @@ Backend SQL Equivalent
========== ============================
PostGIS ``ST_Contains(poly, geom)``
Oracle ``SDO_CONTAINS(poly, geom)``
MySQL ``MBRContains(poly, geom)``
MySQL ``ST_Contains(poly, geom)``
SpatiaLite ``Contains(poly, geom)``
========== ============================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBRContains`` and operates only on bounding
boxes.
.. fieldlookup:: contains_properly
``contains_properly``
@ -233,7 +238,7 @@ SpatiaLite ``Covers(poly, geom)``
-----------
*Availability*: `PostGIS <https://postgis.net/docs/ST_Crosses.html>`__,
SpatiaLite, PGRaster (Conversion)
MySQL, SpatiaLite, PGRaster (Conversion)
Tests if the geometry field spatially crosses the lookup geometry.
@ -245,9 +250,14 @@ Example::
Backend SQL Equivalent
========== ==========================
PostGIS ``ST_Crosses(poly, geom)``
MySQL ``ST_Crosses(poly, geom)``
SpatiaLite ``Crosses(poly, geom)``
========== ==========================
.. versionchanged:: 3.0
MySQL support was added.
.. fieldlookup:: disjoint
``disjoint``
@ -267,10 +277,15 @@ Backend SQL Equivalent
========== =================================================
PostGIS ``ST_Disjoint(poly, geom)``
Oracle ``SDO_GEOM.RELATE(poly, 'DISJOINT', geom, 0.05)``
MySQL ``MBRDisjoint(poly, geom)``
MySQL ``ST_Disjoint(poly, geom)``
SpatiaLite ``Disjoint(poly, geom)``
========== =================================================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBRDisjoint`` and operates only on bounding
boxes.
.. fieldlookup:: equals
``equals``
@ -290,10 +305,15 @@ Backend SQL Equivalent
========== =================================================
PostGIS ``ST_Equals(poly, geom)``
Oracle ``SDO_EQUAL(poly, geom)``
MySQL ``MBREquals(poly, geom)``
MySQL ``ST_Equals(poly, geom)``
SpatiaLite ``Equals(poly, geom)``
========== =================================================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBREquals`` and operates only on bounding
boxes.
.. fieldlookup:: exact
.. fieldlookup:: same_as
@ -303,8 +323,8 @@ SpatiaLite ``Equals(poly, geom)``
*Availability*: `PostGIS <https://postgis.net/docs/ST_Geometry_Same.html>`__,
Oracle, MySQL, SpatiaLite, PGRaster (Bilateral)
Tests if the geometry field is "equal" to the lookup geometry. On Oracle and
SpatiaLite it tests spatial equality, while on MySQL and PostGIS it tests
Tests if the geometry field is "equal" to the lookup geometry. On Oracle,
MySQL, and SpatiaLite, it tests spatial equality, while on PostGIS it tests
equality of bounding boxes.
Example::
@ -316,10 +336,15 @@ Backend SQL Equivalent
========== =================================================
PostGIS ``poly ~= geom``
Oracle ``SDO_EQUAL(poly, geom)``
MySQL ``MBREquals(poly, geom)``
MySQL ``ST_Equals(poly, geom)``
SpatiaLite ``Equals(poly, geom)``
========== =================================================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBREquals`` and operates only on bounding
boxes.
.. fieldlookup:: intersects
``intersects``
@ -339,10 +364,15 @@ Backend SQL Equivalent
========== =================================================
PostGIS ``ST_Intersects(poly, geom)``
Oracle ``SDO_OVERLAPBDYINTERSECT(poly, geom)``
MySQL ``MBRIntersects(poly, geom)``
MySQL ``ST_Intersects(poly, geom)``
SpatiaLite ``Intersects(poly, geom)``
========== =================================================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBRIntersects`` and operates only on
bounding boxes.
.. fieldlookup:: isvalid
``isvalid``
@ -379,10 +409,15 @@ Backend SQL Equivalent
========== ============================
PostGIS ``ST_Overlaps(poly, geom)``
Oracle ``SDO_OVERLAPS(poly, geom)``
MySQL ``MBROverlaps(poly, geom)``
MySQL ``ST_Overlaps(poly, geom)``
SpatiaLite ``Overlaps(poly, geom)``
========== ============================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBROverlaps`` and operates only on bounding
boxes.
.. fieldlookup:: relate
``relate``
@ -464,11 +499,16 @@ Example::
Backend SQL Equivalent
========== ==========================
PostGIS ``ST_Touches(poly, geom)``
MySQL ``MBRTouches(poly, geom)``
MySQL ``ST_Touches(poly, geom)``
Oracle ``SDO_TOUCH(poly, geom)``
SpatiaLite ``Touches(poly, geom)``
========== ==========================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBRTouches`` and operates only on bounding
boxes.
.. fieldlookup:: within
``within``
@ -487,11 +527,16 @@ Example::
Backend SQL Equivalent
========== ==========================
PostGIS ``ST_Within(poly, geom)``
MySQL ``MBRWithin(poly, geom)``
MySQL ``ST_Within(poly, geom)``
Oracle ``SDO_INSIDE(poly, geom)``
SpatiaLite ``Within(poly, geom)``
========== ==========================
.. versionchanged:: 3.0
In older versions, MySQL uses ``MBRWithin`` and operates only on bounding
boxes.
.. fieldlookup:: left
``left``

View File

@ -59,7 +59,7 @@ supported versions, and any notes for each of the supported database backends:
Database Library Requirements Supported Versions Notes
================== ============================== ================== =========================================
PostgreSQL GEOS, GDAL, PROJ.4, PostGIS 9.5+ Requires PostGIS.
MySQL GEOS, GDAL 5.6+ Not OGC-compliant; :ref:`limited functionality <mysql-spatial-limitations>`.
MySQL GEOS, GDAL 5.6.1+ :ref:`Limited functionality <mysql-spatial-limitations>`.
Oracle GEOS, GDAL 12.2+ XE not supported.
SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.8.3+ Requires SpatiaLite 4.3+
================== ============================== ================== =========================================

View File

@ -64,7 +64,8 @@ Minor features
:mod:`django.contrib.gis`
~~~~~~~~~~~~~~~~~~~~~~~~~
* ...
* Allowed MySQL spatial lookup functions to operate on real geometries.
Previous support was limited to bounding boxes.
:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1,4 +1,5 @@
import tempfile
import unittest
from io import StringIO
from django.contrib.gis import gdal
@ -226,14 +227,15 @@ class GeoLookupTest(TestCase):
def test_disjoint_lookup(self):
"Testing the `disjoint` lookup type."
if (connection.vendor == 'mysql' and not connection.mysql_is_mariadb and
connection.mysql_version < (8, 0, 0)):
raise unittest.SkipTest('MySQL < 8 gives different results.')
ptown = City.objects.get(name='Pueblo')
qs1 = City.objects.filter(point__disjoint=ptown.point)
self.assertEqual(7, qs1.count())
if connection.features.supports_real_shape_operations:
qs2 = State.objects.filter(poly__disjoint=ptown.point)
self.assertEqual(1, qs2.count())
self.assertEqual('Kansas', qs2[0].name)
qs2 = State.objects.filter(poly__disjoint=ptown.point)
self.assertEqual(1, qs2.count())
self.assertEqual('Kansas', qs2[0].name)
def test_contains_contained_lookups(self):
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
@ -271,8 +273,7 @@ class GeoLookupTest(TestCase):
# Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas)
# are not contained in Texas or New Zealand.
self.assertEqual(len(Country.objects.filter(mpoly__contains=pueblo.point)), 0) # Query w/GEOSGeometry object
self.assertEqual(len(Country.objects.filter(mpoly__contains=okcity.point.wkt)),
0 if connection.features.supports_real_shape_operations else 1) # Query w/WKT
self.assertEqual(len(Country.objects.filter(mpoly__contains=okcity.point.wkt)), 0) # Query w/WKT
# OK City is contained w/in bounding box of Texas.
if connection.features.supports_bbcontains_lookup:
@ -570,13 +571,7 @@ class GeoQuerySetTest(TestCase):
"""
tex_cities = City.objects.filter(
point__within=Country.objects.filter(name='Texas').values('mpoly')).order_by('name')
expected = ['Dallas', 'Houston']
if not connection.features.supports_real_shape_operations:
expected.append('Oklahoma City')
self.assertEqual(
list(tex_cities.values_list('name', flat=True)),
expected
)
self.assertEqual(list(tex_cities.values_list('name', flat=True)), ['Dallas', 'Houston'])
def test_non_concrete_field(self):
NonConcreteModel.objects.create(point=Point(0, 0), name='name')