mirror of https://github.com/django/django.git
[4.2.x] Fixed #33638 -- Fixed GIS lookups crash with geography fields on PostGIS.
Backport of 4403432b75
from main
This commit is contained in:
parent
600b88db4c
commit
714d59d57f
|
@ -27,7 +27,8 @@ BILATERAL = "bilateral"
|
|||
class PostGISOperator(SpatialOperator):
|
||||
def __init__(self, geography=False, raster=False, **kwargs):
|
||||
# Only a subset of the operators and functions are available for the
|
||||
# geography type.
|
||||
# geography type. Lookups that don't support geography will be cast to
|
||||
# geometry.
|
||||
self.geography = geography
|
||||
# Only a subset of the operators and functions are available for the
|
||||
# raster type. Lookups that don't support raster will be converted to
|
||||
|
@ -37,13 +38,8 @@ class PostGISOperator(SpatialOperator):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def as_sql(self, connection, lookup, template_params, *args):
|
||||
if lookup.lhs.output_field.geography and not self.geography:
|
||||
raise ValueError(
|
||||
'PostGIS geography does not support the "%s" '
|
||||
"function/operator." % (self.func or self.op,)
|
||||
)
|
||||
|
||||
template_params = self.check_raster(lookup, template_params)
|
||||
template_params = self.check_geography(lookup, template_params)
|
||||
return super().as_sql(connection, lookup, template_params, *args)
|
||||
|
||||
def check_raster(self, lookup, template_params):
|
||||
|
@ -93,6 +89,12 @@ class PostGISOperator(SpatialOperator):
|
|||
|
||||
return template_params
|
||||
|
||||
def check_geography(self, lookup, template_params):
|
||||
"""Convert geography fields to geometry types, if necessary."""
|
||||
if lookup.lhs.output_field.geography and not self.geography:
|
||||
template_params["lhs"] += "::geometry"
|
||||
return template_params
|
||||
|
||||
|
||||
class ST_Polygon(Func):
|
||||
function = "ST_Polygon"
|
||||
|
|
|
@ -18,6 +18,16 @@ class City(NamedModel):
|
|||
app_label = "geogapp"
|
||||
|
||||
|
||||
class CityUnique(NamedModel):
|
||||
point = models.PointField(geography=True, unique=True)
|
||||
|
||||
class Meta:
|
||||
required_db_features = {
|
||||
"supports_geography",
|
||||
"supports_geometry_field_unique_index",
|
||||
}
|
||||
|
||||
|
||||
class Zipcode(NamedModel):
|
||||
code = models.CharField(max_length=10)
|
||||
poly = models.PolygonField(geography=True)
|
||||
|
|
|
@ -6,12 +6,14 @@ import os
|
|||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.models.functions import Area, Distance
|
||||
from django.contrib.gis.measure import D
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import NotSupportedError, connection
|
||||
from django.db.models.functions import Cast
|
||||
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
from ..utils import FuncTestMixin
|
||||
from .models import City, County, Zipcode
|
||||
from .models import City, CityUnique, County, Zipcode
|
||||
|
||||
|
||||
class GeographyTest(TestCase):
|
||||
|
@ -38,28 +40,46 @@ class GeographyTest(TestCase):
|
|||
for cities in [cities1, cities2]:
|
||||
self.assertEqual(["Dallas", "Houston", "Oklahoma City"], cities)
|
||||
|
||||
def test04_invalid_operators_functions(self):
|
||||
@skipUnlessDBFeature("supports_geography", "supports_geometry_field_unique_index")
|
||||
def test_geography_unique(self):
|
||||
"""
|
||||
Exceptions are raised for operators & functions invalid on geography
|
||||
fields.
|
||||
Cast geography fields to geometry type when validating uniqueness to
|
||||
remove the reliance on unavailable ~= operator.
|
||||
"""
|
||||
if not connection.ops.postgis:
|
||||
self.skipTest("This is a PostGIS-specific test.")
|
||||
# Only a subset of the geometry functions & operator are available
|
||||
# to PostGIS geography types. For more information, visit:
|
||||
# http://postgis.refractions.net/documentation/manual-1.5/ch08.html#PostGIS_GeographyFunctions
|
||||
z = Zipcode.objects.get(code="77002")
|
||||
# ST_Within not available.
|
||||
with self.assertRaises(ValueError):
|
||||
City.objects.filter(point__within=z.poly).count()
|
||||
# `@` operator not available.
|
||||
with self.assertRaises(ValueError):
|
||||
City.objects.filter(point__contained=z.poly).count()
|
||||
|
||||
# Regression test for #14060, `~=` was never really implemented for PostGIS.
|
||||
htown = City.objects.get(name="Houston")
|
||||
with self.assertRaises(ValueError):
|
||||
City.objects.get(point__exact=htown.point)
|
||||
CityUnique.objects.create(point=htown.point)
|
||||
duplicate = CityUnique(point=htown.point)
|
||||
msg = "City unique with this Point already exists."
|
||||
with self.assertRaisesMessage(ValidationError, msg):
|
||||
duplicate.validate_unique()
|
||||
|
||||
@skipUnlessDBFeature("supports_geography")
|
||||
def test_operators_functions_unavailable_for_geography(self):
|
||||
"""
|
||||
Geography fields are cast to geometry if the relevant operators or
|
||||
functions are not available.
|
||||
"""
|
||||
z = Zipcode.objects.get(code="77002")
|
||||
point_field = "%s.%s::geometry" % (
|
||||
connection.ops.quote_name(City._meta.db_table),
|
||||
connection.ops.quote_name("point"),
|
||||
)
|
||||
# ST_Within.
|
||||
qs = City.objects.filter(point__within=z.poly)
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.assertIn(f"ST_Within({point_field}", ctx.captured_queries[0]["sql"])
|
||||
# @ operator.
|
||||
qs = City.objects.filter(point__contained=z.poly)
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.assertIn(f"{point_field} @", ctx.captured_queries[0]["sql"])
|
||||
# ~= operator.
|
||||
htown = City.objects.get(name="Houston")
|
||||
qs = City.objects.filter(point__exact=htown.point)
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.assertIn(f"{point_field} ~=", ctx.captured_queries[0]["sql"])
|
||||
|
||||
def test05_geography_layermapping(self):
|
||||
"Testing LayerMapping support on models with geography fields."
|
||||
|
|
Loading…
Reference in New Issue