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
# 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

View File

@ -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

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.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:

View File

@ -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 <distance-queries>`.
@ -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``

View File

@ -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 <distance-lookups>`.
* Added the :class:`~django.contrib.gis.db.models.functions.Azimuth` and
: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 (
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()
'''
=============================