From 15715bf2a2303b8f24edefc6c517cad7294edbe9 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 19 Jan 2019 15:28:42 +0100 Subject: [PATCH] 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. --- .../contrib/gis/db/backends/base/features.py | 2 - .../contrib/gis/db/backends/mysql/features.py | 1 - .../gis/db/backends/mysql/operations.py | 22 +++--- docs/ref/contrib/gis/db-api.txt | 20 ++---- docs/ref/contrib/gis/geoquerysets.txt | 67 ++++++++++++++++--- docs/ref/contrib/gis/install/index.txt | 2 +- docs/releases/3.0.txt | 3 +- tests/gis_tests/geoapp/tests.py | 23 +++---- 8 files changed, 85 insertions(+), 55 deletions(-) diff --git a/django/contrib/gis/db/backends/base/features.py b/django/contrib/gis/db/backends/base/features.py index 66c024e606..07f56eb0dc 100644 --- a/django/contrib/gis/db/backends/base/features.py +++ b/django/contrib/gis/db/backends/base/features.py @@ -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? diff --git a/django/contrib/gis/db/backends/mysql/features.py b/django/contrib/gis/db/backends/mysql/features.py index b69fd374fe..424df28d94 100644 --- a/django/contrib/gis/db/backends/mysql/features.py +++ b/django/contrib/gis/db/backends/mysql/features.py @@ -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 diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index d55e9f4937..1ac8055071 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -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 = ( diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 9b88ff7861..1520f06d37 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -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 -`_: +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 ` -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:: diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 154d8736f8..e8a488a323 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -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 `__, -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 `__, 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`` diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index 7adf85c8ce..924dec35f0 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -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 GEOS, GDAL 5.6.1+ :ref:`Limited functionality `. Oracle GEOS, GDAL 12.2+ XE not supported. SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.8.3+ Requires SpatiaLite 4.3+ ================== ============================== ================== ========================================= diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 2e521f76c0..b2e6b479e7 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py index eaf7e61321..47d16434a5 100644 --- a/tests/gis_tests/geoapp/tests.py +++ b/tests/gis_tests/geoapp/tests.py @@ -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')