From c049672a74673293e98104c223f034d5f5e08133 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sun, 14 Oct 2007 20:13:11 +0000 Subject: [PATCH] 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 --- django/contrib/gis/db/backend/__init__.py | 68 +++++++------- .../gis/db/backend/postgis/__init__.py | 24 +---- .../contrib/gis/db/backend/postgis/adaptor.py | 29 ++++++ .../contrib/gis/db/backend/postgis/field.py | 94 ++++++++++++------- .../contrib/gis/db/backend/postgis/query.py | 62 +++++++----- django/contrib/gis/db/backend/util.py | 8 ++ django/contrib/gis/db/models/__init__.py | 2 +- .../contrib/gis/db/models/fields/__init__.py | 3 +- .../db/{backend/postgis => models}/proxy.py | 2 +- django/contrib/gis/db/models/query.py | 33 +++++-- django/contrib/gis/geos/base.py | 32 +++---- django/contrib/gis/geos/libgeos.py | 6 -- ...{city.sql => city.postgresql_psycopg2.sql} | 0 ...ry.sql => country.postgresql_psycopg2.sql} | 0 ...tate.sql => state.postgresql_psycopg2.sql} | 0 15 files changed, 219 insertions(+), 144 deletions(-) create mode 100644 django/contrib/gis/db/backend/postgis/adaptor.py create mode 100644 django/contrib/gis/db/backend/util.py rename django/contrib/gis/db/{backend/postgis => models}/proxy.py (98%) rename django/contrib/gis/tests/geoapp/sql/{city.sql => city.postgresql_psycopg2.sql} (100%) rename django/contrib/gis/tests/geoapp/sql/{country.sql => country.postgresql_psycopg2.sql} (100%) rename django/contrib/gis/tests/geoapp/sql/{state.sql => state.postgresql_psycopg2.sql} (100%) diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py index a28a8d1cff..854648b858 100644 --- a/django/contrib/gis/db/backend/__init__.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -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)) diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index d95bde4370..3868ac4fde 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -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 diff --git a/django/contrib/gis/db/backend/postgis/adaptor.py b/django/contrib/gis/db/backend/postgis/adaptor.py new file mode 100644 index 0000000000..37ef6ffac0 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/adaptor.py @@ -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) diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py index 2d941dfd0b..a08acbf0eb 100644 --- a/django/contrib/gis/db/backend/postgis/field.py +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -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,23 +20,23 @@ class PostGISField(Field): 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 @@ -34,12 +44,12 @@ class PostGISField(Field): 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 @@ class PostGISField(Field): "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,26 +91,37 @@ class PostGISField(Field): 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("Invalid type (%s) used for field lookup value." % str(type(value))) + 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: + return GeoFieldSQL(['%s'], [adapt]) else: raise TypeError("Field has invalid lookup: %s" % lookup_type) @@ -108,24 +129,25 @@ class PostGISField(Field): "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' diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index d8b13eb37f..5a808909d6 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -1,11 +1,11 @@ """ - This module contains the spatial lookup types, and the get_geo_where_clause() - routine for PostGIS. + This module contains the spatial lookup types, and the get_geo_where_clause() + routine for PostGIS. """ from django.db import connection from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple from types import StringType, UnicodeType -quote_name = connection.ops.quote_name +qn = connection.ops.quote_name # Getting the PostGIS version information POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple() @@ -121,21 +121,16 @@ def get_geom_func(lookup_type): def get_geo_where_clause(lookup_type, table_prefix, field_name, value): "Returns the SQL WHERE clause for use in PostGIS SQL construction." if table_prefix.endswith('.'): - table_prefix = quote_name(table_prefix[:-1])+'.' - field_name = quote_name(field_name) + table_prefix = qn(table_prefix[:-1])+'.' + field_name = qn(field_name) # See if a PostGIS operator matches the lookup type first - try: + if lookup_type in POSTGIS_OPERATORS: return '%s%s %s %%s' % (table_prefix, field_name, POSTGIS_OPERATORS[lookup_type]) - except KeyError: - pass # See if a PostGIS Geometry function matches the lookup type next - try: + if lookup_type in POSTGIS_GEOMETRY_FUNCTIONS: lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] - except KeyError: - pass - else: # Lookup types that are tuples take tuple arguments, e.g., 'relate' and # 'dwithin' lookup types. if isinstance(lookup_info, tuple): @@ -145,7 +140,7 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value): # Ensuring that a tuple _value_ was passed in from the user if not isinstance(value, tuple) or len(value) != 2: - raise TypeError('2-element tuple required for %s lookup type.' % lookup_type) + raise TypeError('2-element tuple required for `%s` lookup type.' % lookup_type) # Ensuring the argument type matches what we expect. if not isinstance(value[1], arg_type): @@ -154,7 +149,7 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value): return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name) else: # Returning the SQL necessary for the geometry function call. For example: - # ST_Contains("geoapp_country"."poly", ST_GeomFromText(..)) + # ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..)) return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name) # Handling 'isnull' lookup type @@ -163,10 +158,35 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value): raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) -def geo_quotename(value, dbl=False): - "Returns the quotation used for PostGIS on a given value (uses single quotes by default)." - if isinstance(value, (StringType, UnicodeType)): - if dbl: return '"%s"' % value - else: return "'%s'" % value - else: - return str(value) +# Functions that we define manually. +if MAJOR_VERSION == 1: + if MINOR_VERSION1 == 3: + # PostGIS versions 1.3.x + ASKML = 'ST_AsKML' + ASGML = 'ST_AsGML' + GEOM_FROM_TEXT = 'ST_GeomFromText' + GEOM_FROM_WKB = 'ST_GeomFromWKB' + UNION = 'ST_Union' + TRANSFORM = 'ST_Transform' + elif MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 1: + # PostGIS versions 1.2.x + ASKML = 'AsKML' + ASGML = 'AsGML' + GEOM_FROM_TEXT = 'GeomFromText' + GEOM_FROM_WKB = 'GeomFromWKB' + UNION = 'GeomUnion' + TRANSFORM = 'Transform' + elif MINOR_VERSION1 == 1 and MINOR_VERSION2 >= 0: + # PostGIS versions 1.1.x + ASKML = False + ASGML = 'AsGML' + GEOM_FROM_TEXT = 'GeomFromText' + GEOM_FROM_WKB = 'GeomFromWKB' + TRANSFORM = 'Transform' + UNION = 'GeomUnion' + +# Custom selection not needed for PostGIS since GEOS geometries may be +# instantiated directly from the HEXEWKB returned by default. If +# WKT is needed for some reason in the future, this value may be changed, +# 'AsText(%s)' +GEOM_SELECT = None diff --git a/django/contrib/gis/db/backend/util.py b/django/contrib/gis/db/backend/util.py new file mode 100644 index 0000000000..d49376033b --- /dev/null +++ b/django/contrib/gis/db/backend/util.py @@ -0,0 +1,8 @@ +class GeoFieldSQL(object): + """ + Container for passing values to `parse_lookup` from the various + backend geometry fields. + """ + def __init__(self, where=[], params=[]): + self.where = where + self.params = params diff --git a/django/contrib/gis/db/models/__init__.py b/django/contrib/gis/db/models/__init__.py index 2d19fe9028..02a2c5318e 100644 --- a/django/contrib/gis/db/models/__init__.py +++ b/django/contrib/gis/db/models/__init__.py @@ -7,7 +7,7 @@ from django.contrib.gis.db.models.manager import GeoManager # The GeoQ object from django.contrib.gis.db.models.query import GeoQ -# The various PostGIS/OpenGIS enabled fields. +# The geographic-enabled fields. from django.contrib.gis.db.models.fields import \ GeometryField, PointField, LineStringField, PolygonField, \ MultiPointField, MultiLineStringField, MultiPolygonField, \ diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index 850cac2007..dc23051cf1 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -1,5 +1,6 @@ from django.conf import settings -from django.contrib.gis.db.backend import GeoBackendField, GeometryProxy # these depend on the spatial database backend. +from django.contrib.gis.db.backend import GeoBackendField # these depend on the spatial database backend. +from django.contrib.gis.db.models.proxy import GeometryProxy from django.contrib.gis.oldforms import WKTField from django.contrib.gis.geos import GEOSGeometry diff --git a/django/contrib/gis/db/backend/postgis/proxy.py b/django/contrib/gis/db/models/proxy.py similarity index 98% rename from django/contrib/gis/db/backend/postgis/proxy.py rename to django/contrib/gis/db/models/proxy.py index bb1ee7f280..43e7dbe8ef 100644 --- a/django/contrib/gis/db/backend/postgis/proxy.py +++ b/django/contrib/gis/db/models/proxy.py @@ -8,7 +8,7 @@ from types import NoneType, StringType, UnicodeType -class PostGISProxy(object): +class GeometryProxy(object): def __init__(self, klass, field): """ Proxy initializes on the given Geometry class (not an instance) and diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 8bfec71bf7..169d4fd357 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -6,7 +6,7 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.datastructures import SortedDict from django.contrib.gis.db.models.fields import GeometryField # parse_lookup depends on the spatial database backend. -from django.contrib.gis.db.backend import parse_lookup, ASGML, ASKML, UNION +from django.contrib.gis.db.backend import parse_lookup, ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION from django.contrib.gis.geos import GEOSGeometry class GeoQ(Q): @@ -29,6 +29,11 @@ class GeoQuerySet(QuerySet): # For replacement fields in the SELECT. self._custom_select = {} + # If GEOM_SELECT is defined in the backend, then it will be used + # for the selection format of the geometry column. + if GEOM_SELECT: self._geo_fmt = GEOM_SELECT + else: self._geo_fmt = '%s' + def _filter_or_exclude(self, mapper, *args, **kwargs): # mapper is a callable used to transform Q objects, # or None for identity transform @@ -57,10 +62,21 @@ class GeoQuerySet(QuerySet): # GeoQuerySet. Specifically, this allows operations to be done on fields # in the SELECT, overriding their values -- this is different from using # QuerySet.extra(select=foo) because extra() adds an an _additional_ - # field to be selected. Used in returning transformed geometries. + # field to be selected. Used in returning transformed geometries, and + # handling the selection of native database geometry formats. for f in opts.fields: - if f.column in self._custom_select: select.append(self._custom_select[f.column]) - else: select.append(self._field_column(f)) + # Getting the selection format string. + if hasattr(f, '_geom'): sel_fmt = self._geo_fmt + else: sel_fmt = '%s' + + # Getting the field selection substitution string + if f.column in self._custom_select: + fld_sel = self._custom_select[f.column] + else: + fld_sel = self._field_column(f) + + # Appending the selection + select.append(sel_fmt % fld_sel) tables = [quote_only_if_word(t) for t in self._tables] joins = SortedDict() @@ -204,13 +220,16 @@ class GeoQuerySet(QuerySet): # Is the given field name a geographic field? field = self.model._meta.get_field(field_name) if not isinstance(field, GeometryField): - raise TypeError('ST_Transform() only available for GeometryFields') + raise TypeError('%s() only available for GeometryFields' % TRANSFORM) + + # If there's already custom select SQL. + col = self._custom_select.get(field.column, self._field_column(field)) # Setting the key for the field's column with the custom SELECT SQL to # override the geometry column returned from the database. self._custom_select[field.column] = \ - '(ST_Transform(%s, %s)) AS %s' % (self._field_column(field), srid, - connection.ops.quote_name(field.column)) + '(%s(%s, %s)) AS %s' % (TRANSFORM, col, srid, + connection.ops.quote_name(field.column)) return self._clone() def union(self, field_name): diff --git a/django/contrib/gis/geos/base.py b/django/contrib/gis/geos/base.py index e44a6e7549..53cfb7ebb5 100644 --- a/django/contrib/gis/geos/base.py +++ b/django/contrib/gis/geos/base.py @@ -12,7 +12,7 @@ from types import StringType, UnicodeType, IntType, FloatType, BufferType import re from django.contrib.gis.geos.coordseq import GEOSCoordSeq, create_cs from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError -from django.contrib.gis.geos.libgeos import lgeos, HAS_NUMPY, ISQLQuote +from django.contrib.gis.geos.libgeos import lgeos, HAS_NUMPY from django.contrib.gis.geos.pointer import GEOSPointer, NULL_GEOM # Trying to import GDAL libraries, if available. Have to place in @@ -47,14 +47,15 @@ class GEOSGeometry(object): The `srid` keyword is used to specify the Source Reference Identifier (SRID) number for this Geometry. If not set, the SRID will be None. - """ - + """ + from_hex = False if isinstance(geo_input, UnicodeType): # Encoding to ASCII, WKT or HEXEWKB doesn't need any more. geo_input = geo_input.encode('ascii') if isinstance(geo_input, StringType): if hex_regex.match(geo_input): # If the regex matches, the geometry is in HEX form. + from_hex = True sz = c_size_t(len(geo_input)) buf = create_string_buffer(geo_input) g = lgeos.GEOSGeomFromHEX_buf(buf, sz) @@ -62,7 +63,7 @@ class GEOSGeometry(object): # Otherwise, the geometry is in WKT form. g = lgeos.GEOSGeomFromWKT(c_char_p(geo_input)) else: - raise GEOSException, 'given string input "%s" unrecognized as WKT or HEXEWKB.' % geo_input + raise GEOSException('String or unicode input unrecognized as WKT or HEXEWKB.') elif isinstance(geo_input, (IntType, GEOSPointer)): # When the input is either a memory address (an integer), or a # GEOSPointer object. @@ -85,6 +86,10 @@ class GEOSGeometry(object): # Setting the SRID, if given. if srid and isinstance(srid, int): self.srid = srid + # Exported HEX from other GEOS geometries will have -1 SRID -- + # set here to 0, when the SRID is not explicitly given. + if not srid and from_hex: self.srid = 0 + # Setting the class type (e.g., 'Point', 'Polygon', etc.) self.__class__ = GEOS_CLASSES[self.geom_type] @@ -207,19 +212,6 @@ class GEOSGeometry(object): self.__class__ = GEOS_CLASSES[gtype] if isinstance(self, (Polygon, GeometryCollection)): self._populate() - #### Psycopg2 database adaptor routines #### - def __conform__(self, proto): - # Does the given protocol conform to what Psycopg2 expects? - if proto == ISQLQuote: - return self - else: - raise GEOSException, 'Error implementing psycopg2 protocol. Is psycopg2 installed?' - - def getquoted(self): - "Returns a properly quoted string for use in PostgreSQL/PostGIS." - # Using ST_GeomFromText(), corresponds to SQL/MM ISO standard. - return "ST_GeomFromText('%s', %s)" % (self.wkt, self.srid or -1) - #### Coordinate Sequence Routines #### @property def has_cs(self): @@ -425,7 +417,11 @@ class GEOSGeometry(object): @property def hex(self): - "Returns the HEXEWKB of the Geometry." + """ + Returns the HEX of the Geometry -- please note that the SRID is not + included in this representation, because the GEOS C library uses + -1 by default, even if the SRID is set. + """ sz = c_size_t() h = lgeos.GEOSGeomToHEX_buf(self._ptr(), byref(sz)) return string_at(h, sz.value) diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index 4dcdb16d66..aea6975ae7 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -18,12 +18,6 @@ try: except ImportError: HAS_NUMPY = False -# Is psycopg2 available? -try: - from psycopg2.extensions import ISQLQuote -except (ImportError, EnvironmentError): - ISQLQuote = None - # Setting the appropriate name for the GEOS-C library, depending on which # OS and POSIX platform we're running. if os.name == 'nt': diff --git a/django/contrib/gis/tests/geoapp/sql/city.sql b/django/contrib/gis/tests/geoapp/sql/city.postgresql_psycopg2.sql similarity index 100% rename from django/contrib/gis/tests/geoapp/sql/city.sql rename to django/contrib/gis/tests/geoapp/sql/city.postgresql_psycopg2.sql diff --git a/django/contrib/gis/tests/geoapp/sql/country.sql b/django/contrib/gis/tests/geoapp/sql/country.postgresql_psycopg2.sql similarity index 100% rename from django/contrib/gis/tests/geoapp/sql/country.sql rename to django/contrib/gis/tests/geoapp/sql/country.postgresql_psycopg2.sql diff --git a/django/contrib/gis/tests/geoapp/sql/state.sql b/django/contrib/gis/tests/geoapp/sql/state.postgresql_psycopg2.sql similarity index 100% rename from django/contrib/gis/tests/geoapp/sql/state.sql rename to django/contrib/gis/tests/geoapp/sql/state.postgresql_psycopg2.sql