Changeset 5776
- Timestamp:
- 07/29/07 14:58:24 (1 year ago)
- Files:
-
- django/branches/gis/django/contrib/gis/db/backend (added)
- django/branches/gis/django/contrib/gis/db/backend/__init__.py (added)
- django/branches/gis/django/contrib/gis/db/backend/postgis (added)
- django/branches/gis/django/contrib/gis/db/backend/postgis/creation.py (moved) (moved from django/branches/gis/django/contrib/gis/utils/spatial_db.py)
- django/branches/gis/django/contrib/gis/db/backend/postgis/field.py (added)
- django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py (added)
- django/branches/gis/django/contrib/gis/db/backend/postgis/management.py (added)
- django/branches/gis/django/contrib/gis/db/backend/postgis/query.py (moved) (moved from django/branches/gis/django/contrib/gis/db/models/postgis.py) (6 diffs)
- django/branches/gis/django/contrib/gis/db/models/fields/__init__.py (modified) (3 diffs)
- django/branches/gis/django/contrib/gis/db/models/query.py (modified) (1 diff)
- django/branches/gis/django/contrib/gis/tests/geoapp/tests.py (modified) (1 diff)
- django/branches/gis/django/contrib/gis/tests/__init__.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/branches/gis/django/contrib/gis/db/backend/postgis/query.py
r5773 r5776 1 # This module is meant to re-define the helper routines used by the 2 # django.db.models.query objects to be customized for PostGIS. 1 """ 2 This module contains the spatial lookup types, and the get_geo_where_clause() 3 routine for PostGIS. 4 """ 3 5 from django.db import backend 4 from django.db.models.query import LOOKUP_SEPARATOR, field_choices, find_field, FieldFound, QUERY_TERMS, get_where_clause 5 from django.utils.datastructures import SortedDict 6 from management import postgis_lib_version 7 8 # Getting the PostGIS version 9 POSTGIS_VERSION = postgis_lib_version() 10 MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = map(int, POSTGIS_VERSION.split('.')) 11 12 # The supported PostGIS versions. 13 # TODO: Confirm tests with PostGIS versions 1.1.x -- should work. Versions <= 1.0.x didn't use GEOS C API. 14 if MAJOR_VERSION != 1 or MINOR_VERSION1 <= 1: 15 raise Exception, 'PostGIS version %s not supported.' % POSTGIS_VERSION 6 16 7 17 # PostGIS-specific operators. The commented descriptions of these … … 9 19 POSTGIS_OPERATORS = { 10 20 # The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box. 11 'overlaps_left' : '&< %s',21 'overlaps_left' : '&<', 12 22 # The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box. 13 'overlaps_right' : '&> %s',23 'overlaps_right' : '&>', 14 24 # The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box. 15 'left' : '<< %s',25 'left' : '<<', 16 26 # The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box. 17 'right' : '>> %s',27 'right' : '>>', 18 28 # The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box. 19 'overlaps_below' : '&<| %s',29 'overlaps_below' : '&<|', 20 30 # The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box. 21 'overlaps_above' : '|&> %s',31 'overlaps_above' : '|&>', 22 32 # The "<<|" operator returns true if A's bounding box is strictly below B's bounding box. 23 'strictly_below' : '<<| %s',33 'strictly_below' : '<<|', 24 34 # The "|>>" operator returns true if A's bounding box is strictly above B's bounding box. 25 'strictly_above' : '|>> %s',35 'strictly_above' : '|>>', 26 36 # The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if 27 37 # A and B are the same feature, vertex-by-vertex, the operator returns true. 28 'same_as' : '~= %s',29 'exact' : '~= %s',38 'same_as' : '~=', 39 'exact' : '~=', 30 40 # The "@" operator returns true if A's bounding box is completely contained by B's bounding box. 31 'contained' : '@ %s',41 'contained' : '@', 32 42 # The "~" operator returns true if A's bounding box completely contains B's bounding box. 33 'bbcontains' : '~ %s',43 'bbcontains' : '~', 34 44 # The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the 35 45 # operator returns true. 36 'bboverlaps' : '&& %s',46 'bboverlaps' : '&&', 37 47 } 38 48 39 # PostGIS Geometry Functions -- most of these use GEOS. 49 # PostGIS Geometry Relationship Functions -- most of these use GEOS. 50 # 51 # For PostGIS >= 1.2.2 these routines will do a bounding box query first before calling 52 # the more expensive GEOS routines (called 'inline index magic'). 53 # 40 54 POSTGIS_GEOMETRY_FUNCTIONS = { 41 #'distance' : 'Distance', -- doesn't work right now. 42 'equals' : 'Equals', 43 'disjoint' : 'Disjoint', 44 'touches' : 'Touches', 45 'crosses' : 'Crosses', 46 'within' : 'Within', 47 'overlaps' : 'Overlaps', 48 'contains' : 'Contains', 49 'intersects' : 'Intersects', 50 'relate' : 'Relate', 55 'equals' : '%sEquals', 56 'disjoint' : '%sDisjoint', 57 'touches' : '%sTouches', 58 'crosses' : '%sCrosses', 59 'within' : '%sWithin', 60 'overlaps' : '%sOverlaps', 61 'contains' : '%sContains', 62 'intersects' : '%sIntersects', 63 'relate' : ('%sRelate', str), 51 64 } 65 66 # Versions of PostGIS >= 1.2.2 changed their naming convention to be 'SQL-MM-centric'. 67 # Practically, this means that 'ST_' is appended to geometry function names. 68 if MINOR_VERSION1 >= 2 and MINOR_VERSION2 >= 2: 69 # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2. 70 POSTGIS_GEOMETRY_FUNCTIONS.update( 71 {'dwithin' : ('%sDWithin', float), 72 'coveredby' : '%sCoveredBy', 73 'covers' : '%sCovers', 74 } 75 ) 76 GEOM_FUNC_PREFIX = 'ST_' 77 else: 78 GEOM_FUNC_PREFIX = '' 79 80 # Updating with the geometry function prefix. 81 for k, v in POSTGIS_GEOMETRY_FUNCTIONS.items(): 82 if isinstance(v, tuple): 83 v = list(v) 84 v[0] = v[0] % GEOM_FUNC_PREFIX 85 v = tuple(v) 86 else: 87 v = v % GEOM_FUNC_PREFIX 88 POSTGIS_GEOMETRY_FUNCTIONS[k] = v 52 89 53 90 # Any other lookup types that do not require a mapping. … … 59 96 else: return "'%s'" % value 60 97 61 # These are the PostGIS-customized QUERY_TERMS , combines both the operators62 # and the geometry functions.98 # These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types 99 # allowed for geographic queries. 63 100 POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first 64 101 POSTGIS_TERMS += list(POSTGIS_GEOMETRY_FUNCTIONS.keys()) # Adding on the Geometry Functions … … 67 104 68 105 def get_geo_where_clause(lookup_type, table_prefix, field_name, value): 106 "Returns the SQL WHERE clause for use in PostGIS SQL construction." 69 107 if table_prefix.endswith('.'): 70 108 table_prefix = backend.quote_name(table_prefix[:-1])+'.' … … 73 111 # See if a PostGIS operator matches the lookup type first 74 112 try: 75 return '%s%s %s ' % (table_prefix, field_name, (POSTGIS_OPERATORS[lookup_type] % '%s'))113 return '%s%s %s %%s' % (table_prefix, field_name, POSTGIS_OPERATORS[lookup_type]) 76 114 except KeyError: 77 115 pass … … 83 121 pass 84 122 85 # For any otherlookup type123 # Handling 'isnull' lookup type 86 124 if lookup_type == 'isnull': 87 125 return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) 88 126 89 127 raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) 90 91 #### query.py overloaded functions ####92 # parse_lookup() and lookup_inner() are modified from their django/db/models/query.py93 # counterparts to support constructing SQL for geographic queries.94 #95 # Status: Synced with r5609.96 #97 def parse_lookup(kwarg_items, opts):98 # Helper function that handles converting API kwargs99 # (e.g. "name__exact": "tom") to SQL.100 # Returns a tuple of (joins, where, params).101 102 # 'joins' is a sorted dictionary describing the tables that must be joined103 # to complete the query. The dictionary is sorted because creation order104 # is significant; it is a dictionary to ensure uniqueness of alias names.105 #106 # Each key-value pair follows the form107 # alias: (table, join_type, condition)108 # where109 # alias is the AS alias for the joined table110 # table is the actual table name to be joined111 # join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc)112 # condition is the where-like statement over which narrows the join.113 # alias will be derived from the lookup list name.114 #115 # At present, this method only every returns INNER JOINs; the option is116 # there for others to implement custom Q()s, etc that return other join117 # types.118 joins, where, params = SortedDict(), [], []119 120 for kwarg, value in kwarg_items:121 path = kwarg.split(LOOKUP_SEPARATOR)122 # Extract the last elements of the kwarg.123 # The very-last is the lookup_type (equals, like, etc).124 # The second-last is the table column on which the lookup_type is125 # to be performed. If this name is 'pk', it will be substituted with126 # the name of the primary key.127 # If there is only one part, or the last part is not a query128 # term, assume that the query is an __exact129 lookup_type = path.pop()130 if lookup_type == 'pk':131 lookup_type = 'exact'132 path.append(None)133 elif len(path) == 0 or not ((lookup_type in QUERY_TERMS) or (lookup_type in POSTGIS_TERMS)):134 path.append(lookup_type)135 lookup_type = 'exact'136 137 if len(path) < 1:138 raise TypeError, "Cannot parse keyword query %r" % kwarg139 140 if value is None:141 # Interpret '__exact=None' as the sql '= NULL'; otherwise, reject142 # all uses of None as a query value.143 if lookup_type != 'exact':144 raise ValueError, "Cannot use None as a query value"145 elif callable(value):146 value = value()147 148 joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None)149 joins.update(joins2)150 where.extend(where2)151 params.extend(params2)152 return joins, where, params153 154 def lookup_inner(path, lookup_type, value, opts, table, column):155 qn = backend.quote_name156 joins, where, params = SortedDict(), [], []157 current_opts = opts158 current_table = table159 current_column = column160 intermediate_table = None161 join_required = False162 163 name = path.pop(0)164 # Has the primary key been requested? If so, expand it out165 # to be the name of the current class' primary key166 if name is None or name == 'pk':167 name = current_opts.pk.name168 169 # Try to find the name in the fields associated with the current class170 try:171 # Does the name belong to a defined many-to-many field?172 field = find_field(name, current_opts.many_to_many, False)173 if field:174 new_table = current_table + '__' + name175 new_opts = field.rel.to._meta176 new_column = new_opts.pk.column177 178 # Need to create an intermediate table join over the m2m table179 # This process hijacks current_table/column to point to the180 # intermediate table.181 current_table = "m2m_" + new_table182 intermediate_table = field.m2m_db_table()183 join_column = field.m2m_reverse_name()184 intermediate_column = field.m2m_column_name()185 186 raise FieldFound187 188 # Does the name belong to a reverse defined many-to-many field?189 field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True)190 if field:191 new_table = current_table + '__' + name192 new_opts = field.opts193 new_column = new_opts.pk.column194 195 # Need to create an intermediate table join over the m2m table.196 # This process hijacks current_table/column to point to the197 # intermediate table.198 current_table = "m2m_" + new_table199 intermediate_table = field.field.m2m_db_table()200 join_column = field.field.m2m_column_name()201 intermediate_column = field.field.m2m_reverse_name()202 203 raise FieldFound204 205 # Does the name belong to a one-to-many field?206 field = find_field(name, current_opts.get_all_related_objects(), True)207 if field:208 new_table = table + '__' + name209 new_opts = field.opts210 new_column = field.field.column211 join_column = opts.pk.column212 213 # 1-N fields MUST be joined, regardless of any other conditions.214 join_required = True215 216 raise FieldFound217 218 # Does the name belong to a one-to-one, many-to-one, or regular field?219 field = find_field(name, current_opts.fields, False)220 if field:221 if field.rel: # One-to-One/Many-to-one field222 new_table = current_table + '__' + name223 new_opts = field.rel.to._meta224 new_column = new_opts.pk.column225 join_column = field.column226 raise FieldFound227 elif path:228 # For regular fields, if there are still items on the path,229 # an error has been made. We munge "name" so that the error230 # properly identifies the cause of the problem.231 name += LOOKUP_SEPARATOR + path[0]232 else:233 raise FieldFound234 235 except FieldFound: # Match found, loop has been shortcut.236 pass237 else: # No match found.238 choices = field_choices(current_opts.many_to_many, False) + \239 field_choices(current_opts.get_all_related_many_to_many_objects(), True) + \240 field_choices(current_opts.get_all_related_objects(), True) + \241 field_choices(current_opts.fields, False)242 raise TypeError, "Cannot resolve keyword '%s' into field. Choices are: %s" % (name, ", ".join(choices))243 244 # Check whether an intermediate join is required between current_table245 # and new_table.246 if intermediate_table:247 joins[qn(current_table)] = (248 qn(intermediate_table), "LEFT OUTER JOIN",249 "%s.%s = %s.%s" % (qn(table), qn(current_opts.pk.column), qn(current_table), qn(intermediate_column))250 )251 252 if path:253 # There are elements left in the path. More joins are required.254 if len(path) == 1 and path[0] in (new_opts.pk.name, None) \255 and lookup_type in ('exact', 'isnull') and not join_required:256 # If the next and final name query is for a primary key,257 # and the search is for isnull/exact, then the current258 # (for N-1) or intermediate (for N-N) table can be used259 # for the search. No need to join an extra table just260 # to check the primary key.261 new_table = current_table262 else:263 # There are 1 or more name queries pending, and we have ruled out264 # any shortcuts; therefore, a join is required.265 joins[qn(new_table)] = (266 qn(new_opts.db_table), "INNER JOIN",267 "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column))268 )269 # If we have made the join, we don't need to tell subsequent270 # recursive calls about the column name we joined on.271 join_column = None272 273 # There are name queries remaining. Recurse deeper.274 joins2, where2, params2 = lookup_inner(path, lookup_type, value, new_opts, new_table, join_column)275 276 joins.update(joins2)277 where.extend(where2)278 params.extend(params2)279 else:280 # No elements left in path. Current element is the element on which281 # the search is being performed.282 283 if join_required:284 # Last query term is a RelatedObject285 if field.field.rel.multiple:286 # RelatedObject is from a 1-N relation.287 # Join is required; query operates on joined table.288 column = new_opts.pk.name289 joins[qn(new_table)] = (290 qn(new_opts.db_table), "INNER JOIN",291 "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column))292 )293 current_table = new_table294 else:295 # RelatedObject is from a 1-1 relation,296 # No need to join; get the pk value from the related object,297 # and compare using that.298 column = current_opts.pk.name299 elif intermediate_table:300 # Last query term is a related object from an N-N relation.301 # Join from intermediate table is sufficient.302 column = join_column303 elif name == current_opts.pk.name and lookup_type in ('exact', 'isnull') and current_column:304 # Last query term is for a primary key. If previous iterations305 # introduced a current/intermediate table that can be used to306 # optimize the query, then use that table and column name.307 column = current_column308 else:309 # Last query term was a normal field.310 column = field.column311 312 # If the field is a geometry field, then the WHERE clause will need to be obtained313 # with the get_geo_where_clause()314 if hasattr(field, '_geom'):315 # Getting the geographic where clause.316 gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)317 318 # Getting the geographic parameters from the field.319 geo_params = field.get_db_prep_lookup(lookup_type, value)320 321 # If a dictionary was passed back from the field modify the where clause.322 if isinstance(geo_params, dict):323 gwc = gwc % geo_params['where']324 geo_params = geo_params['params']325 where.append(gwc)326 params.extend(geo_params)327 else:328 where.append(get_where_clause(lookup_type, current_table + '.', column, value))329 params.extend(field.get_db_prep_lookup(lookup_type, value))330 331 return joins, where, paramsdjango/branches/gis/django/contrib/gis/db/models/fields/__init__.py
r5773 r5776 1 # The Django base Field class. 2 from django.db.models.fields import Field 1 from django.contrib.gis.db.backend import GeoBackendField # depends on the spatial database backend. 3 2 from django.contrib.gis.db.models.proxy import GeometryProxy 4 from django.contrib.gis.db.models.postgis import POSTGIS_TERMS, quotename5 3 from django.contrib.gis.oldforms import WKTField 6 4 from django.utils.functional import curry 7 from django.contrib.gis.geos import GEOSGeometry, GEOSException8 from types import StringType9 5 10 6 #TODO: Flesh out widgets. 11 12 class GeometryField(Field): 7 class GeometryField(GeoBackendField): 13 8 "The base GIS field -- maps to the OpenGIS Specification Geometry type." 14 9 … … 34 29 super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function 35 30 36 def _add_geom(self, style, db_table):37 """Constructs the addition of the geometry to the table using the38 AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure.39 40 Takes the style object (provides syntax highlighting) and the41 database table as parameters.42 """43 sql = style.SQL_KEYWORD('SELECT ') + \44 style.SQL_TABLE('AddGeometryColumn') + '(' + \45 style.SQL_TABLE(quotename(db_table)) + ', ' + \46 style.SQL_FIELD(quotename(self.column)) + ', ' + \47 style.SQL_FIELD(str(self._srid)) + ', ' + \48 style.SQL_COLTYPE(quotename(self._geom)) + ', ' + \49 style.SQL_KEYWORD(str(self._dim)) + ');'50 51 if not self.null:52 # Add a NOT NULL constraint to the field53 sql += '\n' + \54 style.SQL_KEYWORD('ALTER TABLE ') + \55 style.SQL_TABLE(quotename(db_table, dbl=True)) + \56 style.SQL_KEYWORD(' ALTER ') + \57 style.SQL_FIELD(quotename(self.column, dbl=True)) + \58 style.SQL_KEYWORD(' SET NOT NULL') + ';'59 return sql60 61 def _geom_index(self, style, db_table,62 index_type='GIST', index_opts='GIST_GEOMETRY_OPS'):63 "Creates a GiST index for this geometry field."64 sql = style.SQL_KEYWORD('CREATE INDEX ') + \65 style.SQL_TABLE(quotename('%s_%s_id' % (db_table, self.column), dbl=True)) + \66 style.SQL_KEYWORD(' ON ') + \67 style.SQL_TABLE(quotename(db_table, dbl=True)) + \68 style.SQL_KEYWORD(' USING ') + \69 style.SQL_COLTYPE(index_type) + ' ( ' + \70 style.SQL_FIELD(quotename(self.column, dbl=True)) + ' ' + \71 style.SQL_KEYWORD(index_opts) + ' );'72 return sql73 74 def _post_create_sql(self, style, db_table):75 """Returns SQL that will be executed after the model has been76 created. Geometry columns must be added after creation with the77 PostGIS AddGeometryColumn() function."""78 79 # Getting the AddGeometryColumn() SQL necessary to create a PostGIS80 # geometry field.81 post_sql = self._add_geom(style, db_table)82 83 # If the user wants to index this data, then get the indexing SQL as well.84 if self._index:85 return '%s\n%s' % (post_sql, self._geom_index(style, db_table))86 else:87 return post_sql88 89 def _post_delete_sql(self, style, db_table):90 "Drops the geometry column."91 sql = style.SQL_KEYWORD('SELECT ') + \92 style.SQL_KEYWORD('DropGeometryColumn') + '(' + \93 style.SQL_TABLE(quotename(db_table)) + ', ' + \94 style.SQL_FIELD(quotename(self.column)) + ');'95 return sql96 97 31 def contribute_to_class(self, cls, name): 98 32 super(GeometryField, self).contribute_to_class(cls, name) … … 113 47 return "NoField" 114 48 115 def get_db_prep_lookup(self, lookup_type, value):116 "Returns field's value prepared for database lookup, accepts WKT and GEOS Geometries for the value."117 if lookup_type in POSTGIS_TERMS:118 if lookup_type == 'isnull': return [value] # special case for NULL geometries.119 if not bool(value): return [None] # If invalid value passed in.120 if isinstance(value, GEOSGeometry):121 # GEOSGeometry instance passed in.122 if value.srid != self._srid:123 # Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key124 # to the where parameters, since we need to transform the geometry in the query.125 return {'where' : "Transform(%s,%s)",126 'params' : [value, self._srid]127 }128 else:129 # Just return the GEOSGeometry, it has its own psycopg2 adaptor.130 return [value]131 elif isinstance(value, StringType):132 # String instance passed in, assuming WKT.133 # TODO: Any validation needed here to prevent SQL injection?134 return ["SRID=%d;%s" % (self._srid, value)]135 else:136 raise TypeError("Invalid type (%s) used for field lookup value." % str(type(value)))137 else:138 raise TypeError("Field has invalid lookup: %s" % lookup_type)139 140 def get_db_prep_save(self, value):141 "Prepares the value for saving in the database."142 if not bool(value): return None143 if isinstance(value, GEOSGeometry):144 return value145 else:146 return ("SRID=%d;%s" % (self._srid, wkt))147 148 def get_placeholder(self, value):149 "Provides a proper substitution value for "150 if isinstance(value, GEOSGeometry) and value.srid != self._srid:151 # Adding Transform() to the SQL placeholder.152 return 'Transform(%%s, %s)' % self._srid153 else:154 return '%s'155 156 49 def get_manipulator_field_objs(self): 157 50 "Using the WKTField (defined above) to be our manipulator." django/branches/gis/django/contrib/gis/db/models/query.py
r5751 r5776 2 2 from django.db import backend 3 3 from django.contrib.gis.db.models.fields import GeometryField 4 from django.contrib.gis.db. models.postgis import parse_lookup4 from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends on the spatial database backend. 5 5 from django.db.models.fields import FieldDoesNotExist 6 6 import operator django/branches/gis/django/contrib/gis/tests/geoapp/tests.py
r5773 r5776 92 92 93 93 def test005_left_right(self): 94 "Testing the left ('<<') right ('>>') operators."94 "Testing the 'left' and 'right' lookup types." 95 95 96 96 # Left: A << B => true if xmax(A) < xmin(B) django/branches/gis/django/contrib/gis/tests/__init__.py
r5763 r5776 1 1 from copy import copy 2 2 from unittest import TestSuite, TextTestRunner 3 from django.contrib.gis. utilsimport create_spatial_db3 from django.contrib.gis.db.backend import create_spatial_db 4 4 from django.db import connection 5 5 from django.test.utils import destroy_test_db
