web/lib/django/contrib/gis/db/models/sql/query.py
changeset 29 cc9b7e14412b
parent 0 0d40e90630ef
--- a/web/lib/django/contrib/gis/db/models/sql/query.py	Wed May 19 17:43:59 2010 +0200
+++ b/web/lib/django/contrib/gis/db/models/sql/query.py	Tue May 25 02:43:45 2010 +0200
@@ -1,21 +1,25 @@
-from itertools import izip
+from django.db import connections
 from django.db.models.query import sql
-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 import aggregates as gis_aggregates_module
+from django.contrib.gis.db.models.sql import aggregates as gis_aggregates
 from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField
 from django.contrib.gis.db.models.sql.where import GeoWhereNode
+from django.contrib.gis.geometry.backend import Geometry
 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)
 
-# Pulling out other needed constants/routines to avoid attribute lookups.
-TABLE_NAME = sql.constants.TABLE_NAME
-get_proxied_model = sql.query.get_proxied_model
+ALL_TERMS = dict([(x, None) for x in (
+            'bbcontains', 'bboverlaps', 'contained', 'contains',
+            'contains_properly', 'coveredby', 'covers', 'crosses', 'disjoint',
+            'distance_gt', 'distance_gte', 'distance_lt', 'distance_lte',
+            'dwithin', 'equals', 'exact',
+            'intersects', 'overlaps', 'relate', 'same_as', 'touches', 'within',
+            'left', 'right', 'overlaps_left', 'overlaps_right',
+            'overlaps_above', 'overlaps_below',
+            'strictly_above', 'strictly_below'
+            )])
+ALL_TERMS.update(sql.constants.QUERY_TERMS)
 
 class GeoQuery(sql.Query):
     """
@@ -23,11 +27,13 @@
     """
     # Overridding the valid query terms.
     query_terms = ALL_TERMS
-    aggregates_module = gis_aggregates_module
+    aggregates_module = gis_aggregates
+
+    compiler = 'GeoSQLCompiler'
 
     #### Methods overridden from the base Query class ####
-    def __init__(self, model, conn):
-        super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
+    def __init__(self, model, where=GeoWhereNode):
+        super(GeoQuery, self).__init__(model, where)
         # The following attributes are customized for the GeoQuerySet.
         # The GeoWhereNode and SpatialBackend classes contain backend-specific
         # routines and functions.
@@ -35,13 +41,6 @@
         self.transformed_srid = None
         self.extra_select_fields = {}
 
-    if SpatialBackend.oracle:
-        # Have to override this so that GeoQuery, instead of OracleQuery,
-        # is returned when unpickling.
-        def __reduce__(self):
-            callable, args, data = super(GeoQuery, self).__reduce__()
-            return (unpickle_geoquery, (), data)
-
     def clone(self, *args, **kwargs):
         obj = super(GeoQuery, self).clone(*args, **kwargs)
         # Customized selection dictionary and transformed srid flag have
@@ -51,286 +50,57 @@
         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:
-            only_load = self.deferred_to_columns()
-            # This loop customized for GeoQuery.
-            for col, field in izip(self.select, self.select_fields):
-                if isinstance(col, (list, tuple)):
-                    alias, column = col
-                    table = self.alias_map[alias][TABLE_NAME]
-                    if table in only_load and col not in only_load[table]:
-                        continue
-                    r = self.get_field_select(field, alias, column)
-                    if with_aliases:
-                        if 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('%s AS %s' % (r, qn2(col[1])))
-                            aliases.add(r)
-                            col_aliases.add(col[1])
-                    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)
-
-        result.extend([
-                '%s%s' % (
-                    self.get_extra_select_format(alias) % aggregate.as_sql(quote_func=qn),
-                    alias is not None and ' AS %s' % alias or ''
-                    )
-                for alias, aggregate in self.aggregate_select.items()
-        ])
-
-        # This loop customized for GeoQuery.
-        for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
-            r = self.get_field_select(field, table, col)
-            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. Will sometimes be called to pull in related models (e.g. via
-        select_related), in which case "opts" and "start_alias" will be given
-        to provide a starting point for the traversal.
-
-        Returns a list of strings, quoted appropriately for use in SQL
-        directly, as well as a set of aliases used in the select statement (if
-        'as_pairs' is True, returns a list of (alias, col_name) pairs instead
-        of strings as the first component and None as the second component).
-
-        This routine is overridden from Query to handle customized selection of
-        geometry columns.
-        """
-        result = []
-        if opts is None:
-            opts = self.model._meta
-        aliases = set()
-        only_load = self.deferred_to_columns()
-        # Skip all proxy to the root proxied model
-        proxied_model = get_proxied_model(opts)
-
-        if start_alias:
-            seen = {None: start_alias}
-        for field, model in opts.get_fields_with_model():
-            if start_alias:
-                try:
-                    alias = seen[model]
-                except KeyError:
-                    if model is proxied_model:
-                        alias = start_alias
-                    else:
-                        link_field = opts.get_ancestor_link(model)
-                        alias = self.join((start_alias, model._meta.db_table,
-                                           link_field.column, model._meta.pk.column))
-                    seen[model] = alias
-            else:
-                # If we're starting from the base model of the queryset, the
-                # aliases will have already been set up in pre_sql_setup(), so
-                # we can save time here.
-                alias = self.included_inherited_models[model]
-            table = self.alias_map[alias][TABLE_NAME]
-            if table in only_load and field.column not in only_load[table]:
-                continue
-            if as_pairs:
-                result.append((alias, field.column))
-                aliases.add(alias)
-                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)
-        return result, aliases
-
-    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()
-        if self.aggregates:
-            # If we have an aggregate annotation, must extend the aliases
-            # so their corresponding row values are included.
-            aliases.extend([None for i in xrange(len(self.aggregates))])
-
-        # Have to set a starting row number offset that is used for
-        # determining the correct starting row index -- needed for
-        # doing pagination with Oracle.
-        rn_offset = 0
-        if SpatialBackend.oracle:
-            if self.high_mark is not None or self.low_mark: rn_offset = 1
-        index_start = rn_offset + len(aliases)
-
-        # Converting any extra selection values (e.g., geometries and
-        # distance objects added by GeoQuerySet methods).
-        values = [self.convert_values(v, self.extra_select_fields.get(a, None))
-                  for v, a in izip(row[rn_offset:index_start], aliases)]
-        if SpatialBackend.oracle or getattr(self, 'geo_values', False):
-            # We resolve the rest of the columns if we're on Oracle or if
-            # the `geo_values` attribute is defined.
-            for value, field in izip(row[index_start:], fields):
-                values.append(self.convert_values(value, field))
-        else:
-            values.extend(row[index_start:])
-        return tuple(values)
-
-    def convert_values(self, value, field):
+    def convert_values(self, value, field, connection):
         """
         Using the same routines that Oracle does we can convert our
         extra selection objects into Geometry and Distance objects.
         TODO: Make converted objects 'lazy' for less overhead.
         """
-        if SpatialBackend.oracle:
+        if connection.ops.oracle:
             # Running through Oracle's first.
-            value = super(GeoQuery, self).convert_values(value, field or GeomField())
+            value = super(GeoQuery, self).convert_values(value, field or GeomField(), connection)
 
-        if isinstance(field, DistanceField):
+        if value is None:
+            # Output from spatial function is NULL (e.g., called
+            # function on a geometry field with NULL value).
+            pass
+        elif 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, GeometryField)) and value:
-            value = SpatialBackend.Geometry(value)
+            value = Geometry(value)
         return value
 
-    def resolve_aggregate(self, value, aggregate):
+    def get_aggregation(self, using):
+        # Remove any aggregates marked for reduction from the subquery
+        # and move them to the outer AggregateQuery.
+        connection = connections[using]
+        for alias, aggregate in self.aggregate_select.items():
+            if isinstance(aggregate, gis_aggregates.GeoAggregate):
+                if not getattr(aggregate, 'is_extent', False) or connection.ops.oracle:
+                    self.extra_select_fields[alias] = GeomField()
+        return super(GeoQuery, self).get_aggregation(using)
+
+    def resolve_aggregate(self, value, aggregate, connection):
         """
         Overridden from GeoQuery's normalize to handle the conversion of
         GeoAggregate objects.
         """
         if isinstance(aggregate, self.aggregates_module.GeoAggregate):
             if aggregate.is_extent:
-                return self.aggregates_module.convert_extent(value)
+                if aggregate.is_extent == '3D':
+                    return connection.ops.convert_extent3d(value)
+                else:
+                    return connection.ops.convert_extent(value)
             else:
-                return self.aggregates_module.convert_geom(value, aggregate.source)
-        else:
-            return super(GeoQuery, self).resolve_aggregate(value, aggregate)
-
-    #### 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, field, alias=None, column=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`.  Similarly, `column` may be used to specify the exact
-        column name, rather than using the `column` attribute on `field`.
-        """
-        sel_fmt = self.get_select_format(field)
-        if field in self.custom_select:
-            field_sel = sel_fmt % self.custom_select[field]
+                return connection.ops.convert_geom(value, aggregate.source)
         else:
-            field_sel = sel_fmt % self._field_column(field, alias, column)
-        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_type'):
-            # 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 and
-            # SpatiaLite backends.
-            if self.transformed_srid and ( SpatialBackend.oracle or
-                                           SpatialBackend.spatialite ):
-                sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
-        else:
-            sel_fmt = '%s'
-        return sel_fmt
+            return super(GeoQuery, self).resolve_aggregate(value, aggregate, connection)
 
     # Private API utilities, subject to change.
-    def _field_column(self, field, table_alias=None, column=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 `column` is specified, it will be used instead of the value
-        in `field.column`.
-        """
-        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(column or field.column))
-
     def _geo_field(self, field_name=None):
         """
         Returns the first Geometry field encountered; or specified via the
@@ -347,12 +117,3 @@
             # Otherwise, check by the given field name -- which may be
             # a lookup to a _related_ geographic field.
             return GeoWhereNode._check_geo_field(self.model._meta, field_name)
-
-if SpatialBackend.oracle:
-    def unpickle_geoquery():
-        """
-        Utility function, called by Python's unpickling machinery, that handles
-        unpickling of GeoQuery subclasses of OracleQuery.
-        """
-        return GeoQuery.__new__(GeoQuery)
-    unpickle_geoquery.__safe_for_unpickling__ = True