summaryrefslogtreecommitdiffstats
path: root/webapp/django/contrib/gis/db/models/sql/query.py
blob: f3e9fb25ca84f272b09506cd946e42495aa86bbc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
from itertools import izip
from django.db.models.query import sql
from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ForeignKey

from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models.fields import GeometryField
from django.contrib.gis.db.models.sql.where import GeoWhereNode
from django.contrib.gis.measure import Area, Distance

# Valid GIS query types.
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
ALL_TERMS.update(SpatialBackend.gis_terms)

class GeoQuery(sql.Query):
    """
    A single spatial SQL query.
    """
    # Overridding the valid query terms.
    query_terms = ALL_TERMS

    #### Methods overridden from the base Query class ####
    def __init__(self, model, conn):
        super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
        # The following attributes are customized for the GeoQuerySet.
        # The GeoWhereNode and SpatialBackend classes contain backend-specific
        # routines and functions.
        self.aggregate = False
        self.custom_select = {}
        self.transformed_srid = None
        self.extra_select_fields = {}

    def clone(self, *args, **kwargs):
        obj = super(GeoQuery, self).clone(*args, **kwargs)
        # Customized selection dictionary and transformed srid flag have
        # to also be added to obj.
        obj.aggregate = self.aggregate
        obj.custom_select = self.custom_select.copy()
        obj.transformed_srid = self.transformed_srid
        obj.extra_select_fields = self.extra_select_fields.copy()
        return obj

    def get_columns(self, with_aliases=False):
        """
        Return the list of columns to use in the select statement. If no
        columns have been specified, returns all columns relating to fields in
        the model.

        If 'with_aliases' is true, any column names that are duplicated
        (without the table names) are given unique aliases. This is needed in
        some cases to avoid ambiguitity with nested queries.

        This routine is overridden from Query to handle customized selection of 
        geometry columns.
        """
        qn = self.quote_name_unless_alias
        qn2 = self.connection.ops.quote_name
        result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias)) 
                  for alias, col in self.extra_select.iteritems()]
        aliases = set(self.extra_select.keys())
        if with_aliases:
            col_aliases = aliases.copy()
        else:
            col_aliases = set()
        if self.select:
            # This loop customized for GeoQuery.
            for col, field in izip(self.select, self.select_fields):
                if isinstance(col, (list, tuple)):
                    r = self.get_field_select(field, col[0])
                    if with_aliases and col[1] in col_aliases:
                        c_alias = 'Col%d' % len(col_aliases)
                        result.append('%s AS %s' % (r, c_alias))
                        aliases.add(c_alias)
                        col_aliases.add(c_alias)
                    else:
                        result.append(r)
                        aliases.add(r)
                        col_aliases.add(col[1])
                else:
                    result.append(col.as_sql(quote_func=qn))
                    if hasattr(col, 'alias'):
                        aliases.add(col.alias)
                        col_aliases.add(col.alias)
        elif self.default_cols:
            cols, new_aliases = self.get_default_columns(with_aliases,
                    col_aliases)
            result.extend(cols)
            aliases.update(new_aliases)
        # This loop customized for GeoQuery.
        if not self.aggregate:
            for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
                r = self.get_field_select(field, table)
                if with_aliases and col in col_aliases:
                    c_alias = 'Col%d' % len(col_aliases)
                    result.append('%s AS %s' % (r, c_alias))
                    aliases.add(c_alias)
                    col_aliases.add(c_alias)
                else:
                    result.append(r)
                    aliases.add(r)
                    col_aliases.add(col)

        self._select_aliases = aliases
        return result

    def get_default_columns(self, with_aliases=False, col_aliases=None,
                            start_alias=None, opts=None, as_pairs=False):
        """
        Computes the default columns for selecting every field in the base
        model.

        Returns a list of strings, quoted appropriately for use in SQL
        directly, as well as a set of aliases used in the select statement.

        This routine is overridden from Query to handle customized selection of 
        geometry columns.
        """
        result = []
        if opts is None:
            opts = self.model._meta
        if start_alias:
            table_alias = start_alias
        else:
            table_alias = self.tables[0]
        root_pk = self.model._meta.pk.column
        seen = {None: table_alias}
        aliases = set()
        for field, model in opts.get_fields_with_model():
            try:
                alias = seen[model]
            except KeyError:
                alias = self.join((table_alias, model._meta.db_table,
                        root_pk, model._meta.pk.column))
                seen[model] = alias
            if as_pairs:
                result.append((alias, field.column))
                continue
            # This part of the function is customized for GeoQuery. We
            # see if there was any custom selection specified in the
            # dictionary, and set up the selection format appropriately.
            field_sel = self.get_field_select(field, alias)
            if with_aliases and field.column in col_aliases:
                c_alias = 'Col%d' % len(col_aliases)
                result.append('%s AS %s' % (field_sel, c_alias))
                col_aliases.add(c_alias)
                aliases.add(c_alias)
            else:
                r = field_sel
                result.append(r)
                aliases.add(r)
                if with_aliases:
                    col_aliases.add(field.column)
        if as_pairs:
            return result, None
        return result, aliases

    def get_ordering(self):
        """
        This routine is overridden to disable ordering for aggregate
        spatial queries.
        """
        if not self.aggregate:
            return super(GeoQuery, self).get_ordering()
        else:
            return ()

    def resolve_columns(self, row, fields=()):
        """
        This routine is necessary so that distances and geometries returned
        from extra selection SQL get resolved appropriately into Python 
        objects.
        """
        values = []
        aliases = self.extra_select.keys()
        index_start = len(aliases)
        values = [self.convert_values(v, self.extra_select_fields.get(a, None)) 
                  for v, a in izip(row[:index_start], aliases)]
        if SpatialBackend.oracle:
            # This is what happens normally in Oracle's `resolve_columns`.
            for value, field in izip(row[index_start:], fields):
                values.append(self.convert_values(value, field))
        else:
            values.extend(row[index_start:])
        return values

    def convert_values(self, value, field):
        """
        Using the same routines that Oracle does we can convert our
        extra selection objects into Geometry and Distance objects.
        TODO: Laziness.
        """
        if SpatialBackend.oracle:
            # Running through Oracle's first.
            value = super(GeoQuery, self).convert_values(value, field)
        if isinstance(field, DistanceField):
            # Using the field's distance attribute, can instantiate
            # `Distance` with the right context.
            value = Distance(**{field.distance_att : value})
        elif isinstance(field, AreaField):
            value = Area(**{field.area_att : value})
        elif isinstance(field, GeomField):
            value = SpatialBackend.Geometry(value)
        return value

    #### Routines unique to GeoQuery ####
    def get_extra_select_format(self, alias):
        sel_fmt = '%s'
        if alias in self.custom_select:
            sel_fmt = sel_fmt % self.custom_select[alias]
        return sel_fmt

    def get_field_select(self, fld, alias=None):
        """
        Returns the SELECT SQL string for the given field.  Figures out
        if any custom selection SQL is needed for the column  The `alias` 
        keyword may be used to manually specify the database table where 
        the column exists, if not in the model associated with this 
        `GeoQuery`.
        """
        sel_fmt = self.get_select_format(fld)
        if fld in self.custom_select:
            field_sel = sel_fmt % self.custom_select[fld]
        else:
            field_sel = sel_fmt % self._field_column(fld, alias)
        return field_sel

    def get_select_format(self, fld):
        """
        Returns the selection format string, depending on the requirements
        of the spatial backend.  For example, Oracle and MySQL require custom
        selection formats in order to retrieve geometries in OGC WKT. For all
        other fields a simple '%s' format string is returned.
        """
        if SpatialBackend.select and hasattr(fld, '_geom'):
            # This allows operations to be done on fields in the SELECT,
            # overriding their values -- used by the Oracle and MySQL
            # spatial backends to get database values as WKT, and by the
            # `transform` method.
            sel_fmt = SpatialBackend.select

            # Because WKT doesn't contain spatial reference information,
            # the SRID is prefixed to the returned WKT to ensure that the
            # transformed geometries have an SRID different than that of the
            # field -- this is only used by `transform` for Oracle backends.
            if self.transformed_srid and SpatialBackend.oracle:
                sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
        else:
            sel_fmt = '%s'
        return sel_fmt

    # Private API utilities, subject to change.
    def _check_geo_field(self, model, name_param):
        """
        Recursive utility routine for checking the given name parameter
        on the given model.  Initially, the name parameter is a string,
        of the field on the given model e.g., 'point', 'the_geom'. 
        Related model field strings like 'address__point', may also be 
        used.

        If a GeometryField exists according to the given name parameter 
        it will be returned, otherwise returns False.
        """
        if isinstance(name_param, basestring):
            # This takes into account the situation where the name is a 
            # lookup to a related geographic field, e.g., 'address__point'.
            name_param = name_param.split(sql.constants.LOOKUP_SEP)
            name_param.reverse() # Reversing so list operates like a queue of related lookups.
        elif not isinstance(name_param, list):
            raise TypeError
        try:
            # Getting the name of the field for the model (by popping the first
            # name from the `name_param` list created above).
            fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop())
        except (FieldDoesNotExist, IndexError):
            return False
        # TODO: ManyToManyField?
        if isinstance(fld, GeometryField): 
            return fld # A-OK.
        elif isinstance(fld, ForeignKey):
            # ForeignKey encountered, return the output of this utility called
            # on the _related_ model with the remaining name parameters.
            return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation.
        else:
            return False

    def _field_column(self, field, table_alias=None):
        """
        Helper function that returns the database column for the given field.
        The table and column are returned (quoted) in the proper format, e.g.,
        `"geoapp_city"."point"`.  If `table_alias` is not specified, the 
        database table associated with the model of this `GeoQuery` will be
        used.
        """
        if table_alias is None: table_alias = self.model._meta.db_table
        return "%s.%s" % (self.quote_name_unless_alias(table_alias), 
                          self.connection.ops.quote_name(field.column))

    def _geo_field(self, field_name=None):
        """
        Returns the first Geometry field encountered; or specified via the
        `field_name` keyword.  The `field_name` may be a string specifying
        the geometry field on this GeoQuery's model, or a lookup string
        to a geometry field via a ForeignKey relation.
        """
        if field_name is None:
            # Incrementing until the first geographic field is found.
            for fld in self.model._meta.fields:
                if isinstance(fld, GeometryField): return fld
            return False
        else:
            # Otherwise, check by the given field name -- which may be
            # a lookup to a _related_ geographic field.
            return self._check_geo_field(self.model, field_name)

### Field Classes for `convert_values` ####
class AreaField(object):
    def __init__(self, area_att):
        self.area_att = area_att

class DistanceField(object):
    def __init__(self, distance_att):
        self.distance_att = distance_att

# Rather than use GeometryField (which requires a SQL query
# upon instantiation), use this lighter weight class.
class GeomField(object): 
    pass