|
0
|
1 |
from itertools import izip |
|
|
2 |
from django.db.models.query import sql |
|
|
3 |
from django.db.models.fields.related import ForeignKey |
|
|
4 |
|
|
|
5 |
from django.contrib.gis.db.backend import SpatialBackend |
|
|
6 |
from django.contrib.gis.db.models.fields import GeometryField |
|
|
7 |
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates_module |
|
|
8 |
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField |
|
|
9 |
from django.contrib.gis.db.models.sql.where import GeoWhereNode |
|
|
10 |
from django.contrib.gis.measure import Area, Distance |
|
|
11 |
|
|
|
12 |
# Valid GIS query types. |
|
|
13 |
ALL_TERMS = sql.constants.QUERY_TERMS.copy() |
|
|
14 |
ALL_TERMS.update(SpatialBackend.gis_terms) |
|
|
15 |
|
|
|
16 |
# Pulling out other needed constants/routines to avoid attribute lookups. |
|
|
17 |
TABLE_NAME = sql.constants.TABLE_NAME |
|
|
18 |
get_proxied_model = sql.query.get_proxied_model |
|
|
19 |
|
|
|
20 |
class GeoQuery(sql.Query): |
|
|
21 |
""" |
|
|
22 |
A single spatial SQL query. |
|
|
23 |
""" |
|
|
24 |
# Overridding the valid query terms. |
|
|
25 |
query_terms = ALL_TERMS |
|
|
26 |
aggregates_module = gis_aggregates_module |
|
|
27 |
|
|
|
28 |
#### Methods overridden from the base Query class #### |
|
|
29 |
def __init__(self, model, conn): |
|
|
30 |
super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode) |
|
|
31 |
# The following attributes are customized for the GeoQuerySet. |
|
|
32 |
# The GeoWhereNode and SpatialBackend classes contain backend-specific |
|
|
33 |
# routines and functions. |
|
|
34 |
self.custom_select = {} |
|
|
35 |
self.transformed_srid = None |
|
|
36 |
self.extra_select_fields = {} |
|
|
37 |
|
|
|
38 |
if SpatialBackend.oracle: |
|
|
39 |
# Have to override this so that GeoQuery, instead of OracleQuery, |
|
|
40 |
# is returned when unpickling. |
|
|
41 |
def __reduce__(self): |
|
|
42 |
callable, args, data = super(GeoQuery, self).__reduce__() |
|
|
43 |
return (unpickle_geoquery, (), data) |
|
|
44 |
|
|
|
45 |
def clone(self, *args, **kwargs): |
|
|
46 |
obj = super(GeoQuery, self).clone(*args, **kwargs) |
|
|
47 |
# Customized selection dictionary and transformed srid flag have |
|
|
48 |
# to also be added to obj. |
|
|
49 |
obj.custom_select = self.custom_select.copy() |
|
|
50 |
obj.transformed_srid = self.transformed_srid |
|
|
51 |
obj.extra_select_fields = self.extra_select_fields.copy() |
|
|
52 |
return obj |
|
|
53 |
|
|
|
54 |
def get_columns(self, with_aliases=False): |
|
|
55 |
""" |
|
|
56 |
Return the list of columns to use in the select statement. If no |
|
|
57 |
columns have been specified, returns all columns relating to fields in |
|
|
58 |
the model. |
|
|
59 |
|
|
|
60 |
If 'with_aliases' is true, any column names that are duplicated |
|
|
61 |
(without the table names) are given unique aliases. This is needed in |
|
|
62 |
some cases to avoid ambiguitity with nested queries. |
|
|
63 |
|
|
|
64 |
This routine is overridden from Query to handle customized selection of |
|
|
65 |
geometry columns. |
|
|
66 |
""" |
|
|
67 |
qn = self.quote_name_unless_alias |
|
|
68 |
qn2 = self.connection.ops.quote_name |
|
|
69 |
result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias)) |
|
|
70 |
for alias, col in self.extra_select.iteritems()] |
|
|
71 |
aliases = set(self.extra_select.keys()) |
|
|
72 |
if with_aliases: |
|
|
73 |
col_aliases = aliases.copy() |
|
|
74 |
else: |
|
|
75 |
col_aliases = set() |
|
|
76 |
if self.select: |
|
|
77 |
only_load = self.deferred_to_columns() |
|
|
78 |
# This loop customized for GeoQuery. |
|
|
79 |
for col, field in izip(self.select, self.select_fields): |
|
|
80 |
if isinstance(col, (list, tuple)): |
|
|
81 |
alias, column = col |
|
|
82 |
table = self.alias_map[alias][TABLE_NAME] |
|
|
83 |
if table in only_load and col not in only_load[table]: |
|
|
84 |
continue |
|
|
85 |
r = self.get_field_select(field, alias, column) |
|
|
86 |
if with_aliases: |
|
|
87 |
if col[1] in col_aliases: |
|
|
88 |
c_alias = 'Col%d' % len(col_aliases) |
|
|
89 |
result.append('%s AS %s' % (r, c_alias)) |
|
|
90 |
aliases.add(c_alias) |
|
|
91 |
col_aliases.add(c_alias) |
|
|
92 |
else: |
|
|
93 |
result.append('%s AS %s' % (r, qn2(col[1]))) |
|
|
94 |
aliases.add(r) |
|
|
95 |
col_aliases.add(col[1]) |
|
|
96 |
else: |
|
|
97 |
result.append(r) |
|
|
98 |
aliases.add(r) |
|
|
99 |
col_aliases.add(col[1]) |
|
|
100 |
else: |
|
|
101 |
result.append(col.as_sql(quote_func=qn)) |
|
|
102 |
|
|
|
103 |
if hasattr(col, 'alias'): |
|
|
104 |
aliases.add(col.alias) |
|
|
105 |
col_aliases.add(col.alias) |
|
|
106 |
|
|
|
107 |
elif self.default_cols: |
|
|
108 |
cols, new_aliases = self.get_default_columns(with_aliases, |
|
|
109 |
col_aliases) |
|
|
110 |
result.extend(cols) |
|
|
111 |
aliases.update(new_aliases) |
|
|
112 |
|
|
|
113 |
result.extend([ |
|
|
114 |
'%s%s' % ( |
|
|
115 |
self.get_extra_select_format(alias) % aggregate.as_sql(quote_func=qn), |
|
|
116 |
alias is not None and ' AS %s' % alias or '' |
|
|
117 |
) |
|
|
118 |
for alias, aggregate in self.aggregate_select.items() |
|
|
119 |
]) |
|
|
120 |
|
|
|
121 |
# This loop customized for GeoQuery. |
|
|
122 |
for (table, col), field in izip(self.related_select_cols, self.related_select_fields): |
|
|
123 |
r = self.get_field_select(field, table, col) |
|
|
124 |
if with_aliases and col in col_aliases: |
|
|
125 |
c_alias = 'Col%d' % len(col_aliases) |
|
|
126 |
result.append('%s AS %s' % (r, c_alias)) |
|
|
127 |
aliases.add(c_alias) |
|
|
128 |
col_aliases.add(c_alias) |
|
|
129 |
else: |
|
|
130 |
result.append(r) |
|
|
131 |
aliases.add(r) |
|
|
132 |
col_aliases.add(col) |
|
|
133 |
|
|
|
134 |
self._select_aliases = aliases |
|
|
135 |
return result |
|
|
136 |
|
|
|
137 |
def get_default_columns(self, with_aliases=False, col_aliases=None, |
|
|
138 |
start_alias=None, opts=None, as_pairs=False): |
|
|
139 |
""" |
|
|
140 |
Computes the default columns for selecting every field in the base |
|
|
141 |
model. Will sometimes be called to pull in related models (e.g. via |
|
|
142 |
select_related), in which case "opts" and "start_alias" will be given |
|
|
143 |
to provide a starting point for the traversal. |
|
|
144 |
|
|
|
145 |
Returns a list of strings, quoted appropriately for use in SQL |
|
|
146 |
directly, as well as a set of aliases used in the select statement (if |
|
|
147 |
'as_pairs' is True, returns a list of (alias, col_name) pairs instead |
|
|
148 |
of strings as the first component and None as the second component). |
|
|
149 |
|
|
|
150 |
This routine is overridden from Query to handle customized selection of |
|
|
151 |
geometry columns. |
|
|
152 |
""" |
|
|
153 |
result = [] |
|
|
154 |
if opts is None: |
|
|
155 |
opts = self.model._meta |
|
|
156 |
aliases = set() |
|
|
157 |
only_load = self.deferred_to_columns() |
|
|
158 |
# Skip all proxy to the root proxied model |
|
|
159 |
proxied_model = get_proxied_model(opts) |
|
|
160 |
|
|
|
161 |
if start_alias: |
|
|
162 |
seen = {None: start_alias} |
|
|
163 |
for field, model in opts.get_fields_with_model(): |
|
|
164 |
if start_alias: |
|
|
165 |
try: |
|
|
166 |
alias = seen[model] |
|
|
167 |
except KeyError: |
|
|
168 |
if model is proxied_model: |
|
|
169 |
alias = start_alias |
|
|
170 |
else: |
|
|
171 |
link_field = opts.get_ancestor_link(model) |
|
|
172 |
alias = self.join((start_alias, model._meta.db_table, |
|
|
173 |
link_field.column, model._meta.pk.column)) |
|
|
174 |
seen[model] = alias |
|
|
175 |
else: |
|
|
176 |
# If we're starting from the base model of the queryset, the |
|
|
177 |
# aliases will have already been set up in pre_sql_setup(), so |
|
|
178 |
# we can save time here. |
|
|
179 |
alias = self.included_inherited_models[model] |
|
|
180 |
table = self.alias_map[alias][TABLE_NAME] |
|
|
181 |
if table in only_load and field.column not in only_load[table]: |
|
|
182 |
continue |
|
|
183 |
if as_pairs: |
|
|
184 |
result.append((alias, field.column)) |
|
|
185 |
aliases.add(alias) |
|
|
186 |
continue |
|
|
187 |
# This part of the function is customized for GeoQuery. We |
|
|
188 |
# see if there was any custom selection specified in the |
|
|
189 |
# dictionary, and set up the selection format appropriately. |
|
|
190 |
field_sel = self.get_field_select(field, alias) |
|
|
191 |
if with_aliases and field.column in col_aliases: |
|
|
192 |
c_alias = 'Col%d' % len(col_aliases) |
|
|
193 |
result.append('%s AS %s' % (field_sel, c_alias)) |
|
|
194 |
col_aliases.add(c_alias) |
|
|
195 |
aliases.add(c_alias) |
|
|
196 |
else: |
|
|
197 |
r = field_sel |
|
|
198 |
result.append(r) |
|
|
199 |
aliases.add(r) |
|
|
200 |
if with_aliases: |
|
|
201 |
col_aliases.add(field.column) |
|
|
202 |
return result, aliases |
|
|
203 |
|
|
|
204 |
def resolve_columns(self, row, fields=()): |
|
|
205 |
""" |
|
|
206 |
This routine is necessary so that distances and geometries returned |
|
|
207 |
from extra selection SQL get resolved appropriately into Python |
|
|
208 |
objects. |
|
|
209 |
""" |
|
|
210 |
values = [] |
|
|
211 |
aliases = self.extra_select.keys() |
|
|
212 |
if self.aggregates: |
|
|
213 |
# If we have an aggregate annotation, must extend the aliases |
|
|
214 |
# so their corresponding row values are included. |
|
|
215 |
aliases.extend([None for i in xrange(len(self.aggregates))]) |
|
|
216 |
|
|
|
217 |
# Have to set a starting row number offset that is used for |
|
|
218 |
# determining the correct starting row index -- needed for |
|
|
219 |
# doing pagination with Oracle. |
|
|
220 |
rn_offset = 0 |
|
|
221 |
if SpatialBackend.oracle: |
|
|
222 |
if self.high_mark is not None or self.low_mark: rn_offset = 1 |
|
|
223 |
index_start = rn_offset + len(aliases) |
|
|
224 |
|
|
|
225 |
# Converting any extra selection values (e.g., geometries and |
|
|
226 |
# distance objects added by GeoQuerySet methods). |
|
|
227 |
values = [self.convert_values(v, self.extra_select_fields.get(a, None)) |
|
|
228 |
for v, a in izip(row[rn_offset:index_start], aliases)] |
|
|
229 |
if SpatialBackend.oracle or getattr(self, 'geo_values', False): |
|
|
230 |
# We resolve the rest of the columns if we're on Oracle or if |
|
|
231 |
# the `geo_values` attribute is defined. |
|
|
232 |
for value, field in izip(row[index_start:], fields): |
|
|
233 |
values.append(self.convert_values(value, field)) |
|
|
234 |
else: |
|
|
235 |
values.extend(row[index_start:]) |
|
|
236 |
return tuple(values) |
|
|
237 |
|
|
|
238 |
def convert_values(self, value, field): |
|
|
239 |
""" |
|
|
240 |
Using the same routines that Oracle does we can convert our |
|
|
241 |
extra selection objects into Geometry and Distance objects. |
|
|
242 |
TODO: Make converted objects 'lazy' for less overhead. |
|
|
243 |
""" |
|
|
244 |
if SpatialBackend.oracle: |
|
|
245 |
# Running through Oracle's first. |
|
|
246 |
value = super(GeoQuery, self).convert_values(value, field or GeomField()) |
|
|
247 |
|
|
|
248 |
if isinstance(field, DistanceField): |
|
|
249 |
# Using the field's distance attribute, can instantiate |
|
|
250 |
# `Distance` with the right context. |
|
|
251 |
value = Distance(**{field.distance_att : value}) |
|
|
252 |
elif isinstance(field, AreaField): |
|
|
253 |
value = Area(**{field.area_att : value}) |
|
|
254 |
elif isinstance(field, (GeomField, GeometryField)) and value: |
|
|
255 |
value = SpatialBackend.Geometry(value) |
|
|
256 |
return value |
|
|
257 |
|
|
|
258 |
def resolve_aggregate(self, value, aggregate): |
|
|
259 |
""" |
|
|
260 |
Overridden from GeoQuery's normalize to handle the conversion of |
|
|
261 |
GeoAggregate objects. |
|
|
262 |
""" |
|
|
263 |
if isinstance(aggregate, self.aggregates_module.GeoAggregate): |
|
|
264 |
if aggregate.is_extent: |
|
|
265 |
return self.aggregates_module.convert_extent(value) |
|
|
266 |
else: |
|
|
267 |
return self.aggregates_module.convert_geom(value, aggregate.source) |
|
|
268 |
else: |
|
|
269 |
return super(GeoQuery, self).resolve_aggregate(value, aggregate) |
|
|
270 |
|
|
|
271 |
#### Routines unique to GeoQuery #### |
|
|
272 |
def get_extra_select_format(self, alias): |
|
|
273 |
sel_fmt = '%s' |
|
|
274 |
if alias in self.custom_select: |
|
|
275 |
sel_fmt = sel_fmt % self.custom_select[alias] |
|
|
276 |
return sel_fmt |
|
|
277 |
|
|
|
278 |
def get_field_select(self, field, alias=None, column=None): |
|
|
279 |
""" |
|
|
280 |
Returns the SELECT SQL string for the given field. Figures out |
|
|
281 |
if any custom selection SQL is needed for the column The `alias` |
|
|
282 |
keyword may be used to manually specify the database table where |
|
|
283 |
the column exists, if not in the model associated with this |
|
|
284 |
`GeoQuery`. Similarly, `column` may be used to specify the exact |
|
|
285 |
column name, rather than using the `column` attribute on `field`. |
|
|
286 |
""" |
|
|
287 |
sel_fmt = self.get_select_format(field) |
|
|
288 |
if field in self.custom_select: |
|
|
289 |
field_sel = sel_fmt % self.custom_select[field] |
|
|
290 |
else: |
|
|
291 |
field_sel = sel_fmt % self._field_column(field, alias, column) |
|
|
292 |
return field_sel |
|
|
293 |
|
|
|
294 |
def get_select_format(self, fld): |
|
|
295 |
""" |
|
|
296 |
Returns the selection format string, depending on the requirements |
|
|
297 |
of the spatial backend. For example, Oracle and MySQL require custom |
|
|
298 |
selection formats in order to retrieve geometries in OGC WKT. For all |
|
|
299 |
other fields a simple '%s' format string is returned. |
|
|
300 |
""" |
|
|
301 |
if SpatialBackend.select and hasattr(fld, 'geom_type'): |
|
|
302 |
# This allows operations to be done on fields in the SELECT, |
|
|
303 |
# overriding their values -- used by the Oracle and MySQL |
|
|
304 |
# spatial backends to get database values as WKT, and by the |
|
|
305 |
# `transform` method. |
|
|
306 |
sel_fmt = SpatialBackend.select |
|
|
307 |
|
|
|
308 |
# Because WKT doesn't contain spatial reference information, |
|
|
309 |
# the SRID is prefixed to the returned WKT to ensure that the |
|
|
310 |
# transformed geometries have an SRID different than that of the |
|
|
311 |
# field -- this is only used by `transform` for Oracle and |
|
|
312 |
# SpatiaLite backends. |
|
|
313 |
if self.transformed_srid and ( SpatialBackend.oracle or |
|
|
314 |
SpatialBackend.spatialite ): |
|
|
315 |
sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt) |
|
|
316 |
else: |
|
|
317 |
sel_fmt = '%s' |
|
|
318 |
return sel_fmt |
|
|
319 |
|
|
|
320 |
# Private API utilities, subject to change. |
|
|
321 |
def _field_column(self, field, table_alias=None, column=None): |
|
|
322 |
""" |
|
|
323 |
Helper function that returns the database column for the given field. |
|
|
324 |
The table and column are returned (quoted) in the proper format, e.g., |
|
|
325 |
`"geoapp_city"."point"`. If `table_alias` is not specified, the |
|
|
326 |
database table associated with the model of this `GeoQuery` will be |
|
|
327 |
used. If `column` is specified, it will be used instead of the value |
|
|
328 |
in `field.column`. |
|
|
329 |
""" |
|
|
330 |
if table_alias is None: table_alias = self.model._meta.db_table |
|
|
331 |
return "%s.%s" % (self.quote_name_unless_alias(table_alias), |
|
|
332 |
self.connection.ops.quote_name(column or field.column)) |
|
|
333 |
|
|
|
334 |
def _geo_field(self, field_name=None): |
|
|
335 |
""" |
|
|
336 |
Returns the first Geometry field encountered; or specified via the |
|
|
337 |
`field_name` keyword. The `field_name` may be a string specifying |
|
|
338 |
the geometry field on this GeoQuery's model, or a lookup string |
|
|
339 |
to a geometry field via a ForeignKey relation. |
|
|
340 |
""" |
|
|
341 |
if field_name is None: |
|
|
342 |
# Incrementing until the first geographic field is found. |
|
|
343 |
for fld in self.model._meta.fields: |
|
|
344 |
if isinstance(fld, GeometryField): return fld |
|
|
345 |
return False |
|
|
346 |
else: |
|
|
347 |
# Otherwise, check by the given field name -- which may be |
|
|
348 |
# a lookup to a _related_ geographic field. |
|
|
349 |
return GeoWhereNode._check_geo_field(self.model._meta, field_name) |
|
|
350 |
|
|
|
351 |
if SpatialBackend.oracle: |
|
|
352 |
def unpickle_geoquery(): |
|
|
353 |
""" |
|
|
354 |
Utility function, called by Python's unpickling machinery, that handles |
|
|
355 |
unpickling of GeoQuery subclasses of OracleQuery. |
|
|
356 |
""" |
|
|
357 |
return GeoQuery.__new__(GeoQuery) |
|
|
358 |
unpickle_geoquery.__safe_for_unpickling__ = True |