~canonical-django/canonical-django/project-template

« back to all changes in this revision

Viewing changes to trunk/python-packages/django/contrib/gis/db/models/sql/query.py

  • Committer: Matthew Nuzum
  • Date: 2008-11-13 05:46:03 UTC
  • Revision ID: matthew.nuzum@canonical.com-20081113054603-v0kvr6z6xyexvqt3
adding to version control

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from itertools import izip
 
2
from django.db.models.query import sql
 
3
from django.db.models.fields import FieldDoesNotExist
 
4
from django.db.models.fields.related import ForeignKey
 
5
 
 
6
from django.contrib.gis.db.backend import SpatialBackend
 
7
from django.contrib.gis.db.models.fields import GeometryField
 
8
from django.contrib.gis.db.models.sql.where import GeoWhereNode
 
9
from django.contrib.gis.measure import Area, Distance
 
10
 
 
11
# Valid GIS query types.
 
12
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
 
13
ALL_TERMS.update(SpatialBackend.gis_terms)
 
14
 
 
15
class GeoQuery(sql.Query):
 
16
    """
 
17
    A single spatial SQL query.
 
18
    """
 
19
    # Overridding the valid query terms.
 
20
    query_terms = ALL_TERMS
 
21
 
 
22
    #### Methods overridden from the base Query class ####
 
23
    def __init__(self, model, conn):
 
24
        super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
 
25
        # The following attributes are customized for the GeoQuerySet.
 
26
        # The GeoWhereNode and SpatialBackend classes contain backend-specific
 
27
        # routines and functions.
 
28
        self.aggregate = False
 
29
        self.custom_select = {}
 
30
        self.transformed_srid = None
 
31
        self.extra_select_fields = {}
 
32
 
 
33
    def clone(self, *args, **kwargs):
 
34
        obj = super(GeoQuery, self).clone(*args, **kwargs)
 
35
        # Customized selection dictionary and transformed srid flag have
 
36
        # to also be added to obj.
 
37
        obj.aggregate = self.aggregate
 
38
        obj.custom_select = self.custom_select.copy()
 
39
        obj.transformed_srid = self.transformed_srid
 
40
        obj.extra_select_fields = self.extra_select_fields.copy()
 
41
        return obj
 
42
 
 
43
    def get_columns(self, with_aliases=False):
 
44
        """
 
45
        Return the list of columns to use in the select statement. If no
 
46
        columns have been specified, returns all columns relating to fields in
 
47
        the model.
 
48
 
 
49
        If 'with_aliases' is true, any column names that are duplicated
 
50
        (without the table names) are given unique aliases. This is needed in
 
51
        some cases to avoid ambiguitity with nested queries.
 
52
 
 
53
        This routine is overridden from Query to handle customized selection of 
 
54
        geometry columns.
 
55
        """
 
56
        qn = self.quote_name_unless_alias
 
57
        qn2 = self.connection.ops.quote_name
 
58
        result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias)) 
 
59
                  for alias, col in self.extra_select.iteritems()]
 
60
        aliases = set(self.extra_select.keys())
 
61
        if with_aliases:
 
62
            col_aliases = aliases.copy()
 
63
        else:
 
64
            col_aliases = set()
 
65
        if self.select:
 
66
            # This loop customized for GeoQuery.
 
67
            for col, field in izip(self.select, self.select_fields):
 
68
                if isinstance(col, (list, tuple)):
 
69
                    r = self.get_field_select(field, col[0])
 
70
                    if with_aliases and col[1] in col_aliases:
 
71
                        c_alias = 'Col%d' % len(col_aliases)
 
72
                        result.append('%s AS %s' % (r, c_alias))
 
73
                        aliases.add(c_alias)
 
74
                        col_aliases.add(c_alias)
 
75
                    else:
 
76
                        result.append(r)
 
77
                        aliases.add(r)
 
78
                        col_aliases.add(col[1])
 
79
                else:
 
80
                    result.append(col.as_sql(quote_func=qn))
 
81
                    if hasattr(col, 'alias'):
 
82
                        aliases.add(col.alias)
 
83
                        col_aliases.add(col.alias)
 
84
        elif self.default_cols:
 
85
            cols, new_aliases = self.get_default_columns(with_aliases,
 
86
                    col_aliases)
 
87
            result.extend(cols)
 
88
            aliases.update(new_aliases)
 
89
        # This loop customized for GeoQuery.
 
90
        if not self.aggregate:
 
91
            for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
 
92
                r = self.get_field_select(field, table)
 
93
                if with_aliases and col in col_aliases:
 
94
                    c_alias = 'Col%d' % len(col_aliases)
 
95
                    result.append('%s AS %s' % (r, c_alias))
 
96
                    aliases.add(c_alias)
 
97
                    col_aliases.add(c_alias)
 
98
                else:
 
99
                    result.append(r)
 
100
                    aliases.add(r)
 
101
                    col_aliases.add(col)
 
102
 
 
103
        self._select_aliases = aliases
 
104
        return result
 
105
 
 
106
    def get_default_columns(self, with_aliases=False, col_aliases=None,
 
107
                            start_alias=None, opts=None, as_pairs=False):
 
108
        """
 
109
        Computes the default columns for selecting every field in the base
 
110
        model.
 
111
 
 
112
        Returns a list of strings, quoted appropriately for use in SQL
 
113
        directly, as well as a set of aliases used in the select statement.
 
114
 
 
115
        This routine is overridden from Query to handle customized selection of 
 
116
        geometry columns.
 
117
        """
 
118
        result = []
 
119
        if opts is None:
 
120
            opts = self.model._meta
 
121
        if start_alias:
 
122
            table_alias = start_alias
 
123
        else:
 
124
            table_alias = self.tables[0]
 
125
        root_pk = self.model._meta.pk.column
 
126
        seen = {None: table_alias}
 
127
        aliases = set()
 
128
        for field, model in opts.get_fields_with_model():
 
129
            try:
 
130
                alias = seen[model]
 
131
            except KeyError:
 
132
                alias = self.join((table_alias, model._meta.db_table,
 
133
                        root_pk, model._meta.pk.column))
 
134
                seen[model] = alias
 
135
            if as_pairs:
 
136
                result.append((alias, field.column))
 
137
                continue
 
138
            # This part of the function is customized for GeoQuery. We
 
139
            # see if there was any custom selection specified in the
 
140
            # dictionary, and set up the selection format appropriately.
 
141
            field_sel = self.get_field_select(field, alias)
 
142
            if with_aliases and field.column in col_aliases:
 
143
                c_alias = 'Col%d' % len(col_aliases)
 
144
                result.append('%s AS %s' % (field_sel, c_alias))
 
145
                col_aliases.add(c_alias)
 
146
                aliases.add(c_alias)
 
147
            else:
 
148
                r = field_sel
 
149
                result.append(r)
 
150
                aliases.add(r)
 
151
                if with_aliases:
 
152
                    col_aliases.add(field.column)
 
153
        if as_pairs:
 
154
            return result, None
 
155
        return result, aliases
 
156
 
 
157
    def get_ordering(self):
 
158
        """
 
159
        This routine is overridden to disable ordering for aggregate
 
160
        spatial queries.
 
161
        """
 
162
        if not self.aggregate:
 
163
            return super(GeoQuery, self).get_ordering()
 
164
        else:
 
165
            return ()
 
166
 
 
167
    def resolve_columns(self, row, fields=()):
 
168
        """
 
169
        This routine is necessary so that distances and geometries returned
 
170
        from extra selection SQL get resolved appropriately into Python 
 
171
        objects.
 
172
        """
 
173
        values = []
 
174
        aliases = self.extra_select.keys()
 
175
        index_start = len(aliases)
 
176
        values = [self.convert_values(v, self.extra_select_fields.get(a, None)) 
 
177
                  for v, a in izip(row[:index_start], aliases)]
 
178
        if SpatialBackend.oracle:
 
179
            # This is what happens normally in Oracle's `resolve_columns`.
 
180
            for value, field in izip(row[index_start:], fields):
 
181
                values.append(self.convert_values(value, field))
 
182
        else:
 
183
            values.extend(row[index_start:])
 
184
        return values
 
185
 
 
186
    def convert_values(self, value, field):
 
187
        """
 
188
        Using the same routines that Oracle does we can convert our
 
189
        extra selection objects into Geometry and Distance objects.
 
190
        TODO: Laziness.
 
191
        """
 
192
        if SpatialBackend.oracle:
 
193
            # Running through Oracle's first.
 
194
            value = super(GeoQuery, self).convert_values(value, field)
 
195
        if isinstance(field, DistanceField):
 
196
            # Using the field's distance attribute, can instantiate
 
197
            # `Distance` with the right context.
 
198
            value = Distance(**{field.distance_att : value})
 
199
        elif isinstance(field, AreaField):
 
200
            value = Area(**{field.area_att : value})
 
201
        elif isinstance(field, GeomField):
 
202
            value = SpatialBackend.Geometry(value)
 
203
        return value
 
204
 
 
205
    #### Routines unique to GeoQuery ####
 
206
    def get_extra_select_format(self, alias):
 
207
        sel_fmt = '%s'
 
208
        if alias in self.custom_select:
 
209
            sel_fmt = sel_fmt % self.custom_select[alias]
 
210
        return sel_fmt
 
211
 
 
212
    def get_field_select(self, fld, alias=None):
 
213
        """
 
214
        Returns the SELECT SQL string for the given field.  Figures out
 
215
        if any custom selection SQL is needed for the column  The `alias` 
 
216
        keyword may be used to manually specify the database table where 
 
217
        the column exists, if not in the model associated with this 
 
218
        `GeoQuery`.
 
219
        """
 
220
        sel_fmt = self.get_select_format(fld)
 
221
        if fld in self.custom_select:
 
222
            field_sel = sel_fmt % self.custom_select[fld]
 
223
        else:
 
224
            field_sel = sel_fmt % self._field_column(fld, alias)
 
225
        return field_sel
 
226
 
 
227
    def get_select_format(self, fld):
 
228
        """
 
229
        Returns the selection format string, depending on the requirements
 
230
        of the spatial backend.  For example, Oracle and MySQL require custom
 
231
        selection formats in order to retrieve geometries in OGC WKT. For all
 
232
        other fields a simple '%s' format string is returned.
 
233
        """
 
234
        if SpatialBackend.select and hasattr(fld, '_geom'):
 
235
            # This allows operations to be done on fields in the SELECT,
 
236
            # overriding their values -- used by the Oracle and MySQL
 
237
            # spatial backends to get database values as WKT, and by the
 
238
            # `transform` method.
 
239
            sel_fmt = SpatialBackend.select
 
240
 
 
241
            # Because WKT doesn't contain spatial reference information,
 
242
            # the SRID is prefixed to the returned WKT to ensure that the
 
243
            # transformed geometries have an SRID different than that of the
 
244
            # field -- this is only used by `transform` for Oracle backends.
 
245
            if self.transformed_srid and SpatialBackend.oracle:
 
246
                sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
 
247
        else:
 
248
            sel_fmt = '%s'
 
249
        return sel_fmt
 
250
 
 
251
    # Private API utilities, subject to change.
 
252
    def _check_geo_field(self, model, name_param):
 
253
        """
 
254
        Recursive utility routine for checking the given name parameter
 
255
        on the given model.  Initially, the name parameter is a string,
 
256
        of the field on the given model e.g., 'point', 'the_geom'. 
 
257
        Related model field strings like 'address__point', may also be 
 
258
        used.
 
259
 
 
260
        If a GeometryField exists according to the given name parameter 
 
261
        it will be returned, otherwise returns False.
 
262
        """
 
263
        if isinstance(name_param, basestring):
 
264
            # This takes into account the situation where the name is a 
 
265
            # lookup to a related geographic field, e.g., 'address__point'.
 
266
            name_param = name_param.split(sql.constants.LOOKUP_SEP)
 
267
            name_param.reverse() # Reversing so list operates like a queue of related lookups.
 
268
        elif not isinstance(name_param, list):
 
269
            raise TypeError
 
270
        try:
 
271
            # Getting the name of the field for the model (by popping the first
 
272
            # name from the `name_param` list created above).
 
273
            fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop())
 
274
        except (FieldDoesNotExist, IndexError):
 
275
            return False
 
276
        # TODO: ManyToManyField?
 
277
        if isinstance(fld, GeometryField): 
 
278
            return fld # A-OK.
 
279
        elif isinstance(fld, ForeignKey):
 
280
            # ForeignKey encountered, return the output of this utility called
 
281
            # on the _related_ model with the remaining name parameters.
 
282
            return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation.
 
283
        else:
 
284
            return False
 
285
 
 
286
    def _field_column(self, field, table_alias=None):
 
287
        """
 
288
        Helper function that returns the database column for the given field.
 
289
        The table and column are returned (quoted) in the proper format, e.g.,
 
290
        `"geoapp_city"."point"`.  If `table_alias` is not specified, the 
 
291
        database table associated with the model of this `GeoQuery` will be
 
292
        used.
 
293
        """
 
294
        if table_alias is None: table_alias = self.model._meta.db_table
 
295
        return "%s.%s" % (self.quote_name_unless_alias(table_alias), 
 
296
                          self.connection.ops.quote_name(field.column))
 
297
 
 
298
    def _geo_field(self, field_name=None):
 
299
        """
 
300
        Returns the first Geometry field encountered; or specified via the
 
301
        `field_name` keyword.  The `field_name` may be a string specifying
 
302
        the geometry field on this GeoQuery's model, or a lookup string
 
303
        to a geometry field via a ForeignKey relation.
 
304
        """
 
305
        if field_name is None:
 
306
            # Incrementing until the first geographic field is found.
 
307
            for fld in self.model._meta.fields:
 
308
                if isinstance(fld, GeometryField): return fld
 
309
            return False
 
310
        else:
 
311
            # Otherwise, check by the given field name -- which may be
 
312
            # a lookup to a _related_ geographic field.
 
313
            return self._check_geo_field(self.model, field_name)
 
314
 
 
315
### Field Classes for `convert_values` ####
 
316
class AreaField(object):
 
317
    def __init__(self, area_att):
 
318
        self.area_att = area_att
 
319
 
 
320
class DistanceField(object):
 
321
    def __init__(self, distance_att):
 
322
        self.distance_att = distance_att
 
323
 
 
324
# Rather than use GeometryField (which requires a SQL query
 
325
# upon instantiation), use this lighter weight class.
 
326
class GeomField(object): 
 
327
    pass