diff --git a/django/contrib/gis/db/backends/base/features.py b/django/contrib/gis/db/backends/base/features.py index 07f56eb0dc..5c25ca5ac0 100644 --- a/django/contrib/gis/db/backends/base/features.py +++ b/django/contrib/gis/db/backends/base/features.py @@ -36,6 +36,9 @@ class BaseSpatialFeatures: # The following properties indicate if the database backend support # certain lookups (dwithin, left and right, relate, ...) supports_left_right_lookups = False + # Does the backend support expressions for specifying distance in the + # dwithin lookup? + supports_dwithin_distance_expr = True # Does the database have raster support? supports_raster = False diff --git a/django/contrib/gis/db/backends/oracle/features.py b/django/contrib/gis/db/backends/oracle/features.py index ece45b2623..f2ed4d3076 100644 --- a/django/contrib/gis/db/backends/oracle/features.py +++ b/django/contrib/gis/db/backends/oracle/features.py @@ -9,3 +9,4 @@ class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures): supports_geometry_field_introspection = False supports_geometry_field_unique_index = False supports_perimeter_geodetic = True + supports_dwithin_distance_expr = False diff --git a/django/contrib/gis/db/models/lookups.py b/django/contrib/gis/db/models/lookups.py index 6d5df2c10f..f0f5e14b43 100644 --- a/django/contrib/gis/db/models/lookups.py +++ b/django/contrib/gis/db/models/lookups.py @@ -1,6 +1,8 @@ import re from django.contrib.gis.db.models.fields import BaseSpatialField +from django.contrib.gis.measure import Distance +from django.db import NotSupportedError from django.db.models.expressions import Expression from django.db.models.lookups import Lookup, Transform from django.db.models.sql.query import Query @@ -301,7 +303,20 @@ class DistanceLookupBase(GISLookup): @BaseSpatialField.register_lookup class DWithinLookup(DistanceLookupBase): lookup_name = 'dwithin' - sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s)' + sql_template = '%(func)s(%(lhs)s, %(rhs)s, %(value)s)' + + def process_distance(self, compiler, connection): + dist_param = self.rhs_params[0] + if ( + not connection.features.supports_dwithin_distance_expr and + hasattr(dist_param, 'resolve_expression') and + not isinstance(dist_param, Distance) + ): + raise NotSupportedError( + 'This backend does not support expressions for specifying ' + 'distance in the dwithin lookup.' + ) + return super().process_distance(compiler, connection) def process_rhs(self, compiler, connection): dist_sql, dist_params = self.process_distance(compiler, connection) diff --git a/tests/gis_tests/distapp/models.py b/tests/gis_tests/distapp/models.py index fcad6fc097..1971741c54 100644 --- a/tests/gis_tests/distapp/models.py +++ b/tests/gis_tests/distapp/models.py @@ -28,6 +28,7 @@ class AustraliaCity(NamedModel): "City model for Australia, using WGS84." point = models.PointField() radius = models.IntegerField(default=10000) + allowed_distance = models.FloatField(default=0.5) class CensusZipcode(NamedModel): diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py index d84e829868..2cdd0e8f0e 100644 --- a/tests/gis_tests/distapp/tests.py +++ b/tests/gis_tests/distapp/tests.py @@ -234,6 +234,30 @@ class DistanceTest(TestCase): ).filter(annotated_value=True) self.assertEqual(self.get_names(qs), ['77002', '77025', '77401']) + @skipUnlessDBFeature('supports_dwithin_lookup', 'supports_dwithin_distance_expr') + def test_dwithin_with_expression_rhs(self): + # LineString of Wollongong and Adelaide coords. + ls = LineString(((150.902, -34.4245), (138.6, -34.9258)), srid=4326) + qs = AustraliaCity.objects.filter( + point__dwithin=(ls, F('allowed_distance')), + ).order_by('name') + self.assertEqual( + self.get_names(qs), + ['Adelaide', 'Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong'], + ) + + @skipIfDBFeature('supports_dwithin_distance_expr') + def test_dwithin_with_expression_rhs_not_supported(self): + ls = LineString(((150.902, -34.4245), (138.6, -34.9258)), srid=4326) + msg = ( + 'This backend does not support expressions for specifying ' + 'distance in the dwithin lookup.' + ) + with self.assertRaisesMessage(NotSupportedError, msg): + list(AustraliaCity.objects.filter( + point__dwithin=(ls, F('allowed_distance')), + )) + ''' =============================