web/lib/django/contrib/gis/db/models/sql/query.py
changeset 0 0d40e90630ef
child 29 cc9b7e14412b
equal deleted inserted replaced
-1:000000000000 0:0d40e90630ef
       
     1 from itertools import izip
       
     2 from django.db.models.query import sql
       
     3 from django.db.models.fields.related import ForeignKey
       
     4 
       
     5 from django.contrib.gis.db.backend import SpatialBackend
       
     6 from django.contrib.gis.db.models.fields import GeometryField
       
     7 from django.contrib.gis.db.models.sql import aggregates as gis_aggregates_module
       
     8 from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField
       
     9 from django.contrib.gis.db.models.sql.where import GeoWhereNode
       
    10 from django.contrib.gis.measure import Area, Distance
       
    11 
       
    12 # Valid GIS query types.
       
    13 ALL_TERMS = sql.constants.QUERY_TERMS.copy()
       
    14 ALL_TERMS.update(SpatialBackend.gis_terms)
       
    15 
       
    16 # Pulling out other needed constants/routines to avoid attribute lookups.
       
    17 TABLE_NAME = sql.constants.TABLE_NAME
       
    18 get_proxied_model = sql.query.get_proxied_model
       
    19 
       
    20 class GeoQuery(sql.Query):
       
    21     """
       
    22     A single spatial SQL query.
       
    23     """
       
    24     # Overridding the valid query terms.
       
    25     query_terms = ALL_TERMS
       
    26     aggregates_module = gis_aggregates_module
       
    27 
       
    28     #### Methods overridden from the base Query class ####
       
    29     def __init__(self, model, conn):
       
    30         super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
       
    31         # The following attributes are customized for the GeoQuerySet.
       
    32         # The GeoWhereNode and SpatialBackend classes contain backend-specific
       
    33         # routines and functions.
       
    34         self.custom_select = {}
       
    35         self.transformed_srid = None
       
    36         self.extra_select_fields = {}
       
    37 
       
    38     if SpatialBackend.oracle:
       
    39         # Have to override this so that GeoQuery, instead of OracleQuery,
       
    40         # is returned when unpickling.
       
    41         def __reduce__(self):
       
    42             callable, args, data = super(GeoQuery, self).__reduce__()
       
    43             return (unpickle_geoquery, (), data)
       
    44 
       
    45     def clone(self, *args, **kwargs):
       
    46         obj = super(GeoQuery, self).clone(*args, **kwargs)
       
    47         # Customized selection dictionary and transformed srid flag have
       
    48         # to also be added to obj.
       
    49         obj.custom_select = self.custom_select.copy()
       
    50         obj.transformed_srid = self.transformed_srid
       
    51         obj.extra_select_fields = self.extra_select_fields.copy()
       
    52         return obj
       
    53 
       
    54     def get_columns(self, with_aliases=False):
       
    55         """
       
    56         Return the list of columns to use in the select statement. If no
       
    57         columns have been specified, returns all columns relating to fields in
       
    58         the model.
       
    59 
       
    60         If 'with_aliases' is true, any column names that are duplicated
       
    61         (without the table names) are given unique aliases. This is needed in
       
    62         some cases to avoid ambiguitity with nested queries.
       
    63 
       
    64         This routine is overridden from Query to handle customized selection of
       
    65         geometry columns.
       
    66         """
       
    67         qn = self.quote_name_unless_alias
       
    68         qn2 = self.connection.ops.quote_name
       
    69         result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias))
       
    70                   for alias, col in self.extra_select.iteritems()]
       
    71         aliases = set(self.extra_select.keys())
       
    72         if with_aliases:
       
    73             col_aliases = aliases.copy()
       
    74         else:
       
    75             col_aliases = set()
       
    76         if self.select:
       
    77             only_load = self.deferred_to_columns()
       
    78             # This loop customized for GeoQuery.
       
    79             for col, field in izip(self.select, self.select_fields):
       
    80                 if isinstance(col, (list, tuple)):
       
    81                     alias, column = col
       
    82                     table = self.alias_map[alias][TABLE_NAME]
       
    83                     if table in only_load and col not in only_load[table]:
       
    84                         continue
       
    85                     r = self.get_field_select(field, alias, column)
       
    86                     if with_aliases:
       
    87                         if col[1] in col_aliases:
       
    88                             c_alias = 'Col%d' % len(col_aliases)
       
    89                             result.append('%s AS %s' % (r, c_alias))
       
    90                             aliases.add(c_alias)
       
    91                             col_aliases.add(c_alias)
       
    92                         else:
       
    93                             result.append('%s AS %s' % (r, qn2(col[1])))
       
    94                             aliases.add(r)
       
    95                             col_aliases.add(col[1])
       
    96                     else:
       
    97                         result.append(r)
       
    98                         aliases.add(r)
       
    99                         col_aliases.add(col[1])
       
   100                 else:
       
   101                     result.append(col.as_sql(quote_func=qn))
       
   102 
       
   103                     if hasattr(col, 'alias'):
       
   104                         aliases.add(col.alias)
       
   105                         col_aliases.add(col.alias)
       
   106 
       
   107         elif self.default_cols:
       
   108             cols, new_aliases = self.get_default_columns(with_aliases,
       
   109                     col_aliases)
       
   110             result.extend(cols)
       
   111             aliases.update(new_aliases)
       
   112 
       
   113         result.extend([
       
   114                 '%s%s' % (
       
   115                     self.get_extra_select_format(alias) % aggregate.as_sql(quote_func=qn),
       
   116                     alias is not None and ' AS %s' % alias or ''
       
   117                     )
       
   118                 for alias, aggregate in self.aggregate_select.items()
       
   119         ])
       
   120 
       
   121         # This loop customized for GeoQuery.
       
   122         for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
       
   123             r = self.get_field_select(field, table, col)
       
   124             if with_aliases and col in col_aliases:
       
   125                 c_alias = 'Col%d' % len(col_aliases)
       
   126                 result.append('%s AS %s' % (r, c_alias))
       
   127                 aliases.add(c_alias)
       
   128                 col_aliases.add(c_alias)
       
   129             else:
       
   130                 result.append(r)
       
   131                 aliases.add(r)
       
   132                 col_aliases.add(col)
       
   133 
       
   134         self._select_aliases = aliases
       
   135         return result
       
   136 
       
   137     def get_default_columns(self, with_aliases=False, col_aliases=None,
       
   138                             start_alias=None, opts=None, as_pairs=False):
       
   139         """
       
   140         Computes the default columns for selecting every field in the base
       
   141         model. Will sometimes be called to pull in related models (e.g. via
       
   142         select_related), in which case "opts" and "start_alias" will be given
       
   143         to provide a starting point for the traversal.
       
   144 
       
   145         Returns a list of strings, quoted appropriately for use in SQL
       
   146         directly, as well as a set of aliases used in the select statement (if
       
   147         'as_pairs' is True, returns a list of (alias, col_name) pairs instead
       
   148         of strings as the first component and None as the second component).
       
   149 
       
   150         This routine is overridden from Query to handle customized selection of
       
   151         geometry columns.
       
   152         """
       
   153         result = []
       
   154         if opts is None:
       
   155             opts = self.model._meta
       
   156         aliases = set()
       
   157         only_load = self.deferred_to_columns()
       
   158         # Skip all proxy to the root proxied model
       
   159         proxied_model = get_proxied_model(opts)
       
   160 
       
   161         if start_alias:
       
   162             seen = {None: start_alias}
       
   163         for field, model in opts.get_fields_with_model():
       
   164             if start_alias:
       
   165                 try:
       
   166                     alias = seen[model]
       
   167                 except KeyError:
       
   168                     if model is proxied_model:
       
   169                         alias = start_alias
       
   170                     else:
       
   171                         link_field = opts.get_ancestor_link(model)
       
   172                         alias = self.join((start_alias, model._meta.db_table,
       
   173                                            link_field.column, model._meta.pk.column))
       
   174                     seen[model] = alias
       
   175             else:
       
   176                 # If we're starting from the base model of the queryset, the
       
   177                 # aliases will have already been set up in pre_sql_setup(), so
       
   178                 # we can save time here.
       
   179                 alias = self.included_inherited_models[model]
       
   180             table = self.alias_map[alias][TABLE_NAME]
       
   181             if table in only_load and field.column not in only_load[table]:
       
   182                 continue
       
   183             if as_pairs:
       
   184                 result.append((alias, field.column))
       
   185                 aliases.add(alias)
       
   186                 continue
       
   187             # This part of the function is customized for GeoQuery. We
       
   188             # see if there was any custom selection specified in the
       
   189             # dictionary, and set up the selection format appropriately.
       
   190             field_sel = self.get_field_select(field, alias)
       
   191             if with_aliases and field.column in col_aliases:
       
   192                 c_alias = 'Col%d' % len(col_aliases)
       
   193                 result.append('%s AS %s' % (field_sel, c_alias))
       
   194                 col_aliases.add(c_alias)
       
   195                 aliases.add(c_alias)
       
   196             else:
       
   197                 r = field_sel
       
   198                 result.append(r)
       
   199                 aliases.add(r)
       
   200                 if with_aliases:
       
   201                     col_aliases.add(field.column)
       
   202         return result, aliases
       
   203 
       
   204     def resolve_columns(self, row, fields=()):
       
   205         """
       
   206         This routine is necessary so that distances and geometries returned
       
   207         from extra selection SQL get resolved appropriately into Python
       
   208         objects.
       
   209         """
       
   210         values = []
       
   211         aliases = self.extra_select.keys()
       
   212         if self.aggregates:
       
   213             # If we have an aggregate annotation, must extend the aliases
       
   214             # so their corresponding row values are included.
       
   215             aliases.extend([None for i in xrange(len(self.aggregates))])
       
   216 
       
   217         # Have to set a starting row number offset that is used for
       
   218         # determining the correct starting row index -- needed for
       
   219         # doing pagination with Oracle.
       
   220         rn_offset = 0
       
   221         if SpatialBackend.oracle:
       
   222             if self.high_mark is not None or self.low_mark: rn_offset = 1
       
   223         index_start = rn_offset + len(aliases)
       
   224 
       
   225         # Converting any extra selection values (e.g., geometries and
       
   226         # distance objects added by GeoQuerySet methods).
       
   227         values = [self.convert_values(v, self.extra_select_fields.get(a, None))
       
   228                   for v, a in izip(row[rn_offset:index_start], aliases)]
       
   229         if SpatialBackend.oracle or getattr(self, 'geo_values', False):
       
   230             # We resolve the rest of the columns if we're on Oracle or if
       
   231             # the `geo_values` attribute is defined.
       
   232             for value, field in izip(row[index_start:], fields):
       
   233                 values.append(self.convert_values(value, field))
       
   234         else:
       
   235             values.extend(row[index_start:])
       
   236         return tuple(values)
       
   237 
       
   238     def convert_values(self, value, field):
       
   239         """
       
   240         Using the same routines that Oracle does we can convert our
       
   241         extra selection objects into Geometry and Distance objects.
       
   242         TODO: Make converted objects 'lazy' for less overhead.
       
   243         """
       
   244         if SpatialBackend.oracle:
       
   245             # Running through Oracle's first.
       
   246             value = super(GeoQuery, self).convert_values(value, field or GeomField())
       
   247 
       
   248         if isinstance(field, DistanceField):
       
   249             # Using the field's distance attribute, can instantiate
       
   250             # `Distance` with the right context.
       
   251             value = Distance(**{field.distance_att : value})
       
   252         elif isinstance(field, AreaField):
       
   253             value = Area(**{field.area_att : value})
       
   254         elif isinstance(field, (GeomField, GeometryField)) and value:
       
   255             value = SpatialBackend.Geometry(value)
       
   256         return value
       
   257 
       
   258     def resolve_aggregate(self, value, aggregate):
       
   259         """
       
   260         Overridden from GeoQuery's normalize to handle the conversion of
       
   261         GeoAggregate objects.
       
   262         """
       
   263         if isinstance(aggregate, self.aggregates_module.GeoAggregate):
       
   264             if aggregate.is_extent:
       
   265                 return self.aggregates_module.convert_extent(value)
       
   266             else:
       
   267                 return self.aggregates_module.convert_geom(value, aggregate.source)
       
   268         else:
       
   269             return super(GeoQuery, self).resolve_aggregate(value, aggregate)
       
   270 
       
   271     #### Routines unique to GeoQuery ####
       
   272     def get_extra_select_format(self, alias):
       
   273         sel_fmt = '%s'
       
   274         if alias in self.custom_select:
       
   275             sel_fmt = sel_fmt % self.custom_select[alias]
       
   276         return sel_fmt
       
   277 
       
   278     def get_field_select(self, field, alias=None, column=None):
       
   279         """
       
   280         Returns the SELECT SQL string for the given field.  Figures out
       
   281         if any custom selection SQL is needed for the column  The `alias`
       
   282         keyword may be used to manually specify the database table where
       
   283         the column exists, if not in the model associated with this
       
   284         `GeoQuery`.  Similarly, `column` may be used to specify the exact
       
   285         column name, rather than using the `column` attribute on `field`.
       
   286         """
       
   287         sel_fmt = self.get_select_format(field)
       
   288         if field in self.custom_select:
       
   289             field_sel = sel_fmt % self.custom_select[field]
       
   290         else:
       
   291             field_sel = sel_fmt % self._field_column(field, alias, column)
       
   292         return field_sel
       
   293 
       
   294     def get_select_format(self, fld):
       
   295         """
       
   296         Returns the selection format string, depending on the requirements
       
   297         of the spatial backend.  For example, Oracle and MySQL require custom
       
   298         selection formats in order to retrieve geometries in OGC WKT. For all
       
   299         other fields a simple '%s' format string is returned.
       
   300         """
       
   301         if SpatialBackend.select and hasattr(fld, 'geom_type'):
       
   302             # This allows operations to be done on fields in the SELECT,
       
   303             # overriding their values -- used by the Oracle and MySQL
       
   304             # spatial backends to get database values as WKT, and by the
       
   305             # `transform` method.
       
   306             sel_fmt = SpatialBackend.select
       
   307 
       
   308             # Because WKT doesn't contain spatial reference information,
       
   309             # the SRID is prefixed to the returned WKT to ensure that the
       
   310             # transformed geometries have an SRID different than that of the
       
   311             # field -- this is only used by `transform` for Oracle and
       
   312             # SpatiaLite backends.
       
   313             if self.transformed_srid and ( SpatialBackend.oracle or
       
   314                                            SpatialBackend.spatialite ):
       
   315                 sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
       
   316         else:
       
   317             sel_fmt = '%s'
       
   318         return sel_fmt
       
   319 
       
   320     # Private API utilities, subject to change.
       
   321     def _field_column(self, field, table_alias=None, column=None):
       
   322         """
       
   323         Helper function that returns the database column for the given field.
       
   324         The table and column are returned (quoted) in the proper format, e.g.,
       
   325         `"geoapp_city"."point"`.  If `table_alias` is not specified, the
       
   326         database table associated with the model of this `GeoQuery` will be
       
   327         used.  If `column` is specified, it will be used instead of the value
       
   328         in `field.column`.
       
   329         """
       
   330         if table_alias is None: table_alias = self.model._meta.db_table
       
   331         return "%s.%s" % (self.quote_name_unless_alias(table_alias),
       
   332                           self.connection.ops.quote_name(column or field.column))
       
   333 
       
   334     def _geo_field(self, field_name=None):
       
   335         """
       
   336         Returns the first Geometry field encountered; or specified via the
       
   337         `field_name` keyword.  The `field_name` may be a string specifying
       
   338         the geometry field on this GeoQuery's model, or a lookup string
       
   339         to a geometry field via a ForeignKey relation.
       
   340         """
       
   341         if field_name is None:
       
   342             # Incrementing until the first geographic field is found.
       
   343             for fld in self.model._meta.fields:
       
   344                 if isinstance(fld, GeometryField): return fld
       
   345             return False
       
   346         else:
       
   347             # Otherwise, check by the given field name -- which may be
       
   348             # a lookup to a _related_ geographic field.
       
   349             return GeoWhereNode._check_geo_field(self.model._meta, field_name)
       
   350 
       
   351 if SpatialBackend.oracle:
       
   352     def unpickle_geoquery():
       
   353         """
       
   354         Utility function, called by Python's unpickling machinery, that handles
       
   355         unpickling of GeoQuery subclasses of OracleQuery.
       
   356         """
       
   357         return GeoQuery.__new__(GeoQuery)
       
   358     unpickle_geoquery.__safe_for_unpickling__ = True