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 supports_3d_functions = False
# Does the database support SRID transform operations? # Does the database support SRID transform operations?
supports_transform = True 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? # Can geometry fields be null?
supports_null_geometries = True supports_null_geometries = True
# Are empty geometries supported? # Are empty geometries supported?

View File

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

View File

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

View File

@ -25,21 +25,15 @@ GeoDjango currently provides the following spatial database backends:
MySQL Spatial Limitations MySQL Spatial Limitations
------------------------- -------------------------
MySQL's spatial extensions only support bounding box operations Before MySQL 5.6.1, spatial extensions only support bounding box operations
(what MySQL calls minimum bounding rectangles, or MBR). Specifically, (what MySQL calls minimum bounding rectangles, or MBR). Specifically, MySQL did
`MySQL does not conform to the OGC standard not conform to the OGC standard. Django supports spatial functions operating on
<https://dev.mysql.com/doc/refman/en/spatial-relation-functions.html>`_: 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 .. versionchanged:: 3.0
[``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.
In other words, while spatial lookups such as :lookup:`contains <gis-contains>` Support for spatial functions operating on real geometries was added.
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.
.. warning:: .. warning::

View File

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

View File

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