Fixed #28436 -- Added support for distance lookups on MySQL.

This commit is contained in:
Sergey Fedoseev 2017-07-26 12:40:19 +05:00 committed by Tim Graham
parent 38af496b98
commit f3bada9889
6 changed files with 43 additions and 13 deletions

View File

@ -37,7 +37,6 @@ class BaseSpatialFeatures:
# The following properties indicate if the database backend support # The following properties indicate if the database backend support
# certain lookups (dwithin, left and right, relate, ...) # certain lookups (dwithin, left and right, relate, ...)
supports_distances_lookups = True
supports_left_right_lookups = False supports_left_right_lookups = False
# Does the database have raster support? # Does the database have raster support?
@ -58,6 +57,10 @@ class BaseSpatialFeatures:
def supports_crosses_lookup(self): def supports_crosses_lookup(self):
return 'crosses' in self.connection.ops.gis_operators return 'crosses' in self.connection.ops.gis_operators
@property
def supports_distances_lookups(self):
return self.has_Distance_function
@property @property
def supports_dwithin_lookup(self): def supports_dwithin_lookup(self):
return 'dwithin' in self.connection.ops.gis_operators return 'dwithin' in self.connection.ops.gis_operators

View File

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

View File

@ -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.backends.utils import SpatialOperator
from django.contrib.gis.db.models import GeometryField, aggregates 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.db.backends.mysql.operations import DatabaseOperations
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -87,6 +88,19 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
def geo_db_type(self, f): def geo_db_type(self, f):
return f.geom_type 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): def get_db_converters(self, expression):
converters = super().get_db_converters(expression) converters = super().get_db_converters(expression)
if isinstance(expression.output_field, GeometryField) and self.uses_invalid_empty_geometry_collection: if isinstance(expression.output_field, GeometryField) and self.uses_invalid_empty_geometry_collection:

View File

@ -606,7 +606,7 @@ PostGIS equivalent::
Distance Lookups 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 For an overview on performing distance queries, please refer to
the :ref:`distance queries introduction <distance-queries>`. the :ref:`distance queries introduction <distance-queries>`.
@ -639,6 +639,10 @@ spheroid based lookups.
Support for the ``'spheroid'`` option on SQLite was added. Support for the ``'spheroid'`` option on SQLite was added.
.. versionadded:: 2.0
MySQL support was added.
.. fieldlookup:: distance_gt .. fieldlookup:: distance_gt
``distance_gt`` ``distance_gt``

View File

@ -65,8 +65,8 @@ Minor features
* Added MySQL support for the * Added MySQL support for the
:class:`~django.contrib.gis.db.models.functions.AsGeoJSON` function, :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.GeoHash` function,
:class:`~django.contrib.gis.db.models.functions.IsValid` function, and :class:`~django.contrib.gis.db.models.functions.IsValid` function,
:lookup:`isvalid` lookup. :lookup:`isvalid` lookup, and :ref:`distance lookups <distance-lookups>`.
* Added the :class:`~django.contrib.gis.db.models.functions.Azimuth` and * Added the :class:`~django.contrib.gis.db.models.functions.Azimuth` and
:class:`~django.contrib.gis.db.models.functions.LineLocatePoint` functions, :class:`~django.contrib.gis.db.models.functions.LineLocatePoint` functions,

View File

@ -1,5 +1,7 @@
import unittest
from django.contrib.gis.db.models.functions import ( 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.geos import GEOSGeometry, LineString, Point
from django.contrib.gis.measure import D # alias for Distance 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.db.models import F, Q
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature 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 ( from .models import (
AustraliaCity, CensusZipcode, Interstate, SouthTexasCity, SouthTexasCityFt, AustraliaCity, CensusZipcode, Interstate, SouthTexasCity, SouthTexasCityFt,
SouthTexasInterstate, SouthTexasZipcode, SouthTexasInterstate, SouthTexasZipcode,
@ -107,8 +109,9 @@ class DistanceTest(TestCase):
# (thus, Houston and Southside place will be excluded as tested in # (thus, Houston and Southside place will be excluded as tested in
# the `test02_dwithin` above). # the `test02_dwithin` above).
for model in [SouthTexasCity, SouthTexasCityFt]: for model in [SouthTexasCity, SouthTexasCityFt]:
qs = model.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter( stx_pnt = self.stx_pnt.transform(model._meta.get_field('point').srid, clone=True)
point__distance_lte=(self.stx_pnt, D(km=20)), 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) cities = self.get_names(qs)
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place']) self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
@ -183,8 +186,9 @@ class DistanceTest(TestCase):
@skipUnlessDBFeature("supports_distances_lookups") @skipUnlessDBFeature("supports_distances_lookups")
def test_distance_lookups_with_expression_rhs(self): 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( qs = SouthTexasCity.objects.filter(
point__distance_lte=(self.stx_pnt, F('radius')), point__distance_lte=(stx_pnt, F('radius')),
).order_by('name') ).order_by('name')
self.assertEqual( self.assertEqual(
self.get_names(qs), self.get_names(qs),
@ -193,7 +197,7 @@ class DistanceTest(TestCase):
# With a combined expression # With a combined expression
qs = SouthTexasCity.objects.filter( qs = SouthTexasCity.objects.filter(
point__distance_lte=(self.stx_pnt, F('radius') * 2), point__distance_lte=(stx_pnt, F('radius') * 2),
).order_by('name') ).order_by('name')
self.assertEqual(len(qs), 5) self.assertEqual(len(qs), 5)
self.assertIn('Pearland', self.get_names(qs)) self.assertIn('Pearland', self.get_names(qs))
@ -207,12 +211,18 @@ class DistanceTest(TestCase):
self.assertEqual(self.get_names(qs), ['Canberra', 'Hobart', 'Melbourne']) self.assertEqual(self.get_names(qs), ['Canberra', 'Hobart', 'Melbourne'])
# With a complex geometry expression # 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( 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(), 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()
''' '''
============================= =============================