From 0a13b249e2e0e901d795f4198a3a5dfd1fc49554 Mon Sep 17 00:00:00 2001 From: Sergey Fedoseev Date: Sat, 1 Apr 2017 22:43:53 +0500 Subject: [PATCH] Fixed #26967 -- Added MySQL support for AsGeoJSON, GeoHash, IsValid functions, and isvalid lookup. --- .../gis/db/backends/mysql/operations.py | 9 ++++--- django/contrib/gis/db/models/functions.py | 7 +++++ docs/ref/contrib/gis/db-api.txt | 8 +++--- docs/ref/contrib/gis/functions.txt | 26 ++++++++++++++----- docs/ref/contrib/gis/geoquerysets.txt | 20 ++++++++------ docs/releases/2.0.txt | 6 ++++- tests/gis_tests/geoapp/test_functions.py | 15 ++++++++--- tests/gis_tests/geoapp/tests.py | 7 +++-- 8 files changed, 68 insertions(+), 30 deletions(-) diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 6408d76fa4..58aeda4deb 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/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 diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index dcd09472e3..c595a143d0 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/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 diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 09a49c8418..d0555ec145 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/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 diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index fc22d43c30..b54feb03a1 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -56,8 +56,8 @@ geographic SRSes. .. class:: AsGeoJSON(expression, bbox=False, crs=False, precision=8, **extra) -*Availability*: `PostGIS `__, -SpatiaLite +*Availability*: MySQL (≥ 5.7.5), `PostGIS +`__, SpatiaLite Accepts a single geographic field or expression and returns a `GeoJSON `_ 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 `__, -SpatiaLite (LWGEOM) +*Availability*: MySQL (≥ 5.7.5), `PostGIS +`__, 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 `__, -Oracle, SpatiaLite (LWGEOM) +*Availability*: MySQL (≥ 5.7.5), `PostGIS +`__, 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`` ========== diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index d7135a7c39..d106191485 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -306,8 +306,8 @@ SpatiaLite ``Intersects(poly, geom)`` ``isvalid`` ----------- -*Availability*: `PostGIS `__, -Oracle, SpatiaLite +*Availability*: MySQL (≥ 5.7.5), `PostGIS +`__, 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`` diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index ee2117546b..0006ef4c45 100644 --- a/docs/releases/2.0.txt +++ b/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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 34deb88f4a..818ec56e93 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/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") diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py index e0ee1ac238..068b9f3de2 100644 --- a/tests/gis_tests/geoapp/tests.py +++ b/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)