diff --git a/django/contrib/gis/db/backends/base/features.py b/django/contrib/gis/db/backends/base/features.py index 7498fb2514..91d0020ce8 100644 --- a/django/contrib/gis/db/backends/base/features.py +++ b/django/contrib/gis/db/backends/base/features.py @@ -37,7 +37,6 @@ class BaseSpatialFeatures: # The following properties indicate if the database backend support # certain lookups (dwithin, left and right, relate, ...) - supports_distances_lookups = True supports_left_right_lookups = False # Does the database have raster support? @@ -58,6 +57,10 @@ class BaseSpatialFeatures: def supports_crosses_lookup(self): return 'crosses' in self.connection.ops.gis_operators + @property + def supports_distances_lookups(self): + return self.has_Distance_function + @property def supports_dwithin_lookup(self): return 'dwithin' in self.connection.ops.gis_operators diff --git a/django/contrib/gis/db/backends/mysql/features.py b/django/contrib/gis/db/backends/mysql/features.py index 9d4f8982d5..7eb375d571 100644 --- a/django/contrib/gis/db/backends/mysql/features.py +++ b/django/contrib/gis/db/backends/mysql/features.py @@ -10,7 +10,6 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): supports_distance_geodetic = False supports_length_geodetic = False supports_area_geodetic = False - supports_distances_lookups = False supports_transform = False supports_real_shape_operations = False supports_null_geometries = False diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 786e8f887f..1d1da1887d 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -4,6 +4,7 @@ from django.contrib.gis.db.backends.base.operations import ( ) from django.contrib.gis.db.backends.utils import SpatialOperator from django.contrib.gis.db.models import GeometryField, aggregates +from django.contrib.gis.measure import Distance from django.db.backends.mysql.operations import DatabaseOperations from django.utils.functional import cached_property @@ -87,6 +88,19 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): def geo_db_type(self, f): return f.geom_type + def get_distance(self, f, value, lookup_type): + value = value[0] + if isinstance(value, Distance): + if f.geodetic(self.connection): + raise ValueError( + 'Only numeric values of degree units are allowed on ' + 'geodetic distance queries.' + ) + dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) + else: + dist_param = value + return [dist_param] + def get_db_converters(self, expression): converters = super().get_db_converters(expression) if isinstance(expression.output_field, GeometryField) and self.uses_invalid_empty_geometry_collection: diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 3db95162f1..eea02f1315 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -606,7 +606,7 @@ PostGIS equivalent:: Distance Lookups ================ -*Availability*: PostGIS, Oracle, SpatiaLite, PGRaster (Native) +*Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Native) For an overview on performing distance queries, please refer to the :ref:`distance queries introduction `. @@ -639,6 +639,10 @@ spheroid based lookups. Support for the ``'spheroid'`` option on SQLite was added. +.. versionadded:: 2.0 + + MySQL support was added. + .. fieldlookup:: distance_gt ``distance_gt`` diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 162ac42306..0757d834f9 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -65,8 +65,8 @@ Minor features * 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. + :class:`~django.contrib.gis.db.models.functions.IsValid` function, + :lookup:`isvalid` lookup, and :ref:`distance lookups `. * Added the :class:`~django.contrib.gis.db.models.functions.Azimuth` and :class:`~django.contrib.gis.db.models.functions.LineLocatePoint` functions, diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py index c4003e14f3..ec6c40eac3 100644 --- a/tests/gis_tests/distapp/tests.py +++ b/tests/gis_tests/distapp/tests.py @@ -1,5 +1,7 @@ +import unittest + from django.contrib.gis.db.models.functions import ( - Area, Distance, Intersection, Length, Perimeter, Transform, + Area, Distance, Length, Perimeter, Transform, Union, ) from django.contrib.gis.geos import GEOSGeometry, LineString, Point from django.contrib.gis.measure import D # alias for Distance @@ -7,7 +9,7 @@ from django.db import connection from django.db.models import F, Q from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature -from ..utils import no_oracle, oracle, postgis, spatialite +from ..utils import mysql, no_oracle, oracle, postgis, spatialite from .models import ( AustraliaCity, CensusZipcode, Interstate, SouthTexasCity, SouthTexasCityFt, SouthTexasInterstate, SouthTexasZipcode, @@ -107,8 +109,9 @@ class DistanceTest(TestCase): # (thus, Houston and Southside place will be excluded as tested in # the `test02_dwithin` above). for model in [SouthTexasCity, SouthTexasCityFt]: - qs = model.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter( - point__distance_lte=(self.stx_pnt, D(km=20)), + stx_pnt = self.stx_pnt.transform(model._meta.get_field('point').srid, clone=True) + qs = model.objects.filter(point__distance_gte=(stx_pnt, D(km=7))).filter( + point__distance_lte=(stx_pnt, D(km=20)), ) cities = self.get_names(qs) self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place']) @@ -183,8 +186,9 @@ class DistanceTest(TestCase): @skipUnlessDBFeature("supports_distances_lookups") def test_distance_lookups_with_expression_rhs(self): + stx_pnt = self.stx_pnt.transform(SouthTexasCity._meta.get_field('point').srid, clone=True) qs = SouthTexasCity.objects.filter( - point__distance_lte=(self.stx_pnt, F('radius')), + point__distance_lte=(stx_pnt, F('radius')), ).order_by('name') self.assertEqual( self.get_names(qs), @@ -193,7 +197,7 @@ class DistanceTest(TestCase): # With a combined expression qs = SouthTexasCity.objects.filter( - point__distance_lte=(self.stx_pnt, F('radius') * 2), + point__distance_lte=(stx_pnt, F('radius') * 2), ).order_by('name') self.assertEqual(len(qs), 5) self.assertIn('Pearland', self.get_names(qs)) @@ -207,12 +211,18 @@ class DistanceTest(TestCase): self.assertEqual(self.get_names(qs), ['Canberra', 'Hobart', 'Melbourne']) # With a complex geometry expression - self.assertFalse(SouthTexasCity.objects.filter(point__distance_gt=(Intersection('point', 'point'), 0))) + self.assertFalse(SouthTexasCity.objects.filter(point__distance_gt=(Union('point', 'point'), 0))) self.assertEqual( - SouthTexasCity.objects.filter(point__distance_lte=(Intersection('point', 'point'), 0)).count(), + SouthTexasCity.objects.filter(point__distance_lte=(Union('point', 'point'), 0)).count(), SouthTexasCity.objects.count(), ) + @unittest.skipUnless(mysql, 'This is a MySQL-specific test') + def test_mysql_geodetic_distance_error(self): + msg = 'Only numeric values of degree units are allowed on geodetic distance queries.' + with self.assertRaisesMessage(ValueError, msg): + AustraliaCity.objects.filter(point__distance_lte=(Point(0, 0), D(m=100))).exists() + ''' =============================