web/lib/django/contrib/gis/db/models/sql/compiler.py
changeset 38 77b6da96e6f1
parent 29 cc9b7e14412b
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 from itertools import izip
       
     2 from django.db.backends.util import truncate_name
       
     3 from django.db.models.sql import compiler
       
     4 from django.db.models.sql.constants import TABLE_NAME
       
     5 from django.db.models.sql.query import get_proxied_model
       
     6 
       
     7 SQLCompiler = compiler.SQLCompiler
       
     8 
       
     9 class GeoSQLCompiler(compiler.SQLCompiler):
       
    10 
       
    11     def get_columns(self, with_aliases=False):
       
    12         """
       
    13         Return the list of columns to use in the select statement. If no
       
    14         columns have been specified, returns all columns relating to fields in
       
    15         the model.
       
    16 
       
    17         If 'with_aliases' is true, any column names that are duplicated
       
    18         (without the table names) are given unique aliases. This is needed in
       
    19         some cases to avoid ambiguitity with nested queries.
       
    20 
       
    21         This routine is overridden from Query to handle customized selection of
       
    22         geometry columns.
       
    23         """
       
    24         qn = self.quote_name_unless_alias
       
    25         qn2 = self.connection.ops.quote_name
       
    26         result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias))
       
    27                   for alias, col in self.query.extra_select.iteritems()]
       
    28         aliases = set(self.query.extra_select.keys())
       
    29         if with_aliases:
       
    30             col_aliases = aliases.copy()
       
    31         else:
       
    32             col_aliases = set()
       
    33         if self.query.select:
       
    34             only_load = self.deferred_to_columns()
       
    35             # This loop customized for GeoQuery.
       
    36             for col, field in izip(self.query.select, self.query.select_fields):
       
    37                 if isinstance(col, (list, tuple)):
       
    38                     alias, column = col
       
    39                     table = self.query.alias_map[alias][TABLE_NAME]
       
    40                     if table in only_load and col not in only_load[table]:
       
    41                         continue
       
    42                     r = self.get_field_select(field, alias, column)
       
    43                     if with_aliases:
       
    44                         if col[1] in col_aliases:
       
    45                             c_alias = 'Col%d' % len(col_aliases)
       
    46                             result.append('%s AS %s' % (r, c_alias))
       
    47                             aliases.add(c_alias)
       
    48                             col_aliases.add(c_alias)
       
    49                         else:
       
    50                             result.append('%s AS %s' % (r, qn2(col[1])))
       
    51                             aliases.add(r)
       
    52                             col_aliases.add(col[1])
       
    53                     else:
       
    54                         result.append(r)
       
    55                         aliases.add(r)
       
    56                         col_aliases.add(col[1])
       
    57                 else:
       
    58                     result.append(col.as_sql(qn, self.connection))
       
    59                     
       
    60                     if hasattr(col, 'alias'):
       
    61                         aliases.add(col.alias)
       
    62                         col_aliases.add(col.alias)
       
    63 
       
    64         elif self.query.default_cols:
       
    65             cols, new_aliases = self.get_default_columns(with_aliases,
       
    66                     col_aliases)
       
    67             result.extend(cols)
       
    68             aliases.update(new_aliases)
       
    69 
       
    70         max_name_length = self.connection.ops.max_name_length()
       
    71         result.extend([
       
    72                 '%s%s' % (
       
    73                     self.get_extra_select_format(alias) % aggregate.as_sql(qn, self.connection),
       
    74                     alias is not None
       
    75                         and ' AS %s' % qn(truncate_name(alias, max_name_length))
       
    76                         or ''
       
    77                     )
       
    78                 for alias, aggregate in self.query.aggregate_select.items()
       
    79         ])
       
    80 
       
    81         # This loop customized for GeoQuery.
       
    82         for (table, col), field in izip(self.query.related_select_cols, self.query.related_select_fields):
       
    83             r = self.get_field_select(field, table, col)
       
    84             if with_aliases and col in col_aliases:
       
    85                 c_alias = 'Col%d' % len(col_aliases)
       
    86                 result.append('%s AS %s' % (r, c_alias))
       
    87                 aliases.add(c_alias)
       
    88                 col_aliases.add(c_alias)
       
    89             else:
       
    90                 result.append(r)
       
    91                 aliases.add(r)
       
    92                 col_aliases.add(col)
       
    93 
       
    94         self._select_aliases = aliases
       
    95         return result
       
    96 
       
    97     def get_default_columns(self, with_aliases=False, col_aliases=None,
       
    98                             start_alias=None, opts=None, as_pairs=False):
       
    99         """
       
   100         Computes the default columns for selecting every field in the base
       
   101         model. Will sometimes be called to pull in related models (e.g. via
       
   102         select_related), in which case "opts" and "start_alias" will be given
       
   103         to provide a starting point for the traversal.
       
   104 
       
   105         Returns a list of strings, quoted appropriately for use in SQL
       
   106         directly, as well as a set of aliases used in the select statement (if
       
   107         'as_pairs' is True, returns a list of (alias, col_name) pairs instead
       
   108         of strings as the first component and None as the second component).
       
   109 
       
   110         This routine is overridden from Query to handle customized selection of
       
   111         geometry columns.
       
   112         """
       
   113         result = []
       
   114         if opts is None:
       
   115             opts = self.query.model._meta
       
   116         aliases = set()
       
   117         only_load = self.deferred_to_columns()
       
   118         # Skip all proxy to the root proxied model
       
   119         proxied_model = get_proxied_model(opts)
       
   120 
       
   121         if start_alias:
       
   122             seen = {None: start_alias}
       
   123         for field, model in opts.get_fields_with_model():
       
   124             if start_alias:
       
   125                 try:
       
   126                     alias = seen[model]
       
   127                 except KeyError:
       
   128                     if model is proxied_model:
       
   129                         alias = start_alias
       
   130                     else:
       
   131                         link_field = opts.get_ancestor_link(model)
       
   132                         alias = self.query.join((start_alias, model._meta.db_table,
       
   133                                 link_field.column, model._meta.pk.column))
       
   134                     seen[model] = alias
       
   135             else:
       
   136                 # If we're starting from the base model of the queryset, the
       
   137                 # aliases will have already been set up in pre_sql_setup(), so
       
   138                 # we can save time here.
       
   139                 alias = self.query.included_inherited_models[model]
       
   140             table = self.query.alias_map[alias][TABLE_NAME]
       
   141             if table in only_load and field.column not in only_load[table]:
       
   142                 continue
       
   143             if as_pairs:
       
   144                 result.append((alias, field.column))
       
   145                 aliases.add(alias)
       
   146                 continue
       
   147             # This part of the function is customized for GeoQuery. We
       
   148             # see if there was any custom selection specified in the
       
   149             # dictionary, and set up the selection format appropriately.
       
   150             field_sel = self.get_field_select(field, alias)
       
   151             if with_aliases and field.column in col_aliases:
       
   152                 c_alias = 'Col%d' % len(col_aliases)
       
   153                 result.append('%s AS %s' % (field_sel, c_alias))
       
   154                 col_aliases.add(c_alias)
       
   155                 aliases.add(c_alias)
       
   156             else:
       
   157                 r = field_sel
       
   158                 result.append(r)
       
   159                 aliases.add(r)
       
   160                 if with_aliases:
       
   161                     col_aliases.add(field.column)
       
   162         return result, aliases
       
   163 
       
   164     def resolve_columns(self, row, fields=()):
       
   165         """
       
   166         This routine is necessary so that distances and geometries returned
       
   167         from extra selection SQL get resolved appropriately into Python
       
   168         objects.
       
   169         """
       
   170         values = []
       
   171         aliases = self.query.extra_select.keys()
       
   172         if self.query.aggregates:
       
   173             # If we have an aggregate annotation, must extend the aliases
       
   174             # so their corresponding row values are included.
       
   175             aliases.extend([None for i in xrange(len(self.query.aggregates))])
       
   176 
       
   177         # Have to set a starting row number offset that is used for
       
   178         # determining the correct starting row index -- needed for
       
   179         # doing pagination with Oracle.
       
   180         rn_offset = 0
       
   181         if self.connection.ops.oracle:
       
   182             if self.query.high_mark is not None or self.query.low_mark: rn_offset = 1
       
   183         index_start = rn_offset + len(aliases)
       
   184 
       
   185         # Converting any extra selection values (e.g., geometries and
       
   186         # distance objects added by GeoQuerySet methods).
       
   187         values = [self.query.convert_values(v,
       
   188                                self.query.extra_select_fields.get(a, None),
       
   189                                self.connection)
       
   190                   for v, a in izip(row[rn_offset:index_start], aliases)]
       
   191         if self.connection.ops.oracle or getattr(self.query, 'geo_values', False):
       
   192             # We resolve the rest of the columns if we're on Oracle or if
       
   193             # the `geo_values` attribute is defined.
       
   194             for value, field in map(None, row[index_start:], fields):
       
   195                 values.append(self.query.convert_values(value, field, connection=self.connection))
       
   196         else:
       
   197             values.extend(row[index_start:])
       
   198         return tuple(values)
       
   199 
       
   200     #### Routines unique to GeoQuery ####
       
   201     def get_extra_select_format(self, alias):
       
   202         sel_fmt = '%s'
       
   203         if alias in self.query.custom_select:
       
   204             sel_fmt = sel_fmt % self.query.custom_select[alias]
       
   205         return sel_fmt
       
   206 
       
   207     def get_field_select(self, field, alias=None, column=None):
       
   208         """
       
   209         Returns the SELECT SQL string for the given field.  Figures out
       
   210         if any custom selection SQL is needed for the column  The `alias`
       
   211         keyword may be used to manually specify the database table where
       
   212         the column exists, if not in the model associated with this
       
   213         `GeoQuery`.  Similarly, `column` may be used to specify the exact
       
   214         column name, rather than using the `column` attribute on `field`.
       
   215         """
       
   216         sel_fmt = self.get_select_format(field)
       
   217         if field in self.query.custom_select:
       
   218             field_sel = sel_fmt % self.query.custom_select[field]
       
   219         else:
       
   220             field_sel = sel_fmt % self._field_column(field, alias, column)
       
   221         return field_sel
       
   222 
       
   223     def get_select_format(self, fld):
       
   224         """
       
   225         Returns the selection format string, depending on the requirements
       
   226         of the spatial backend.  For example, Oracle and MySQL require custom
       
   227         selection formats in order to retrieve geometries in OGC WKT. For all
       
   228         other fields a simple '%s' format string is returned.
       
   229         """
       
   230         if self.connection.ops.select and hasattr(fld, 'geom_type'):
       
   231             # This allows operations to be done on fields in the SELECT,
       
   232             # overriding their values -- used by the Oracle and MySQL
       
   233             # spatial backends to get database values as WKT, and by the
       
   234             # `transform` method.
       
   235             sel_fmt = self.connection.ops.select
       
   236 
       
   237             # Because WKT doesn't contain spatial reference information,
       
   238             # the SRID is prefixed to the returned WKT to ensure that the
       
   239             # transformed geometries have an SRID different than that of the
       
   240             # field -- this is only used by `transform` for Oracle and
       
   241             # SpatiaLite backends.
       
   242             if self.query.transformed_srid and ( self.connection.ops.oracle or
       
   243                                                  self.connection.ops.spatialite ):
       
   244                 sel_fmt = "'SRID=%d;'||%s" % (self.query.transformed_srid, sel_fmt)
       
   245         else:
       
   246             sel_fmt = '%s'
       
   247         return sel_fmt
       
   248 
       
   249     # Private API utilities, subject to change.
       
   250     def _field_column(self, field, table_alias=None, column=None):
       
   251         """
       
   252         Helper function that returns the database column for the given field.
       
   253         The table and column are returned (quoted) in the proper format, e.g.,
       
   254         `"geoapp_city"."point"`.  If `table_alias` is not specified, the
       
   255         database table associated with the model of this `GeoQuery` will be
       
   256         used.  If `column` is specified, it will be used instead of the value
       
   257         in `field.column`.
       
   258         """
       
   259         if table_alias is None: table_alias = self.query.model._meta.db_table
       
   260         return "%s.%s" % (self.quote_name_unless_alias(table_alias),
       
   261                           self.connection.ops.quote_name(column or field.column))
       
   262 
       
   263 class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler):
       
   264     pass
       
   265 
       
   266 class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler):
       
   267     pass
       
   268 
       
   269 class SQLUpdateCompiler(compiler.SQLUpdateCompiler, GeoSQLCompiler):
       
   270     pass
       
   271 
       
   272 class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler):
       
   273     pass
       
   274 
       
   275 class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler):
       
   276     pass