Browse Source

Fixed #26967 -- Added MySQL support for AsGeoJSON, GeoHash, IsValid functions, and isvalid lookup.

Sergey Fedoseev 8 years ago
parent
commit
0a13b249e2

+ 5 - 4
django/contrib/gis/db/backends/mysql/operations.py

@@ -72,11 +72,12 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
     @cached_property
     def unsupported_functions(self):
         unsupported = {
-            'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle',
-            'ForceRHR', 'GeoHash', 'IsValid', 'MakeValid', 'MemSize',
-            'Perimeter', 'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid',
-            'Transform', 'Translate',
+            'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle', 'ForceRHR',
+            'MakeValid', 'MemSize', 'Perimeter', 'PointOnSurface', 'Reverse',
+            'Scale', 'SnapToGrid', 'Transform', 'Translate',
         }
+        if self.connection.mysql_version < (5, 7, 5):
+            unsupported.update({'AsGeoJSON', 'GeoHash', 'IsValid'})
         if self.is_mysql_5_5:
             unsupported.update({'Difference', 'Distance', 'Intersection', 'SymDifference', 'Union'})
         return unsupported

+ 7 - 0
django/contrib/gis/db/models/functions.py

@@ -330,6 +330,13 @@ class GeoHash(GeoFunc):
             expressions.append(self._handle_param(precision, 'precision', int))
         super().__init__(*expressions, **extra)
 
+    def as_mysql(self, compiler, connection):
+        clone = self.copy()
+        # If no precision is provided, set it to the maximum.
+        if len(clone.source_expressions) < 2:
+            clone.source_expressions.append(Value(100))
+        return clone.as_sql(compiler, connection)
+
 
 class Intersection(OracleToleranceMixin, GeomOutputGeoFunc):
     arity = 2

+ 4 - 4
docs/ref/contrib/gis/db-api.txt

@@ -341,7 +341,7 @@ Lookup Type                        PostGIS    Oracle    MySQL [#]_   SpatiaLite
 :lookup:`equals`                   X          X         X            X          C
 :lookup:`exact`                    X          X         X            X          B
 :lookup:`intersects`               X          X         X            X          B
-:lookup:`isvalid`                  X          X                      X (LWGEOM)
+:lookup:`isvalid`                  X          X         X (≥ 5.7.5)  X (LWGEOM)
 :lookup:`overlaps`                 X          X         X            X          B
 :lookup:`relate`                   X          X                      X          C
 :lookup:`same_as`                  X          X         X            X          B
@@ -372,7 +372,7 @@ functions are available on each spatial backend.
 Function                              PostGIS  Oracle          MySQL        SpatiaLite
 ====================================  =======  ==============  ===========  ==========
 :class:`Area`                         X        X               X            X
-:class:`AsGeoJSON`                    X                                     X
+:class:`AsGeoJSON`                    X                        X (≥ 5.7.5)  X
 :class:`AsGML`                        X        X                            X
 :class:`AsKML`                        X                                     X
 :class:`AsSVG`                        X                                     X
@@ -382,9 +382,9 @@ Function                              PostGIS  Oracle          MySQL        Spat
 :class:`Distance`                     X        X               X (≥ 5.6.1)  X
 :class:`Envelope`                     X                        X            X
 :class:`ForceRHR`                     X
-:class:`GeoHash`                      X                                     X (LWGEOM)
+:class:`GeoHash`                      X                        X (≥ 5.7.5)  X (LWGEOM)
 :class:`Intersection`                 X        X               X (≥ 5.6.1)  X
-:class:`IsValid`                      X        X                            X (LWGEOM)
+:class:`IsValid`                      X        X               X (≥ 5.7.5)  X (LWGEOM)
 :class:`Length`                       X        X               X            X
 :class:`MakeValid`                    X                                     X (LWGEOM)
 :class:`MemSize`                      X

+ 19 - 7
docs/ref/contrib/gis/functions.txt

@@ -56,8 +56,8 @@ geographic SRSes.
 
 .. class:: AsGeoJSON(expression, bbox=False, crs=False, precision=8, **extra)
 
-*Availability*: `PostGIS <https://postgis.net/docs/ST_AsGeoJSON.html>`__,
-SpatiaLite
+*Availability*: MySQL (≥ 5.7.5), `PostGIS
+<https://postgis.net/docs/ST_AsGeoJSON.html>`__, SpatiaLite
 
 Accepts a single geographic field or expression and returns a `GeoJSON
 <http://geojson.org/>`_ representation of the geometry. Note that the result is
@@ -77,13 +77,17 @@ Keyword Argument       Description
 
 ``crs``                Set this to ``True`` if you want the coordinate
                        reference system to be included in the returned
-                       GeoJSON.
+                       GeoJSON. Ignored on MySQL.
 
 ``precision``          It may be used to specify the number of significant
                        digits for the coordinates in the GeoJSON
                        representation -- the default value is 8.
 =====================  =====================================================
 
+.. versionchanged:: 2.0
+
+    MySQL support was added.
+
 ``AsGML``
 =========
 
@@ -286,8 +290,8 @@ right-hand rule.
 
 .. class:: GeoHash(expression, precision=None, **extra)
 
-*Availability*: `PostGIS <https://postgis.net/docs/ST_GeoHash.html>`__,
-SpatiaLite (LWGEOM)
+*Availability*: MySQL (≥ 5.7.5), `PostGIS
+<https://postgis.net/docs/ST_GeoHash.html>`__, SpatiaLite (LWGEOM)
 
 Accepts a single geographic field or expression and returns a `GeoHash`__
 representation of the geometry.
@@ -295,6 +299,10 @@ representation of the geometry.
 The ``precision`` keyword argument controls the number of characters in the
 result.
 
+.. versionchanged:: 2.0
+
+    MySQL support was added.
+
 __ https://en.wikipedia.org/wiki/Geohash
 
 ``Intersection``
@@ -313,8 +321,8 @@ intersection between them.
 
 .. class:: IsValid(expr)
 
-*Availability*: `PostGIS <https://postgis.net/docs/ST_IsValid.html>`__,
-Oracle, SpatiaLite (LWGEOM)
+*Availability*: MySQL (≥ 5.7.5), `PostGIS
+<https://postgis.net/docs/ST_IsValid.html>`__, Oracle, SpatiaLite (LWGEOM)
 
 Accepts a geographic field or expression and tests if the value is well formed.
 Returns ``True`` if its value is a valid geometry and ``False`` otherwise.
@@ -323,6 +331,10 @@ Returns ``True`` if its value is a valid geometry and ``False`` otherwise.
 
     SpatiaLite and Oracle support was added.
 
+.. versionchanged:: 2.0
+
+    MySQL support was added.
+
 ``Length``
 ==========
 

+ 12 - 8
docs/ref/contrib/gis/geoquerysets.txt

@@ -306,8 +306,8 @@ SpatiaLite  ``Intersects(poly, geom)``
 ``isvalid``
 -----------
 
-*Availability*: `PostGIS <https://postgis.net/docs/ST_IsValid.html>`__,
-Oracle, SpatiaLite
+*Availability*: MySQL (≥ 5.7.5), `PostGIS
+<https://postgis.net/docs/ST_IsValid.html>`__, Oracle, SpatiaLite
 
 Tests if the geometry is valid.
 
@@ -315,17 +315,21 @@ Example::
 
     Zipcode.objects.filter(poly__isvalid=True)
 
-===================  ================================================================
-Backend              SQL Equivalent
-===================  ================================================================
-PostGIS, SpatiaLite  ``ST_IsValid(poly)``
-Oracle               ``SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(poly, 0.05) = 'TRUE'``
-===================  ================================================================
+==========================  ================================================================
+Backend                     SQL Equivalent
+==========================  ================================================================
+MySQL, PostGIS, SpatiaLite  ``ST_IsValid(poly)``
+Oracle                      ``SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(poly, 0.05) = 'TRUE'``
+==========================  ================================================================
 
 .. versionchanged:: 1.11
 
     Oracle and SpatiaLite support was added.
 
+.. versionchanged:: 2.0
+
+    MySQL support was added.
+
 .. fieldlookup:: overlaps
 
 ``overlaps``

+ 5 - 1
docs/releases/2.0.txt

@@ -62,7 +62,11 @@ Minor features
 :mod:`django.contrib.gis`
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* Added MySQL support for the
+  :class:`~django.contrib.gis.db.models.functions.AsGeoJSON` function,
+  :class:`~django.contrib.gis.db.models.functions.GeoHash` function,
+  :class:`~django.contrib.gis.db.models.functions.IsValid` function, and
+  :lookup:`isvalid` lookup.
 
 :mod:`django.contrib.messages`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 11 - 4
tests/gis_tests/geoapp/test_functions.py

@@ -1,3 +1,4 @@
+import json
 import re
 from decimal import Decimal
 
@@ -44,6 +45,12 @@ class GISFunctionsTests(TestCase):
             '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},'
             '"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}'
         )
+        # MySQL ignores the crs option.
+        if mysql:
+            houston_json = json.loads(houston_json)
+            del houston_json['crs']
+            chicago_json = json.loads(chicago_json)
+            del chicago_json['crs']
 
         # Precision argument should only be an integer
         with self.assertRaises(TypeError):
@@ -61,8 +68,8 @@ class GISFunctionsTests(TestCase):
         # WHERE "geoapp_city"."name" = 'Houston';
         # This time we want to include the CRS by using the `crs` keyword.
         self.assertJSONEqual(
+            City.objects.annotate(json=functions.AsGeoJSON('point', crs=True)).get(name='Houston').json,
             houston_json,
-            City.objects.annotate(json=functions.AsGeoJSON('point', crs=True)).get(name='Houston').json
         )
 
         # SELECT ST_AsGeoJson("geoapp_city"."point", 8, 1) FROM "geoapp_city"
@@ -79,10 +86,10 @@ class GISFunctionsTests(TestCase):
         # WHERE "geoapp_city"."name" = 'Chicago';
         # Finally, we set every available keyword.
         self.assertJSONEqual(
-            chicago_json,
             City.objects.annotate(
                 geojson=functions.AsGeoJSON('point', bbox=True, crs=True, precision=5)
-            ).get(name='Chicago').geojson
+            ).get(name='Chicago').geojson,
+            chicago_json,
         )
 
     @skipUnlessDBFeature("has_AsGML_function")
@@ -224,7 +231,7 @@ class GISFunctionsTests(TestCase):
         ref_hash = '9vk1mfq8jx0c8e0386z6'
         h1 = City.objects.annotate(geohash=functions.GeoHash('point')).get(name='Houston')
         h2 = City.objects.annotate(geohash=functions.GeoHash('point', precision=5)).get(name='Houston')
-        self.assertEqual(ref_hash, h1.geohash)
+        self.assertEqual(ref_hash, h1.geohash[:len(ref_hash)])
         self.assertEqual(ref_hash[:5], h2.geohash)
 
     @skipUnlessDBFeature("has_Intersection_function")

+ 5 - 2
tests/gis_tests/geoapp/tests.py

@@ -11,7 +11,9 @@ from django.core.management import call_command
 from django.db import connection
 from django.test import TestCase, skipUnlessDBFeature
 
-from ..utils import no_oracle, oracle, postgis, skipUnlessGISLookup, spatialite
+from ..utils import (
+    mysql, no_oracle, oracle, postgis, skipUnlessGISLookup, spatialite,
+)
 from .models import (
     City, Country, Feature, MinusOneSRID, NonConcreteModel, PennsylvaniaCity,
     State, Track,
@@ -302,9 +304,10 @@ class GeoLookupTest(TestCase):
         invalid_geom = fromstr('POLYGON((0 0, 0 1, 1 1, 1 0, 1 1, 1 0, 0 0))')
         State.objects.create(name='invalid', poly=invalid_geom)
         qs = State.objects.all()
-        if oracle:
+        if oracle or mysql:
             # Kansas has adjacent vertices with distance 6.99244813842e-12
             # which is smaller than the default Oracle tolerance.
+            # It's invalid on MySQL too.
             qs = qs.exclude(name='Kansas')
             self.assertEqual(State.objects.filter(name='Kansas', poly__isvalid=False).count(), 1)
         self.assertEqual(qs.filter(poly__isvalid=False).count(), 1)