Permalink
Browse files

gis: spatial-backend enhancements:

 (1) GEOS no longer has psycopg2-specific routines, functionality now part of PostGIS adaptor in the spatial backend.
 (2) ST_GeomFromWKB() now used to enhance performance.
 (3) Moved GeometryProxy back to its original location.
 (4) Should resolve #5498, but not yet confirmed.
 (5) Test-sql files are now backend-specific.


git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6508 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent b6c8bba commit c049672a74673293e98104c223f034d5f5e08133 @jbronn jbronn committed Oct 14, 2007
@@ -11,32 +11,41 @@
the backend.
(4) The `parse_lookup` function, used for spatial SQL construction by
the GeoQuerySet.
- (5) The `create_spatial_db`, `geo_quotename`, and `get_geo_where_clause`
- routines (needed by `parse_lookup`.
+ (5) The `create_spatial_db`, and `get_geo_where_clause`
+ routines (needed by `parse_lookup`).
Currently only PostGIS is supported, but someday backends will be added for
additional spatial databases (e.g., Oracle, DB2).
"""
+from types import StringType, UnicodeType
from django.conf import settings
from django.db import connection
from django.db.models.query import field_choices, find_field, get_where_clause, \
FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS
from django.utils.datastructures import SortedDict
+from django.contrib.gis.geos import GEOSGeometry
-# These routines default to False
-ASGML, ASKML, UNION = (False, False, False)
+# These routines (needed by GeoManager), default to False.
+ASGML, ASKML, TRANSFORM, UNION= (False, False, False, False)
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# PostGIS is the spatial database, getting the rquired modules,
# renaming as necessary.
from django.contrib.gis.db.backend.postgis import \
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
- PostGISProxy as GeometryProxy, \
- create_spatial_db, geo_quotename, get_geo_where_clause, \
- ASGML, ASKML, UNION
+ create_spatial_db, get_geo_where_clause, gqn, \
+ ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_NAME)
+def geo_quotename(value):
+ """
+ Returns the quotation used on a given Geometry value using the geometry
+ quoting from the backend (the `gqn` function).
+ """
+ if isinstance(value, (StringType, UnicodeType)): return gqn(value)
+ else: return str(value)
+
#### query.py overloaded functions ####
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
# counterparts to support constructing SQL for geographic queries.
@@ -263,38 +272,29 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
# If the field is a geometry field, then the WHERE clause will need to be obtained
# with the get_geo_where_clause()
if hasattr(field, '_geom'):
- # Do we have multiple arguments, e.g., ST_Relate, ST_DWithin lookup types
- # need more than argument.
+ # Do we have multiple arguments, e.g., `relate`, `dwithin` lookup types
+ # need more than argument.
multiple_args = isinstance(value, tuple)
- # Getting the geographic where clause.
- gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
-
- # Getting the geographic parameters from the field.
+ # Getting the preparation SQL object from the field.
if multiple_args:
- geo_params = field.get_db_prep_lookup(lookup_type, value[0])
+ geo_prep = field.get_db_prep_lookup(lookup_type, value[0])
else:
- geo_params = field.get_db_prep_lookup(lookup_type, value)
-
- # If a dictionary was passed back from the field modify the where clause.
- param_dict = isinstance(geo_params, dict)
- if param_dict:
- subst_list = geo_params['where']
- if multiple_args: subst_list += map(geo_quotename, value[1:])
- geo_params = geo_params['params']
- gwc = gwc % tuple(subst_list)
- elif multiple_args:
- # Modify the where clause if we have multiple arguments -- the
- # first substitution will be for another placeholder (for the
- # geometry) since it is already apart of geo_params.
- subst_list = ['%s']
- subst_list += map(geo_quotename, value[1:])
- gwc = gwc % tuple(subst_list)
-
- # Finally, appending onto the WHERE clause, and extending with any
- # additional parameters.
+ geo_prep = field.get_db_prep_lookup(lookup_type, value)
+
+ # Getting the adapted geometry from the field.
+ gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
+
+ # A GeoFieldSQL object is returned by `get_db_prep_lookup` --
+ # getting the substitution list and the geographic parameters.
+ subst_list = geo_prep.where
+ if multiple_args: subst_list += map(geo_quotename, value[1:])
+ gwc = gwc % tuple(subst_list)
+
+ # Finally, appending onto the WHERE clause, and extending with
+ # the additional parameters.
where.append(gwc)
- params.extend(geo_params)
+ params.extend(geo_prep.params)
else:
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
params.extend(field.get_db_prep_lookup(lookup_type, value))
@@ -1,24 +1,10 @@
"""
The PostGIS spatial database backend module.
"""
-from django.contrib.gis.db.backend.postgis.query import \
- get_geo_where_clause, geo_quotename, \
- GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
- MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
-from django.contrib.gis.db.backend.postgis.field import PostGISField
+from django.contrib.gis.db.backend.postgis.field import PostGISField, gqn
from django.contrib.gis.db.backend.postgis.proxy import PostGISProxy
-
-# Functions used by GeoManager methods, and not via lookup types.
-if MAJOR_VERSION == 1:
- if MINOR_VERSION1 == 3:
- ASKML = 'ST_AsKML'
- ASGML = 'ST_AsGML'
- UNION = 'ST_Union'
- elif MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 1:
- ASKML = 'AsKML'
- ASGML = 'AsGML'
- UNION = 'GeomUnion'
-
-
-
+from django.contrib.gis.db.backend.postgis.query import \
+ get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
+ MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2, \
+ ASKML, ASGML, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
@@ -0,0 +1,29 @@
+"""
+ This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
+"""
+
+from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB
+from psycopg2 import Binary
+from psycopg2.extensions import ISQLQuote
+
+class PostGISAdaptor(object):
+ def __init__(self, geom, srid):
+ "Initializes on the geometry and the SRID."
+ # Getting the WKB and the SRID
+ self.wkb = geom.wkb
+ self.srid = srid
+
+ def __conform__(self, proto):
+ # Does the given protocol conform to what Psycopg2 expects?
+ if proto == ISQLQuote:
+ return self
+ else:
+ raise Exception('Error implementing psycopg2 protocol. Is psycopg2 installed?')
+
+ def __str__(self):
+ return self.getquoted()
+
+ def getquoted(self):
+ "Returns a properly quoted string for use in PostgreSQL/PostGIS."
+ # Want to use WKB, so wrap with psycopg2 Binary() to quote properly.
+ return "%s(%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1)
@@ -1,7 +1,17 @@
+from types import StringType, UnicodeType
+from django.db import connection
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry, GEOSException
-from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, geo_quotename as quotename
-from types import StringType
+from django.contrib.gis.db.backend.util import GeoFieldSQL
+from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
+from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, TRANSFORM
+from psycopg2 import Binary
+
+# Quotename & geographic quotename, respectively
+qn = connection.ops.quote_name
+def gqn(value):
+ if isinstance(value, UnicodeType): value = value.encode('ascii')
+ return "'%s'" % value
class PostGISField(Field):
def _add_geom(self, style, db_table):
@@ -10,36 +20,36 @@ def _add_geom(self, style, db_table):
AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure.
Takes the style object (provides syntax highlighting) and the
- database table as parameters.
+ database table as parameters.
"""
sql = style.SQL_KEYWORD('SELECT ') + \
style.SQL_TABLE('AddGeometryColumn') + '(' + \
- style.SQL_TABLE(quotename(db_table)) + ', ' + \
- style.SQL_FIELD(quotename(self.column)) + ', ' + \
+ style.SQL_TABLE(gqn(db_table)) + ', ' + \
+ style.SQL_FIELD(gqn(self.column)) + ', ' + \
style.SQL_FIELD(str(self._srid)) + ', ' + \
- style.SQL_COLTYPE(quotename(self._geom)) + ', ' + \
+ style.SQL_COLTYPE(gqn(self._geom)) + ', ' + \
style.SQL_KEYWORD(str(self._dim)) + ');'
if not self.null:
# Add a NOT NULL constraint to the field
sql += '\n' + \
style.SQL_KEYWORD('ALTER TABLE ') + \
- style.SQL_TABLE(quotename(db_table, dbl=True)) + \
+ style.SQL_TABLE(qn(db_table)) + \
style.SQL_KEYWORD(' ALTER ') + \
- style.SQL_FIELD(quotename(self.column, dbl=True)) + \
+ style.SQL_FIELD(qn(self.column)) + \
style.SQL_KEYWORD(' SET NOT NULL') + ';'
return sql
def _geom_index(self, style, db_table,
index_type='GIST', index_opts='GIST_GEOMETRY_OPS'):
"Creates a GiST index for this geometry field."
sql = style.SQL_KEYWORD('CREATE INDEX ') + \
- style.SQL_TABLE(quotename('%s_%s_id' % (db_table, self.column), dbl=True)) + \
+ style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + \
style.SQL_KEYWORD(' ON ') + \
- style.SQL_TABLE(quotename(db_table, dbl=True)) + \
+ style.SQL_TABLE(qn(db_table)) + \
style.SQL_KEYWORD(' USING ') + \
style.SQL_COLTYPE(index_type) + ' ( ' + \
- style.SQL_FIELD(quotename(self.column, dbl=True)) + ' ' + \
+ style.SQL_FIELD(qn(self.column)) + ' ' + \
style.SQL_KEYWORD(index_opts) + ' );'
return sql
@@ -64,8 +74,8 @@ def _post_delete_sql(self, style, db_table):
"Drops the geometry column."
sql = style.SQL_KEYWORD('SELECT ') + \
style.SQL_KEYWORD('DropGeometryColumn') + '(' + \
- style.SQL_TABLE(quotename(db_table)) + ', ' + \
- style.SQL_FIELD(quotename(self.column)) + ');'
+ style.SQL_TABLE(gqn(db_table)) + ', ' + \
+ style.SQL_FIELD(gqn(self.column)) + ');'
return sql
def db_type(self):
@@ -81,51 +91,63 @@ def get_db_prep_lookup(self, lookup_type, value):
GEOS Geometries for the value.
"""
if lookup_type in POSTGIS_TERMS:
- if lookup_type == 'isnull': return [value] # special case for NULL geometries.
- if not bool(value): return [None] # If invalid value passed in.
+ # special case for isnull lookup
+ if lookup_type == 'isnull':
+ return GeoFieldSQL([], [value])
+
+ # When the input is not a GEOS geometry, attempt to construct one
+ # from the given string input.
if isinstance(value, GEOSGeometry):
- # GEOSGeometry instance passed in.
- if value.srid != self._srid:
- # Returning a dictionary instructs the parse_lookup() to add
- # what's in the 'where' key to the where parameters, since we
- # need to transform the geometry in the query.
- return {'where' : ["ST_Transform(%s,%s)"],
- 'params' : [value, self._srid]
- }
- else:
- # Just return the GEOSGeometry, it has its own psycopg2 adaptor.
- return [value]
- elif isinstance(value, StringType):
- # String instance passed in, assuming WKT.
- # TODO: Any validation needed here to prevent SQL injection?
- return ["SRID=%d;%s" % (self._srid, value)]
+ pass
+ elif isinstance(value, (StringType, UnicodeType)):
+ try:
+ value = GEOSGeometry(value)
+ except GEOSException:
+ raise TypeError("Could not create geometry from lookup value: %s" % str(value))
+ else:
+ raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
+
+ # Getting the SRID of the geometry, or defaulting to that of the field if
+ # it is None.
+ if value.srid is None: srid = self._srid
+ else: srid = value.srid
+
+ # The adaptor will be used by psycopg2 for quoting the WKB.
+ adapt = PostGISAdaptor(value, srid)
+
+ if srid != self._srid:
+ # Adding the necessary string substitutions and parameters
+ # to perform a geometry transformation.
+ return GeoFieldSQL(['%s(%%s,%%s)' % TRANSFORM],
+ [adapt, self._srid])
else:
- raise TypeError("Invalid type (%s) used for field lookup value." % str(type(value)))
+ return GeoFieldSQL(['%s'], [adapt])
else:
raise TypeError("Field has invalid lookup: %s" % lookup_type)
def get_db_prep_save(self, value):
"Prepares the value for saving in the database."
if not bool(value): return None
if isinstance(value, GEOSGeometry):
- return value
+ return PostGISAdaptor(value, value.srid)
else:
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
def get_internal_type(self):
"""
- Returns NoField because a stored procedure is used by PostGIS to create the
+ Returns NoField because a stored procedure is used by PostGIS to create
+ the Geometry Fields.
"""
return 'NoField'
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
- SRID of the field. Specifically, this routine will substitute in the
- ST_Transform() function call.
+ SRID of the field. Specifically, this routine will substitute in the
+ ST_Transform() function call.
"""
if isinstance(value, GEOSGeometry) and value.srid != self._srid:
# Adding Transform() to the SQL placeholder.
- return 'ST_Transform(%%s, %s)' % self._srid
+ return '%s(%%s, %s)' % (TRANSFORM, self._srid)
else:
return '%s'
Oops, something went wrong.

0 comments on commit c049672

Please sign in to comment.