diff -r 6641e941ef1e -r ff1a9aa48cfd app/django/contrib/gis/db/models/sql/query.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/django/contrib/gis/db/models/sql/query.py Tue Oct 14 16:00:59 2008 +0000 @@ -0,0 +1,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