diff -r b758351d191f -r cc9b7e14412b web/lib/django/contrib/gis/db/models/sql/query.py --- 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