diff options
| author | Russell Keith-Magee <russell@keith-magee.com> | 2009-01-15 11:06:34 +0000 |
|---|---|---|
| committer | Russell Keith-Magee <russell@keith-magee.com> | 2009-01-15 11:06:34 +0000 |
| commit | cc4e4d9aee0b3ebfb45bee01aec79edc9e144c78 (patch) | |
| tree | 2cdba846a105d406ecceff2c02e071c50502d487 /django/contrib/gis/db/models | |
| parent | 50a293a0c31e7325ebd520338f9c8881f951d8a7 (diff) | |
Fixed #3566 -- Added support for aggregation to the ORM. See the documentation for details on usage.
Many thanks to:
* Nicolas Lara, who worked on this feature during the 2008 Google Summer of Code.
* Alex Gaynor for his help debugging and fixing a number of issues.
* Justin Bronn for his help integrating with contrib.gis.
* Karen Tracey for her help with cross-platform testing.
* Ian Kelly for his help testing and fixing Oracle support.
* Malcolm Tredinnick for his invaluable review notes.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@9742 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/contrib/gis/db/models')
| -rw-r--r-- | django/contrib/gis/db/models/aggregates.py | 10 | ||||
| -rw-r--r-- | django/contrib/gis/db/models/query.py | 181 | ||||
| -rw-r--r-- | django/contrib/gis/db/models/sql/aggregates.py | 36 | ||||
| -rw-r--r-- | django/contrib/gis/db/models/sql/query.py | 130 |
4 files changed, 195 insertions, 162 deletions
diff --git a/django/contrib/gis/db/models/aggregates.py b/django/contrib/gis/db/models/aggregates.py new file mode 100644 index 0000000000..111601171b --- /dev/null +++ b/django/contrib/gis/db/models/aggregates.py @@ -0,0 +1,10 @@ +from django.db.models import Aggregate + +class Extent(Aggregate): + name = 'Extent' + +class MakeLine(Aggregate): + name = 'MakeLine' + +class Union(Aggregate): + name = 'Union' diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index b7b7dcda93..8eb435de93 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -3,6 +3,7 @@ from django.db import connection from django.db.models.query import sql, QuerySet, Q from django.contrib.gis.db.backend import SpatialBackend +from django.contrib.gis.db.models import aggregates from django.contrib.gis.db.models.fields import GeometryField, PointField from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode from django.contrib.gis.measure import Area, Distance @@ -17,7 +18,7 @@ class GeomSQL(object): "Simple wrapper object for geometric SQL." def __init__(self, geo_sql): self.sql = geo_sql - + def as_sql(self, *args, **kwargs): return self.sql @@ -30,7 +31,7 @@ class GeoQuerySet(QuerySet): def area(self, tolerance=0.05, **kwargs): """ - Returns the area of the geographic field in an `area` attribute on + Returns the area of the geographic field in an `area` attribute on each element of this GeoQuerySet. """ # Peforming setup here rather than in `_spatial_attribute` so that @@ -75,21 +76,21 @@ class GeoQuerySet(QuerySet): Keyword Arguments: `spheroid` => If the geometry field is geodetic and PostGIS is - the spatial database, then the more accurate + the spatial database, then the more accurate spheroid calculation will be used instead of the quicker sphere calculation. - - `tolerance` => Used only for Oracle. The tolerance is - in meters -- a default of 5 centimeters (0.05) + + `tolerance` => Used only for Oracle. The tolerance is + in meters -- a default of 5 centimeters (0.05) is used. """ return self._distance_attribute('distance', geom, **kwargs) def envelope(self, **kwargs): """ - Returns a Geometry representing the bounding box of the + Returns a Geometry representing the bounding box of the Geometry field in an `envelope` attribute on each element of - the GeoQuerySet. + the GeoQuerySet. """ return self._geom_attribute('envelope', **kwargs) @@ -98,20 +99,7 @@ class GeoQuerySet(QuerySet): Returns the extent (aggregate) of the features in the GeoQuerySet. The extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax). """ - convert_extent = None - if SpatialBackend.postgis: - def convert_extent(box, geo_field): - # TODO: Parsing of BOX3D, Oracle support (patches welcome!) - # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; - # parsing out and returning as a 4-tuple. - ll, ur = box[4:-1].split(',') - xmin, ymin = map(float, ll.split()) - xmax, ymax = map(float, ur.split()) - return (xmin, ymin, xmax, ymax) - elif SpatialBackend.oracle: - def convert_extent(wkt, geo_field): - raise NotImplementedError - return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs) + return self._spatial_aggregate(aggregates.Extent, **kwargs) def gml(self, precision=8, version=2, **kwargs): """ @@ -120,7 +108,7 @@ class GeoQuerySet(QuerySet): """ s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} if SpatialBackend.postgis: - # PostGIS AsGML() aggregate function parameter order depends on the + # PostGIS AsGML() aggregate function parameter order depends on the # version -- uggh. major, minor1, minor2 = SpatialBackend.version if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)): @@ -163,9 +151,7 @@ class GeoQuerySet(QuerySet): this GeoQuerySet and returns it. This is a spatial aggregate method, and thus returns a geometry rather than a GeoQuerySet. """ - kwargs['geo_field_type'] = PointField - kwargs['agg_field'] = GeometryField - return self._spatial_aggregate('make_line', **kwargs) + return self._spatial_aggregate(aggregates.MakeLine, geo_field_type=PointField, **kwargs) def mem_size(self, **kwargs): """ @@ -185,7 +171,7 @@ class GeoQuerySet(QuerySet): def num_points(self, **kwargs): """ - Returns the number of points in the first linestring in the + Returns the number of points in the first linestring in the Geometry field in a `num_points` attribute on each element of this GeoQuerySet; otherwise sets with None. """ @@ -231,7 +217,7 @@ class GeoQuerySet(QuerySet): def sym_difference(self, geom, **kwargs): """ - Returns the symmetric difference of the geographic field in a + Returns the symmetric difference of the geographic field in a `sym_difference` attribute on each element of this GeoQuerySet. """ return self._geomset_attribute('sym_difference', geom, **kwargs) @@ -265,7 +251,7 @@ class GeoQuerySet(QuerySet): # when there's also a transformation we need to cascade the substitutions. # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' geo_col = self.query.custom_select.get(geo_field, field_col) - + # Setting the key for the field's column with the custom SELECT SQL to # override the geometry column returned from the database. custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid) @@ -288,11 +274,10 @@ class GeoQuerySet(QuerySet): None if the GeoQuerySet is empty. The `tolerance` keyword is for Oracle backends only. """ - kwargs['agg_field'] = GeometryField - return self._spatial_aggregate('unionagg', **kwargs) + return self._spatial_aggregate(aggregates.Union, **kwargs) ### Private API -- Abstracted DRY routines. ### - def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None): + def _spatial_setup(self, att, desc=None, field_name=None, geo_field_type=None): """ Performs set up for executing the spatial function. """ @@ -301,86 +286,52 @@ class GeoQuerySet(QuerySet): if desc is None: desc = att if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc) - # Initializing the procedure arguments. + # Initializing the procedure arguments. procedure_args = {'function' : func} - - # Is there a geographic field in the model to perform this + + # Is there a geographic field in the model to perform this # operation on? geo_field = self.query._geo_field(field_name) if not geo_field: raise TypeError('%s output only available on GeometryFields.' % func) - # If the `geo_field_type` keyword was used, then enforce that + # If the `geo_field_type` keyword was used, then enforce that # type limitation. - if not geo_field_type is None and not isinstance(geo_field, geo_field_type): - raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) + if not geo_field_type is None and not isinstance(geo_field, geo_field_type): + raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) # Setting the procedure args. - procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate) + procedure_args['geo_col'] = self._geocol_select(geo_field, field_name) return procedure_args, geo_field - def _spatial_aggregate(self, att, field_name=None, - agg_field=None, convert_func=None, - geo_field_type=None, tolerance=0.0005): + def _spatial_aggregate(self, aggregate, field_name=None, + geo_field_type=None, tolerance=0.05): """ DRY routine for calling aggregate spatial stored procedures and returning their result to the caller of the function. """ - # Constructing the setup keyword arguments. - setup_kwargs = {'aggregate' : True, - 'field_name' : field_name, - 'geo_field_type' : geo_field_type, - } - procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs) - - if SpatialBackend.oracle: - procedure_args['tolerance'] = tolerance - # Adding in selection SQL for Oracle geometry columns. - if agg_field is GeometryField: - agg_sql = '%s' % SpatialBackend.select - else: - agg_sql = '%s' - agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args) - else: - agg_sql = '%(function)s(%(geo_col)s)' % procedure_args + # Getting the field the geographic aggregate will be called on. + geo_field = self.query._geo_field(field_name) + if not geo_field: + raise TypeError('%s aggregate only available on GeometryFields.' % aggregate.name) - # Wrapping our selection SQL in `GeomSQL` to bypass quoting, and - # specifying the type of the aggregate field. - self.query.select = [GeomSQL(agg_sql)] - self.query.select_fields = [agg_field] + # Checking if there are any geo field type limitations on this + # aggregate (e.g. ST_Makeline only operates on PointFields). + if not geo_field_type is None and not isinstance(geo_field, geo_field_type): + raise TypeError('%s aggregate may only be called on %ss.' % (aggregate.name, geo_field_type.__name__)) - try: - # `asql` => not overriding `sql` module. - asql, params = self.query.as_sql() - except sql.datastructures.EmptyResultSet: - return None + # Getting the string expression of the field name, as this is the + # argument taken by `Aggregate` objects. + agg_col = field_name or geo_field.name - # Getting a cursor, executing the query, and extracting the returned - # value from the aggregate function. - cursor = connection.cursor() - cursor.execute(asql, params) - result = cursor.fetchone()[0] - - # If the `agg_field` is specified as a GeometryField, then autmatically - # set up the conversion function. - if agg_field is GeometryField and not callable(convert_func): - if SpatialBackend.postgis: - def convert_geom(hex, geo_field): - if hex: return SpatialBackend.Geometry(hex) - else: return None - elif SpatialBackend.oracle: - def convert_geom(clob, geo_field): - if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid) - else: return None - convert_func = convert_geom + # Adding any keyword parameters for the Aggregate object. Oracle backends + # in particular need an additional `tolerance` parameter. + agg_kwargs = {} + if SpatialBackend.oracle: agg_kwargs['tolerance'] = tolerance - # Returning the callback function evaluated on the result culled - # from the executed cursor. - if callable(convert_func): - return convert_func(result, geo_field) - else: - return result + # Calling the QuerySet.aggregate, and returning only the value of the aggregate. + return self.aggregate(_geoagg=aggregate(agg_col, **agg_kwargs))['_geoagg'] def _spatial_attribute(self, att, settings, field_name=None, model_att=None): """ @@ -393,7 +344,7 @@ class GeoQuerySet(QuerySet): SQL function to call. settings: - Dictonary of internal settings to customize for the spatial procedure. + Dictonary of internal settings to customize for the spatial procedure. Public Keyword Arguments: @@ -420,7 +371,7 @@ class GeoQuerySet(QuerySet): for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v) else: geo_field = settings['geo_field'] - + # The attribute to attach to the model. if not isinstance(model_att, basestring): model_att = att @@ -429,7 +380,7 @@ class GeoQuerySet(QuerySet): # Using the field's get_db_prep_lookup() to get any needed # transformation SQL -- we pass in a 'dummy' `contains` lookup. where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name]) - # Replacing the procedure format with that of any needed + # Replacing the procedure format with that of any needed # transformation SQL. old_fmt = '%%(%s)s' % name new_fmt = where[0] % '%%s' @@ -438,7 +389,7 @@ class GeoQuerySet(QuerySet): # Getting the format for the stored procedure. fmt = '%%(function)s(%s)' % settings['procedure_fmt'] - + # If the result of this function needs to be converted. if settings.get('select_field', False): sel_fld = settings['select_field'] @@ -446,10 +397,10 @@ class GeoQuerySet(QuerySet): self.query.custom_select[model_att] = SpatialBackend.select self.query.extra_select_fields[model_att] = sel_fld - # Finally, setting the extra selection attribute with + # Finally, setting the extra selection attribute with # the format string expanded with the stored procedure # arguments. - return self.extra(select={model_att : fmt % settings['procedure_args']}, + return self.extra(select={model_att : fmt % settings['procedure_args']}, select_params=settings['select_params']) def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs): @@ -471,10 +422,10 @@ class GeoQuerySet(QuerySet): distance = func == 'distance' length = func == 'length' perimeter = func == 'perimeter' - if not (distance or length or perimeter): + if not (distance or length or perimeter): raise ValueError('Unknown distance function: %s' % func) - # The field's get_db_prep_lookup() is used to get any + # The field's get_db_prep_lookup() is used to get any # extra distance parameters. Here we set up the # parameters that will be passed in to field's function. lookup_params = [geom or 'POINT (0 0)', 0] @@ -482,12 +433,12 @@ class GeoQuerySet(QuerySet): # If the spheroid calculation is desired, either by the `spheroid` # keyword or wehn calculating the length of geodetic field, make # sure the 'spheroid' distance setting string is passed in so we - # get the correct spatial stored procedure. - if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): - lookup_params.append('spheroid') + # get the correct spatial stored procedure. + if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): + lookup_params.append('spheroid') where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params) - # The `geom_args` flag is set to true if a geometry parameter was + # The `geom_args` flag is set to true if a geometry parameter was # passed in. geom_args = bool(geom) @@ -505,7 +456,7 @@ class GeoQuerySet(QuerySet): geodetic = unit_name in geo_field.geodetic_units else: geodetic = geo_field.geodetic - + if distance: if self.query.transformed_srid: # Setting the `geom_args` flag to false because we want to handle @@ -515,7 +466,7 @@ class GeoQuerySet(QuerySet): geom_args = False procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) if geom.srid is None or geom.srid == self.query.transformed_srid: - # If the geom parameter srid is None, it is assumed the coordinates + # If the geom parameter srid is None, it is assumed the coordinates # are in the transformed units. A placeholder is used for the # geometry parameter. procedure_fmt += ', %%s' @@ -529,10 +480,10 @@ class GeoQuerySet(QuerySet): if geodetic: # Spherical distance calculation is needed (because the geographic - # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() + # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() # procedures may only do queries from point columns to point geometries # some error checking is required. - if not isinstance(geo_field, PointField): + if not isinstance(geo_field, PointField): raise TypeError('Spherical distance calculation only supported on PointFields.') if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point': raise TypeError('Spherical distance calculation only supported with Point Geometry parameters') @@ -553,12 +504,12 @@ class GeoQuerySet(QuerySet): # Setting up the settings for `_spatial_attribute`. s = {'select_field' : DistanceField(dist_att), - 'setup' : False, + 'setup' : False, 'geo_field' : geo_field, 'procedure_args' : procedure_args, 'procedure_fmt' : procedure_fmt, } - if geom_args: + if geom_args: s['geom_args'] = ('geom',) s['procedure_args']['geom'] = geom elif geom: @@ -577,12 +528,12 @@ class GeoQuerySet(QuerySet): s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' s['procedure_args'] = {'tolerance' : tolerance} return self._spatial_attribute(func, s, **kwargs) - + def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs): """ DRY routine for setting up a GeoQuerySet method that attaches a Geometry attribute and takes a Geoemtry parameter. This is used - for geometry set-like operations (e.g., intersection, difference, + for geometry set-like operations (e.g., intersection, difference, union, sym_difference). """ s = {'geom_args' : ('geom',), @@ -595,16 +546,12 @@ class GeoQuerySet(QuerySet): s['procedure_args']['tolerance'] = tolerance return self._spatial_attribute(func, s, **kwargs) - def _geocol_select(self, geo_field, field_name, aggregate=False): + def _geocol_select(self, geo_field, field_name): """ Helper routine for constructing the SQL to select the geographic column. Takes into account if the geographic field is in a ForeignKey relation to the current model. """ - # If this is an aggregate spatial query, the flag needs to be - # set on the `GeoQuery` object of this queryset. - if aggregate: self.query.aggregate = True - opts = self.model._meta if not geo_field in opts.fields: # Is this operation going to be on a related geographic field? diff --git a/django/contrib/gis/db/models/sql/aggregates.py b/django/contrib/gis/db/models/sql/aggregates.py new file mode 100644 index 0000000000..ff76334249 --- /dev/null +++ b/django/contrib/gis/db/models/sql/aggregates.py @@ -0,0 +1,36 @@ +from django.db.models.sql.aggregates import * + +from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.backend import SpatialBackend + +if SpatialBackend.oracle: + geo_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))' +else: + geo_template = '%(function)s(%(field)s)' + +class GeoAggregate(Aggregate): + # Overriding the SQL template with the geographic one. + sql_template = geo_template + + is_extent = False + + def __init__(self, col, source=None, is_summary=False, **extra): + super(GeoAggregate, self).__init__(col, source, is_summary, **extra) + + # Can't use geographic aggregates on non-geometry fields. + if not isinstance(self.source, GeometryField): + raise ValueError('Geospatial aggregates only allowed on geometry fields.') + + # Making sure the SQL function is available for this spatial backend. + if not self.sql_function: + raise NotImplementedError('This aggregate functionality not implemented for your spatial backend.') + +class Extent(GeoAggregate): + is_extent = True + sql_function = SpatialBackend.extent + +class MakeLine(GeoAggregate): + sql_function = SpatialBackend.make_line + +class Union(GeoAggregate): + sql_function = SpatialBackend.unionagg diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index 52f521d500..246ea0300f 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -5,6 +5,7 @@ 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.where import GeoWhereNode from django.contrib.gis.measure import Area, Distance @@ -12,12 +13,35 @@ from django.contrib.gis.measure import Area, Distance ALL_TERMS = sql.constants.QUERY_TERMS.copy() ALL_TERMS.update(SpatialBackend.gis_terms) +# Conversion functions used in normalizing geographic aggregates. +if SpatialBackend.postgis: + def convert_extent(box): + # TODO: Parsing of BOX3D, Oracle support (patches welcome!) + # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; + # parsing out and returning as a 4-tuple. + ll, ur = box[4:-1].split(',') + xmin, ymin = map(float, ll.split()) + xmax, ymax = map(float, ur.split()) + return (xmin, ymin, xmax, ymax) + + def convert_geom(hex, geo_field): + if hex: return SpatialBackend.Geometry(hex) + else: return None +else: + def convert_extent(box): + raise NotImplementedError('Aggregate extent not implemented for this spatial backend.') + + def convert_geom(clob, geo_field): + if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid) + else: return None + class GeoQuery(sql.Query): """ A single spatial SQL query. """ # Overridding the valid query terms. query_terms = ALL_TERMS + aggregates_module = gis_aggregates_module #### Methods overridden from the base Query class #### def __init__(self, model, conn): @@ -25,7 +49,6 @@ class GeoQuery(sql.Query): # 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 = {} @@ -34,7 +57,6 @@ class GeoQuery(sql.Query): 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() @@ -50,12 +72,12 @@ class GeoQuery(sql.Query): (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 + 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)) + 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: @@ -67,38 +89,53 @@ class GeoQuery(sql.Query): 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) + 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, 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' % ( + 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. - 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) + 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 @@ -112,7 +149,7 @@ class GeoQuery(sql.Query): 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 + This routine is overridden from Query to handle customized selection of geometry columns. """ result = [] @@ -154,20 +191,10 @@ class GeoQuery(sql.Query): 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 + from extra selection SQL get resolved appropriately into Python objects. """ values = [] @@ -183,7 +210,7 @@ class GeoQuery(sql.Query): # 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)) + 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: # This is what happens normally in OracleQuery's `resolve_columns`. @@ -212,6 +239,19 @@ class GeoQuery(sql.Query): value = SpatialBackend.Geometry(value) return value + def resolve_aggregate(self, value, aggregate): + """ + Overridden from GeoQuery's normalize to handle the conversion of + GeoAggregate objects. + """ + if isinstance(aggregate, self.aggregates_module.GeoAggregate): + if aggregate.is_extent: + return convert_extent(value) + else: + return 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' @@ -222,9 +262,9 @@ class GeoQuery(sql.Query): 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 + 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) @@ -263,15 +303,15 @@ class GeoQuery(sql.Query): """ 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 + 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 + 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 + # 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. @@ -284,7 +324,7 @@ class GeoQuery(sql.Query): except (FieldDoesNotExist, IndexError): return False # TODO: ManyToManyField? - if isinstance(fld, GeometryField): + if isinstance(fld, GeometryField): return fld # A-OK. elif isinstance(fld, ForeignKey): # ForeignKey encountered, return the output of this utility called @@ -297,12 +337,12 @@ class GeoQuery(sql.Query): """ 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 + `"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), + return "%s.%s" % (self.quote_name_unless_alias(table_alias), self.connection.ops.quote_name(field.column)) def _geo_field(self, field_name=None): @@ -333,5 +373,5 @@ class DistanceField(object): # Rather than use GeometryField (which requires a SQL query # upon instantiation), use this lighter weight class. -class GeomField(object): +class GeomField(object): pass |
