diff options
| author | Alex Gaynor <alex.gaynor@gmail.com> | 2009-12-12 02:13:30 +0000 |
|---|---|---|
| committer | Alex Gaynor <alex.gaynor@gmail.com> | 2009-12-12 02:13:30 +0000 |
| commit | 049dc42bdea8f524f9d52edd4ddc05ef0048d401 (patch) | |
| tree | 30186d401cde3159214164995a2ebc88b3114526 /django/contrib/gis/db/models/sql/query.py | |
| parent | c88113683d6538dbc2f779ff57fd1ea20211abec (diff) | |
| download | django-049dc42bdea8f524f9d52edd4ddc05ef0048d401.tar.gz | |
[soc2009/multidb] Merged in all of Justin Bronn's GIS work. Multidb should now work fully with GIS. This is backwards incompatible, if you are using GIS, your ENGINE setting should now be django.contrib.gis.db.backends.XXX where XXX is the name of your DB backend. Thanks to Justin for all his work. This also resolves merge conflicts from the previous commit.
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11813 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/contrib/gis/db/models/sql/query.py')
| -rw-r--r-- | django/contrib/gis/db/models/sql/query.py | 325 |
1 files changed, 39 insertions, 286 deletions
diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index 4b34c20878..ed3ca9d9ff 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -1,21 +1,25 @@ -from itertools import izip +from django.db import connections, DEFAULT_DB_ALIAS 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 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 @@ class GeoQuery(sql.Query): """ # 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 @@ class GeoQuery(sql.Query): 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,199 +50,14 @@ class GeoQuery(sql.Query): 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(qn=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(qn=qn, connection=self.connection), - 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): - """ - Using the same routines that Oracle does we can convert our + 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): # Using the field's distance attribute, can instantiate @@ -252,10 +66,20 @@ class GeoQuery(sql.Query): 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. @@ -263,77 +87,15 @@ class GeoQuery(sql.Query): if isinstance(aggregate, self.aggregates_module.GeoAggregate): if aggregate.is_extent: if aggregate.is_extent == '3D': - return self.aggregates_module.convert_extent3d(value) + return connection.ops.convert_extent3d(value) else: - return self.aggregates_module.convert_extent(value) + 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 @@ -350,12 +112,3 @@ class GeoQuery(sql.Query): # 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 |
