Kaydet (Commit) f3bada98 authored tarafından Sergey Fedoseev's avatar Sergey Fedoseev Kaydeden (comit) Tim Graham

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

üst 38af496b
...@@ -37,7 +37,6 @@ class BaseSpatialFeatures: ...@@ -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: ...@@ -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
......
...@@ -10,7 +10,6 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): ...@@ -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
......
...@@ -4,6 +4,7 @@ from django.contrib.gis.db.backends.base.operations import ( ...@@ -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): ...@@ -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:
......
...@@ -606,7 +606,7 @@ PostGIS equivalent:: ...@@ -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. ...@@ -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``
......
...@@ -65,8 +65,8 @@ Minor features ...@@ -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,
......
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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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()
''' '''
============================= =============================
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment