~ubuntu-branches/ubuntu/quantal/python-django/quantal-security

« back to all changes in this revision

Viewing changes to django/contrib/gis/db/models/sql/query.py

  • Committer: Bazaar Package Importer
  • Author(s): Chris Lamb
  • Date: 2010-05-21 07:52:55 UTC
  • mfrom: (1.3.6 upstream)
  • mto: This revision was merged to the branch mainline in revision 28.
  • Revision ID: james.westby@ubuntu.com-20100521075255-ii78v1dyfmyu3uzx
Tags: upstream-1.2
ImportĀ upstreamĀ versionĀ 1.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from itertools import izip
 
1
from django.db import connections
2
2
from django.db.models.query import sql
3
 
from django.db.models.fields.related import ForeignKey
4
3
 
5
 
from django.contrib.gis.db.backend import SpatialBackend
6
4
from django.contrib.gis.db.models.fields import GeometryField
7
 
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates_module
 
5
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates
8
6
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField
9
7
from django.contrib.gis.db.models.sql.where import GeoWhereNode
 
8
from django.contrib.gis.geometry.backend import Geometry
10
9
from django.contrib.gis.measure import Area, Distance
11
10
 
12
 
# Valid GIS query types.
13
 
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
14
 
ALL_TERMS.update(SpatialBackend.gis_terms)
15
11
 
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
 
12
ALL_TERMS = dict([(x, None) for x in (
 
13
            'bbcontains', 'bboverlaps', 'contained', 'contains',
 
14
            'contains_properly', 'coveredby', 'covers', 'crosses', 'disjoint',
 
15
            'distance_gt', 'distance_gte', 'distance_lt', 'distance_lte',
 
16
            'dwithin', 'equals', 'exact',
 
17
            'intersects', 'overlaps', 'relate', 'same_as', 'touches', 'within',
 
18
            'left', 'right', 'overlaps_left', 'overlaps_right',
 
19
            'overlaps_above', 'overlaps_below',
 
20
            'strictly_above', 'strictly_below'
 
21
            )])
 
22
ALL_TERMS.update(sql.constants.QUERY_TERMS)
19
23
 
20
24
class GeoQuery(sql.Query):
21
25
    """
23
27
    """
24
28
    # Overridding the valid query terms.
25
29
    query_terms = ALL_TERMS
26
 
    aggregates_module = gis_aggregates_module
 
30
    aggregates_module = gis_aggregates
 
31
 
 
32
    compiler = 'GeoSQLCompiler'
27
33
 
28
34
    #### Methods overridden from the base Query class ####
29
 
    def __init__(self, model, conn):
30
 
        super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
 
35
    def __init__(self, model, where=GeoWhereNode):
 
36
        super(GeoQuery, self).__init__(model, where)
31
37
        # The following attributes are customized for the GeoQuerySet.
32
38
        # The GeoWhereNode and SpatialBackend classes contain backend-specific
33
39
        # routines and functions.
35
41
        self.transformed_srid = None
36
42
        self.extra_select_fields = {}
37
43
 
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
44
    def clone(self, *args, **kwargs):
46
45
        obj = super(GeoQuery, self).clone(*args, **kwargs)
47
46
        # Customized selection dictionary and transformed srid flag have
51
50
        obj.extra_select_fields = self.extra_select_fields.copy()
52
51
        return obj
53
52
 
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):
 
53
    def convert_values(self, value, field, connection):
239
54
        """
240
55
        Using the same routines that Oracle does we can convert our
241
56
        extra selection objects into Geometry and Distance objects.
242
57
        TODO: Make converted objects 'lazy' for less overhead.
243
58
        """
244
 
        if SpatialBackend.oracle:
 
59
        if connection.ops.oracle:
245
60
            # Running through Oracle's first.
246
 
            value = super(GeoQuery, self).convert_values(value, field or GeomField())
 
61
            value = super(GeoQuery, self).convert_values(value, field or GeomField(), connection)
247
62
 
248
 
        if isinstance(field, DistanceField):
 
63
        if value is None:
 
64
            # Output from spatial function is NULL (e.g., called
 
65
            # function on a geometry field with NULL value).
 
66
            pass
 
67
        elif isinstance(field, DistanceField):
249
68
            # Using the field's distance attribute, can instantiate
250
69
            # `Distance` with the right context.
251
70
            value = Distance(**{field.distance_att : value})
252
71
        elif isinstance(field, AreaField):
253
72
            value = Area(**{field.area_att : value})
254
73
        elif isinstance(field, (GeomField, GeometryField)) and value:
255
 
            value = SpatialBackend.Geometry(value)
 
74
            value = Geometry(value)
256
75
        return value
257
76
 
258
 
    def resolve_aggregate(self, value, aggregate):
 
77
    def get_aggregation(self, using):
 
78
        # Remove any aggregates marked for reduction from the subquery
 
79
        # and move them to the outer AggregateQuery.
 
80
        connection = connections[using]
 
81
        for alias, aggregate in self.aggregate_select.items():
 
82
            if isinstance(aggregate, gis_aggregates.GeoAggregate):
 
83
                if not getattr(aggregate, 'is_extent', False) or connection.ops.oracle:
 
84
                    self.extra_select_fields[alias] = GeomField()
 
85
        return super(GeoQuery, self).get_aggregation(using)
 
86
 
 
87
    def resolve_aggregate(self, value, aggregate, connection):
259
88
        """
260
89
        Overridden from GeoQuery's normalize to handle the conversion of
261
90
        GeoAggregate objects.
262
91
        """
263
92
        if isinstance(aggregate, self.aggregates_module.GeoAggregate):
264
93
            if aggregate.is_extent:
265
 
                return self.aggregates_module.convert_extent(value)
 
94
                if aggregate.is_extent == '3D':
 
95
                    return connection.ops.convert_extent3d(value)
 
96
                else:
 
97
                    return connection.ops.convert_extent(value)
266
98
            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
 
99
                return connection.ops.convert_geom(value, aggregate.source)
 
100
        else:
 
101
            return super(GeoQuery, self).resolve_aggregate(value, aggregate, connection)
319
102
 
320
103
    # 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
104
    def _geo_field(self, field_name=None):
335
105
        """
336
106
        Returns the first Geometry field encountered; or specified via the
347
117
            # Otherwise, check by the given field name -- which may be
348
118
            # a lookup to a _related_ geographic field.
349
119
            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