Code

Ticket #14483: spatial_subquery3.diff

File spatial_subquery3.diff, 8.5 KB (added by milosu, 3 years ago)

improved patch passing GeoDjango and Django test suite (solved SQLEvaluators related problems)

Line 
1Index: django/contrib/gis/db/models/sql/where.py
2===================================================================
3--- django/contrib/gis/db/models/sql/where.py   (revision 4752)
4+++ django/contrib/gis/db/models/sql/where.py   (working copy)
5@@ -2,6 +2,7 @@
6 from django.db.models.sql.constants import LOOKUP_SEP
7 from django.db.models.sql.expressions import SQLEvaluator
8 from django.db.models.sql.where import Constraint, WhereNode
9+from django.db.models.query_utils import QueryWrapper
10 from django.contrib.gis.db.models.fields import GeometryField
11
12 class GeoConstraint(Constraint):
13@@ -46,6 +47,14 @@
14             data, params = lvalue.process(lookup_type, params_or_value, connection)
15            alias, col, db_type = data
16             spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn)
17+
18+            if isinstance(params, QueryWrapper):
19+                tables = [alias]
20+                if hasattr(params, 'tables'):
21+                    tables.extend(params.tables)
22+                extra, params = params.as_sql(qn, connection)
23+                spatial_sql = spatial_sql % extra
24+                return spatial_sql, params, tables
25             return spatial_sql, params, [alias]
26         else:
27             return super(GeoWhereNode, self).make_atom(child, qn, connection)
28Index: django/contrib/gis/db/models/fields.py
29===================================================================
30--- django/contrib/gis/db/models/fields.py      (revision 4752)
31+++ django/contrib/gis/db/models/fields.py      (working copy)
32@@ -1,5 +1,6 @@
33 from django.db.models.fields import Field
34 from django.db.models.sql.expressions import SQLEvaluator
35+from django.db.models.query_utils import QueryWrapper
36 from django.utils.translation import ugettext_lazy as _
37 from django.contrib.gis import forms
38 from django.contrib.gis.db.models.proxy import GeometryProxy
39@@ -148,6 +149,8 @@
40         """
41         if isinstance(value, SQLEvaluator):
42             return value
43+        elif hasattr(value, 'query'):
44+            return value
45         elif isinstance(value, (tuple, list)):
46             geom = value[0]
47             seq_value = True
48@@ -236,6 +239,17 @@
49                     params += value[1:]
50             elif isinstance(value, SQLEvaluator):
51                 params = []
52+            elif hasattr(value, 'as_sql') or hasattr(value, '_as_sql'):
53+                # If the value has a relabel_aliases method, it will need to
54+                # be invoked before the final SQL is evaluated
55+                if hasattr(value, 'relabel_aliases'):
56+                    return value
57+                if hasattr(value, 'as_sql'):
58+                    sql, params = value.as_sql()
59+                else:
60+                    sql, params = value._as_sql(connection=connection)
61+                subselect_sql = connection.ops.wrap_spatial_subselect(sql)
62+                return QueryWrapper((subselect_sql), params, value.query.tables)
63             else:
64                 params = [connection.ops.Adapter(value)]
65
66Index: django/contrib/gis/db/backends/spatialite/operations.py
67===================================================================
68--- django/contrib/gis/db/backends/spatialite/operations.py     (revision 4753)
69+++ django/contrib/gis/db/backends/spatialite/operations.py     (working copy)
70@@ -188,6 +188,8 @@
71         """
72         def transform_value(value, srid):
73             return not (value is None or value.srid == srid)
74+        if hasattr(value, 'query'):
75+           return '%s'
76         if hasattr(value, 'expression'):
77             if transform_value(value, f.srid):
78                 placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
79Index: django/contrib/gis/db/backends/mysql/operations.py
80===================================================================
81--- django/contrib/gis/db/backends/mysql/operations.py  (revision 4753)
82+++ django/contrib/gis/db/backends/mysql/operations.py  (working copy)
83@@ -41,6 +41,8 @@
84         MySQL does not support spatial transformations, there is no need to
85         modify the placeholder based on the contents of the given value.
86         """
87+       if hasattr(value, 'query'):
88+           return '%s'
89         if hasattr(value, 'expression'):
90             placeholder = '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
91         else:
92Index: django/contrib/gis/db/backends/oracle/operations.py
93===================================================================
94--- django/contrib/gis/db/backends/oracle/operations.py (revision 4753)
95+++ django/contrib/gis/db/backends/oracle/operations.py (working copy)
96@@ -205,7 +205,8 @@
97         """
98         if value is None:
99             return 'NULL'
100-
101+        if hasattr(value, 'query'):
102+           return '%s'
103         def transform_value(val, srid):
104             return val.srid != srid
105 
106Index: django/contrib/gis/db/backends/postgis/operations.py
107===================================================================
108--- django/contrib/gis/db/backends/postgis/operations.py        (revision 4753)
109+++ django/contrib/gis/db/backends/postgis/operations.py        (working copy)
110@@ -387,7 +387,7 @@
111         SRID of the field.  Specifically, this routine will substitute in the
112         ST_Transform() function call.
113         """
114-        if value is None or value.srid == f.srid:
115+        if value is None or hasattr(value, 'query') or value.srid == f.srid:
116             placeholder = '%s'
117         else:
118             # Adding Transform() to the SQL placeholder.
119Index: backends/base.py
120===================================================================
121--- backends/base.py    (revision 4356)
122+++ backends/base.py    (working copy)
123@@ -131,6 +131,9 @@
124     def spatial_ref_sys(self):
125         raise NotImplementedError
126 
127+    def wrap_spatial_subselect(self, subselect_sql):
128+        return '(%s)' % subselect_sql
129+
130 class SpatialRefSysMixin(object):
131     """
132     The SpatialRefSysMixin is a class used by the database-dependent
133Index: backends/postgis/func.sql
134===================================================================
135--- backends/postgis/func.sql   (revision 0)
136+++ backends/postgis/func.sql   (revision 0)
137@@ -0,0 +1,10 @@
138+/* function that will immediately execute subselect that returns spatial geometry */
139+create or replace function execute_spatial_subselect(a_query text)
140+returns geometry as
141+$$
142+  DECLARE poly geometry;
143+  BEGIN
144+    EXECUTE a_query INTO poly;
145+    RETURN poly;
146+  END
147+$$ LANGUAGE 'plpgsql' IMMUTABLE;
148Index: backends/postgis/operations.py
149===================================================================
150--- backends/postgis/operations.py      (revision 4754)
151+++ backends/postgis/operations.py      (working copy)
152@@ -589,3 +589,6 @@
153     def spatial_ref_sys(self):
154         from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
155         return SpatialRefSys
156+
157+    def wrap_spatial_subselect(self, subselect_sql):
158+        # use dollar quoting to prevent problems with quoted params
159+        return '(execute_spatial_subselect($$%s$$))' % subselect_sql
160Index: tests/modeltests/spatial_subquery/__init__.py
161===================================================================
162Index: tests/modeltests/spatial_subquery/models.py
163===================================================================
164--- tests/modeltests/spatial_subquery/models.py (revision 0)
165+++ tests/modeltests/spatial_subquery/models.py (revision 4754)
166@@ -0,0 +1,35 @@
167+"""
168+Tests for spatial subqueries
169+"""
170+
171+from django.contrib.gis.db import models
172+
173+class Address(models.Model):
174+    city = models.CharField(max_length=50)
175+    location = models.PointField(srid=4326)
176+    objects = models.GeoManager()
177+    def __unicode__(self):
178+        return u"%s" % (self.city)
179+
180+class District(models.Model):
181+    name = models.CharField(max_length=50)
182+    area = models.MultiPolygonField(srid=4326)
183+    def __unicode__(self):
184+        return u"%s" % (self.name)
185+
186+__test__ = {'API_TESTS':"""
187+
188+>>> qset = Address.objects.filter(location__within = District.objects.filter(name='Boston area').values('area'))
189+>>> str(qset.query)
190+'SELECT "spatial_subquery_address"."id", "spatial_subquery_address"."city", "spatial_subquery_address"."location" FROM "spatial_subquery_address" WHERE ST_Within("spatial_subquery_address"."location", (execute_spatial_subselect($$SELECT U0."area" FROM "spatial_subquery_district" U0 WHERE U0."name" = Boston area $$)))'
191+
192+>>> list(qset)
193+[]
194+
195+>>> qset.query.tables  # check presence of both tables for johnny cache patch
196+['spatial_subquery_address']
197+
198+>>> qset.query.where.result_tables # also QueryWrapper tables should be present
199+['spatial_subquery_address', 'spatial_subquery_district']
200+
201+"""}