Changeset 7512
- Timestamp:
- 05/01/08 13:17:50 (2 months ago)
- Files:
-
- django/branches/gis/django/contrib/gis/db/backend/__init__.py (modified) (3 diffs)
- django/branches/gis/django/contrib/gis/db/models/query.py (modified) (11 diffs)
- django/branches/gis/django/contrib/gis/db/models/sql (added)
- django/branches/gis/django/contrib/gis/db/models/sql/__init__.py (added)
- django/branches/gis/django/contrib/gis/db/models/sql/query.py (added)
- django/branches/gis/django/contrib/gis/db/models/sql/where.py (added)
- django/branches/gis/django/contrib/gis/tests/__init__.py (modified) (1 diff)
- django/branches/gis/django/contrib/gis/tests/relatedapp (added)
- django/branches/gis/django/contrib/gis/tests/relatedapp/__init__.py (added)
- django/branches/gis/django/contrib/gis/tests/relatedapp/models.py (added)
- django/branches/gis/django/contrib/gis/tests/relatedapp/tests_mysql.py (added)
- django/branches/gis/django/contrib/gis/tests/relatedapp/tests.py (added)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/branches/gis/django/contrib/gis/db/backend/__init__.py
r7482 r7512 3 3 4 4 Specifically, this module will import the correct routines and modules 5 needed for GeoDjango .5 needed for GeoDjango to interface with the spatial database. 6 6 7 Some of the more important classes and routines from the spatial backend 8 include: 9 7 10 (1) `GeoBackEndField`, a base class needed for GeometryField. 8 (2) `GeoWhereNode`, a subclass of `WhereNode` used to contruct spatial SQL. 9 (3) `SpatialBackend`, a container object for information specific to the 11 (2) `get_geo_where_clause`, a routine used by `GeoWhereNode`. 12 (3) `GIS_TERMS`, a listing of all valid GeoDjango lookup types. 13 (4) `SpatialBackend`, a container object for information specific to the 10 14 spatial backend. 11 15 """ 12 16 from django.conf import settings 13 17 from django.db.models.sql.query import QUERY_TERMS 14 from django.db.models.sql.where import WhereNode15 18 from django.contrib.gis.db.backend.util import gqn 16 19 … … 62 65 raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) 63 66 64 class GeoWhereNode(WhereNode):65 """66 The GeoWhereNode calls the `get_geo_where_clause` from the appropriate67 spatial backend in order to construct correct spatial SQL.68 """69 def make_atom(self, child, qn):70 table_alias, name, field, lookup_type, value = child71 if hasattr(field, '_geom'):72 if lookup_type in GIS_TERMS:73 # Getting the geographic where clause; substitution parameters74 # will be populated in the GeoFieldSQL object returned by the75 # GeometryField.76 gwc = get_geo_where_clause(lookup_type, table_alias, field, value)77 where, params = field.get_db_prep_lookup(lookup_type, value)78 return gwc % tuple(where), params79 else:80 raise TypeError('Invalid lookup type: %r' % lookup_type)81 else:82 # If not a GeometryField, call the `make_atom` from the83 # base class.84 return super(GeoWhereNode, self).make_atom(child, qn)85 86 67 class SpatialBackend(object): 87 68 "A container for properties of the SpatialBackend." … … 107 88 limited_where = LIMITED_WHERE 108 89 90 # Shortcut booleans. 91 mysql = SPATIAL_BACKEND == 'mysql' 92 oracle = SPATIAL_BACKEND == 'oracle' 93 postgis = SPATIAL_BACKEND == 'postgis' 94 109 95 # Class for the backend field. 110 96 Field = GeoBackendField django/branches/gis/django/contrib/gis/db/models/query.py
r7482 r7512 1 from itertools import izip 1 2 from django.core.exceptions import ImproperlyConfigured 2 3 from django.db import connection 3 4 from django.db.models.query import sql, QuerySet, Q 4 from django.db.models.fields import FieldDoesNotExist 5 from django.contrib.gis.db.backend import gqn, GeoWhereNode, SpatialBackend, QUERY_TERMS5 6 from django.contrib.gis.db.backend import SpatialBackend 6 7 from django.contrib.gis.db.models.fields import GeometryField, PointField 8 from django.contrib.gis.db.models.sql import GeoQuery, GeoWhereNode 7 9 from django.contrib.gis.geos import GEOSGeometry, Point 8 9 # Aliases.10 10 qn = connection.ops.quote_name 11 oracle = SpatialBackend.name == 'oracle'12 postgis = SpatialBackend.name == 'postgis'13 14 # All valid lookup terms.15 ALL_TERMS = QUERY_TERMS.copy()16 ALL_TERMS.update(dict((term, None) for term in SpatialBackend.gis_terms))17 11 18 12 # For backwards-compatibility; Q object should work just fine 19 # usingqueryset-refactor.13 # after queryset-refactor. 20 14 class GeoQ(Q): pass 21 15 … … 27 21 def as_sql(self, *args, **kwargs): 28 22 return self.sql 29 30 # Getting the `Query` base class from the backend (needed specifically31 # for Oracle backends).32 Query = QuerySet().query.__class__33 34 class GeoQuery(Query):35 "The Geographic Query, needed to construct spatial SQL."36 37 # Overridding the valid query terms.38 query_terms = ALL_TERMS39 40 #### Methods overridden from the base Query class ####41 def __init__(self, model, conn):42 super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)43 # The following attributes are customized for the GeoQuerySet.44 # The GeoWhereNode and SpatialBackend classes contain backend-specific45 # routines and functions.46 self.custom_select = {}47 self.ewkt = None48 49 def clone(self, *args, **kwargs):50 obj = super(GeoQuery, self).clone(*args, **kwargs)51 # Customized selection dictionary and EWKT flag have to be added to obj.52 obj.custom_select = self.custom_select.copy()53 obj.ewkt = self.ewkt54 return obj55 56 def get_default_columns(self, with_aliases=False, col_aliases=None):57 """58 Computes the default columns for selecting every field in the base59 model.60 61 Returns a list of strings, quoted appropriately for use in SQL62 directly, as well as a set of aliases used in the select statement.63 64 This routine is overridden from Query to handle customized selection of65 geometry columns.66 """67 result = []68 table_alias = self.tables[0]69 root_pk = self.model._meta.pk.column70 seen = {None: table_alias}71 qn = self.quote_name_unless_alias72 qn2 = self.connection.ops.quote_name73 aliases = set()74 for field, model in self.model._meta.get_fields_with_model():75 try:76 alias = seen[model]77 except KeyError:78 alias = self.join((table_alias, model._meta.db_table,79 root_pk, model._meta.pk.column))80 seen[model] = alias81 82 # This part of the function is customized for GeoQuerySet. We83 # see if there was any custom selection specified in the84 # dictionary, and set up the selection format appropriately.85 sel_fmt = self.get_select_format(field)86 if field.column in self.custom_select:87 field_sel = sel_fmt % self.custom_select[field.column]88 else:89 field_sel = sel_fmt % self._field_column(field, alias)90 91 if with_aliases and field.column in col_aliases:92 c_alias = 'Col%d' % len(col_aliases)93 result.append('%s AS %s' % (field_sel, c_alias))94 col_aliases.add(c_alias)95 aliases.add(c_alias)96 else:97 r = field_sel98 result.append(r)99 aliases.add(r)100 if with_aliases:101 col_aliases.add(field.column)102 return result, aliases103 104 #### Routines unique to GeoQuery ####105 def get_select_format(self, fld):106 """107 Returns the selection format string, depending on the requirements108 of the spatial backend. For example, Oracle and MySQL require custom109 selection formats in order to retrieve geometries in OGC WKT. For all110 other fields a simple '%s' format string is returned.111 """112 if SpatialBackend.select and hasattr(fld, '_geom'):113 # This allows operations to be done on fields in the SELECT,114 # overriding their values -- used by the Oracle and MySQL115 # spatial backends to get database values as WKT, and by the116 # `transform` method.117 sel_fmt = SpatialBackend.select118 119 # Because WKT doesn't contain spatial reference information,120 # the SRID is prefixed to the returned WKT to ensure that the121 # transformed geometries have an SRID different than that of the122 # field -- this is only used by `transform` for Oracle backends.123 if self.ewkt and oracle:124 sel_fmt = "'SRID=%d;'||%s" % (self.ewkt, sel_fmt)125 else:126 sel_fmt = '%s'127 return sel_fmt128 129 def _field_column(self, field, table_alias=None):130 """131 Helper function that returns the database column for the given field.132 The table and column are returned (quoted) in the proper format, e.g.,133 `"geoapp_city"."point"`.134 """135 if table_alias is None: table_alias = self.model._meta.db_table136 return "%s.%s" % (self.quote_name_unless_alias(table_alias), qn(field.column))137 138 def _geo_field(self, field_name=None):139 """140 Returns the first Geometry field encountered; or specified via the141 `field_name` keyword.142 """143 for field in self.model._meta.fields:144 if isinstance(field, GeometryField):145 fname = field.name146 if field_name:147 if field_name == field.name: return field148 else:149 return field150 return False151 23 152 24 class GeoQuerySet(QuerySet): … … 188 60 # `distance_lte` lookup type. 189 61 where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0)) 190 if oracle:62 if SpatialBackend.oracle: 191 63 # The `tolerance` keyword may be used for Oracle; the tolerance is 192 64 # in meters -- a default of 5 centimeters is used. … … 230 102 231 103 self.query.select = [GeomSQL(extent_sql)] 104 self.query.select_fields = [None] 232 105 try: 233 106 esql, params = self.query.as_sql() … … 268 141 geo_col = self.query._field_column(geo_field) 269 142 270 if oracle:143 if SpatialBackend.oracle: 271 144 gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)} 272 elif postgis:145 elif SpatialBackend.postgis: 273 146 # PostGIS AsGML() aggregate function parameter order depends on the 274 147 # version -- uggh. … … 296 169 if not geo_field: 297 170 raise TypeError('KML output only available on GeometryFields.') 171 298 172 geo_col = self.query._field_column(geo_field) 299 173 300 174 # Adding the AsKML function call to SELECT part of the SQL. 301 175 return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)}) 302 176 303 177 def transform(self, field_name=None, srid=4326): 304 178 """ … … 321 195 if not geo_field: 322 196 raise TypeError('%s() only available for GeometryFields' % TRANSFORM) 323 197 198 # Getting the selection SQL for the given geograph 199 field_col = self._geocol_select(geo_field, field_name) 200 324 201 # Why cascading substitutions? Because spatial backends like 325 202 # Oracle and MySQL already require a function call to convert to text, thus 326 203 # when there's also a transformation we need to cascade the substitutions. 327 204 # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' 328 geo_col = self.query.custom_select.get(geo_field .column, self.query._field_column(geo_field))205 geo_col = self.query.custom_select.get(geo_field, field_col) 329 206 330 207 # Setting the key for the field's column with the custom SELECT SQL to 331 208 # override the geometry column returned from the database. 332 if oracle:209 if SpatialBackend.oracle: 333 210 custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid) 334 211 self.query.ewkt = srid 335 212 else: 336 custom_sel = '(%s(%s, %s)) AS %s' % \ 337 (TRANSFORM, geo_col, srid, qn(geo_field.column)) 338 self.query.custom_select[geo_field.column] = custom_sel 213 custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid) 214 self.query.custom_select[geo_field] = custom_sel 339 215 return self._clone() 340 216 … … 358 234 # Replacing the select with a call to the ST_Union stored procedure 359 235 # on the geographic field column. 360 if oracle:236 if SpatialBackend.oracle: 361 237 union_sql = '%s' % SpatialBackend.select 362 238 union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance)) … … 366 242 # Only want the union SQL to be selected. 367 243 self.query.select = [GeomSQL(union_sql)] 244 self.query.select_fields = [GeometryField] 368 245 try: 369 246 usql, params = self.query.as_sql() … … 374 251 cursor = connection.cursor() 375 252 cursor.execute(usql, params) 376 if oracle:253 if SpatialBackend.oracle: 377 254 # On Oracle have to read out WKT from CLOB first. 378 255 clob = cursor.fetchone()[0] … … 384 261 if u: return GEOSGeometry(u) 385 262 else: return None 263 264 # Private API utilities, subject to change. 265 def _geocol_select(self, geo_field, field_name): 266 """ 267 Helper routine for constructing the SQL to select the geographic 268 column. Takes into account if the geographic field is in a 269 ForeignKey relation to the current model. 270 """ 271 # Is this operation going to be on a related geographic field? 272 if not geo_field in self.model._meta.fields: 273 # If so, it'll have to be added to the select related information 274 # (e.g., if 'location__point' was given as the field name). 275 self.query.add_select_related([field_name]) 276 self.query.pre_sql_setup() 277 rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] 278 return self.query._field_column(geo_field, rel_table) 279 else: 280 return self.query._field_column(geo_field) django/branches/gis/django/contrib/gis/tests/__init__.py
r6918 r7512 12 12 13 13 # Tests that require use of a spatial database (e.g., creation of models) 14 test_models = ['geoapp' ]14 test_models = ['geoapp', 'relatedapp'] 15 15 16 16 # Tests that do not require setting up and tearing down a spatial database.
