From 5799c2e048ff829300af88ae839de20e1763ee1d Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sat, 15 Dec 2007 00:30:48 +0000 Subject: [PATCH] gis: Applied DRY to spatial SQL generation in anticipation of queryset-refactor; fixed `gml` function for PostGIS 1.3.2 parameter ordering. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6919 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/db/backend/__init__.py | 8 +- django/contrib/gis/db/backend/mysql/query.py | 19 +- django/contrib/gis/db/backend/oracle/field.py | 2 +- .../contrib/gis/db/backend/oracle/models.py | 10 +- django/contrib/gis/db/backend/oracle/query.py | 104 +++----- .../gis/db/backend/postgis/creation.py | 2 + .../contrib/gis/db/backend/postgis/field.py | 6 +- .../contrib/gis/db/backend/postgis/models.py | 16 +- .../contrib/gis/db/backend/postgis/query.py | 237 ++++++++++-------- django/contrib/gis/db/backend/util.py | 54 +++- django/contrib/gis/db/models/query.py | 15 +- 11 files changed, 256 insertions(+), 217 deletions(-) diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py index 1c015804ff..63c01b146b 100644 --- a/django/contrib/gis/db/backend/__init__.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -26,7 +26,7 @@ from django.utils.datastructures import SortedDict from django.contrib.gis.geos import GEOSGeometry # These routines (needed by GeoManager), default to False. -ASGML, ASKML, DISTANCE, TRANSFORM, UNION= (False, False, False, False, False) +ASGML, ASKML, DISTANCE, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False) if settings.DATABASE_ENGINE == 'postgresql_psycopg2': # PostGIS is the spatial database, getting the rquired modules, @@ -34,7 +34,9 @@ if settings.DATABASE_ENGINE == 'postgresql_psycopg2': from django.contrib.gis.db.backend.postgis import \ PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \ create_spatial_db, get_geo_where_clause, \ - ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION + ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION, \ + MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 + VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2) SPATIAL_BACKEND = 'postgis' elif settings.DATABASE_ENGINE == 'oracle': from django.contrib.gis.db.backend.oracle import \ @@ -283,7 +285,7 @@ def lookup_inner(path, lookup_type, value, opts, table, column): 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) + gwc = get_geo_where_clause(lookup_type, current_table, column, value) # Substituting in the the where parameters into the geographic where # clause, and extending the parameters. diff --git a/django/contrib/gis/db/backend/mysql/query.py b/django/contrib/gis/db/backend/mysql/query.py index 10060ab57f..d7bac0f84f 100644 --- a/django/contrib/gis/db/backend/mysql/query.py +++ b/django/contrib/gis/db/backend/mysql/query.py @@ -5,6 +5,11 @@ from django.db import connection qn = connection.ops.quote_name +# To ease implementation, WKT is passed to/from MySQL. +GEOM_FROM_TEXT = 'GeomFromText' +GEOM_FROM_WKB = 'GeomFromWKB' +GEOM_SELECT = 'AsText(%s)' + # WARNING: MySQL is NOT compliant w/the OpenGIS specification and # _every_ one of these lookup types is on the _bounding box_ only. MYSQL_GIS_FUNCTIONS = { @@ -31,24 +36,18 @@ MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable def get_geo_where_clause(lookup_type, table_prefix, field_name, value): "Returns the SQL WHERE clause for use in MySQL spatial SQL construction." - if table_prefix.endswith('.'): - table_prefix = qn(table_prefix[:-1])+'.' - field_name = qn(field_name) + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(table_prefix), qn(field_name)) # See if a MySQL Geometry function matches the lookup type next lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False) if lookup_info: - return "%s(%s, %%s)" % (lookup_info, table_prefix + field_name) + return "%s(%s, %%s)" % (lookup_info, geo_col) # Handling 'isnull' lookup type # TODO: Is this needed because MySQL cannot handle NULL # geometries in its spatial indices. if lookup_type == 'isnull': - return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) - -# To ease implementation, WKT is passed to/from MySQL. -GEOM_FROM_TEXT = 'GeomFromText' -GEOM_FROM_WKB = 'GeomFromWKB' -GEOM_SELECT = 'AsText(%s)' diff --git a/django/contrib/gis/db/backend/oracle/field.py b/django/contrib/gis/db/backend/oracle/field.py index 3389c6dfb8..a7d8e68778 100644 --- a/django/contrib/gis/db/backend/oracle/field.py +++ b/django/contrib/gis/db/backend/oracle/field.py @@ -4,7 +4,7 @@ from django.db import connection from django.db.backends.util import truncate_name from django.db.models.fields import Field # Django base Field class from django.contrib.gis.geos import GEOSGeometry -from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL +from django.contrib.gis.db.backend.util import GeoFieldSQL from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM diff --git a/django/contrib/gis/db/backend/oracle/models.py b/django/contrib/gis/db/backend/oracle/models.py index 6cda7ab0bf..c740b48efe 100644 --- a/django/contrib/gis/db/backend/oracle/models.py +++ b/django/contrib/gis/db/backend/oracle/models.py @@ -12,8 +12,8 @@ from django.contrib.gis.models import SpatialRefSysMixin class GeometryColumns(models.Model): "Maps to the Oracle USER_SDO_GEOM_METADATA table." - table_name = models.CharField(maxlength=32) - column_name = models.CharField(maxlength=1024) + table_name = models.CharField(max_length=32) + column_name = models.CharField(max_length=1024) srid = models.IntegerField(primary_key=True) # TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY). class Meta: @@ -28,11 +28,11 @@ class GeometryColumns(models.Model): class SpatialRefSys(models.Model, SpatialRefSysMixin): "Maps to the Oracle MDSYS.CS_SRS table." - cs_name = models.CharField(maxlength=68) + cs_name = models.CharField(max_length=68) srid = models.IntegerField(primary_key=True) auth_srid = models.IntegerField() - auth_name = models.CharField(maxlength=256) - wktext = models.CharField(maxlength=2046) + auth_name = models.CharField(max_length=256) + wktext = models.CharField(max_length=2046) #cs_bounds = models.GeometryField() class Meta: diff --git a/django/contrib/gis/db/backend/oracle/query.py b/django/contrib/gis/db/backend/oracle/query.py index b2982183ac..7303eacf2d 100644 --- a/django/contrib/gis/db/backend/oracle/query.py +++ b/django/contrib/gis/db/backend/oracle/query.py @@ -5,6 +5,7 @@ import re from decimal import Decimal from django.db import connection +from django.contrib.gis.db.backend.util import SpatialFunction from django.contrib.gis.measure import Distance qn = connection.ops.quote_name @@ -14,61 +15,44 @@ DISTANCE = 'SDO_GEOM.SDO_DISTANCE' TRANSFORM = 'SDO_CS.TRANSFORM' UNION = 'SDO_AGGR_UNION' -class SDOOperation(object): +# We want to get SDO Geometries as WKT because it is much easier to +# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings. +# However, this adversely affects performance (i.e., Java is called +# to convert to WKT on every query). If someone wishes to write a +# SDO_GEOMETRY(...) parser in Python, let me know =) +GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)' + +#### Classes used in constructing Oracle spatial SQL #### +class SDOOperation(SpatialFunction): "Base class for SDO* Oracle operations." + def __init__(self, func, end_subst=") %s '%s'"): + super(SDOOperation, self).__init__(func, end_subst=end_subst, operator='=', result='TRUE') - def __init__(self, lookup, subst='', operator='=', result="'TRUE'", - beg_subst='%s(%s%s, %%s'): - self.lookup = lookup - self.subst = subst - self.operator = operator - self.result = result - self.beg_subst = beg_subst - self.end_subst = ') %s %s' % (self.operator, self.result) - - @property - def sql_subst(self): - return ''.join([self.beg_subst, self.subst, self.end_subst]) - - def as_sql(self, table, field): - return self.sql_subst % self.params(table, field) - - def params(self, table, field): - return (self.lookup, table, field) - -class SDODistance(SDOOperation): +class SDODistance(SpatialFunction): "Class for Distance queries." def __init__(self, op, tolerance=0.05): - super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s') - self.tolerance = tolerance + super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance, + operator=op, result='%%s') - def params(self, table, field): - return (self.lookup, table, field, self.tolerance) - -class SDOGeomRelate(SDOOperation): +class SDOGeomRelate(SpatialFunction): "Class for using SDO_GEOM.RELATE." def __init__(self, mask, tolerance=0.05): - super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst="%s(%s%s, '%s'", - subst=", %%s, %s", result="'%s'" % mask) - self.mask = mask - self.tolerance = tolerance + # SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance. + # Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE'). + end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask) + beg_subst = "%%s(%%s, '%s'" % mask + super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst) - def params(self, table, field): - return (self.lookup, table, field, self.mask, self.tolerance) - -class SDORelate(SDOOperation): +class SDORelate(SpatialFunction): "Class for using SDO_RELATE." masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON' mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I) - - def __init__(self, mask, **kwargs): - super(SDORelate, self).__init__('SDO_RELATE', subst=", 'mask=%s'", **kwargs) + def __init__(self, mask): if not self.mask_regex.match(mask): raise ValueError('Invalid %s mask: "%s"' % (self.lookup, mask)) - self.mask = mask + super(SDORelate, self).__init__('SDO_RELATE', end_subst=", 'mask=%s') = 'TRUE'" % mask) - def params(self, table, field): - return (self.lookup, table, field, self.mask) +#### Lookup type mapping dictionaries of Oracle spatial operations #### # Valid distance types and substitutions dtypes = (Decimal, Distance, float, int) @@ -84,7 +68,7 @@ ORACLE_GEOMETRY_FUNCTIONS = { 'coveredby' : SDOOperation('SDO_COVEREDBY'), 'covers' : SDOOperation('SDO_COVERS'), 'disjoint' : SDOGeomRelate('DISJOINT'), - 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', "%%s, 'distance=%%s'"), dtypes), + 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', end_subst=", %%s, 'distance=%%s') %s '%s'"), dtypes), 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()? 'equals' : SDOOperation('SDO_EQUAL'), 'exact' : SDOOperation('SDO_EQUAL'), @@ -104,20 +88,20 @@ ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys() ORACLE_SPATIAL_TERMS += MISC_TERMS ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable +#### The `get_geo_where_clause` function for Oracle #### def get_geo_where_clause(lookup_type, table_prefix, field_name, value): "Returns the SQL WHERE clause for use in Oracle spatial SQL construction." - if table_prefix.endswith('.'): - table_prefix = qn(table_prefix[:-1])+'.' - field_name = qn(field_name) + # Getting the quoted table name as `geo_col`. + geo_col = '%s.%s' % (qn(table_prefix), qn(field_name)) # See if a Oracle Geometry function matches the lookup type next lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False) if lookup_info: # Lookup types that are tuples take tuple arguments, e.g., 'relate' and - # 'dwithin' lookup types. + # 'dwithin' lookup types. if isinstance(lookup_info, tuple): # First element of tuple is lookup type, second element is the type - # of the expected argument (e.g., str, float) + # of the expected argument (e.g., str, float) sdo_op, arg_type = lookup_info # Ensuring that a tuple _value_ was passed in from the user @@ -131,27 +115,19 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value): raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))) if lookup_type == 'relate': - # The SDORelate class handles construction for these queries, and verifies - # the mask argument. - return sdo_op(value[1]).as_sql(table_prefix, field_name) - elif lookup_type in DISTANCE_FUNCTIONS: - op = DISTANCE_FUNCTIONS[lookup_type][0] - return op.as_sql(table_prefix, field_name) - # return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op) + # The SDORelate class handles construction for these queries, + # and verifies the mask argument. + return sdo_op(value[1]).as_sql(geo_col) else: - return sdo_op.as_sql(table_prefix, field_name) + # Otherwise, just call the `as_sql` method on the SDOOperation instance. + return sdo_op.as_sql(geo_col) else: # Lookup info is a SDOOperation instance, whos `as_sql` method returns # the SQL necessary for the geometry function call. For example: # SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE' - return lookup_info.as_sql(table_prefix, field_name) - - # Handling 'isnull' lookup type - if lookup_type == 'isnull': - return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) + return lookup_info.as_sql(geo_col) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) - -# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies -# from WKT than SDO_GEOMETRY(...) strings ;) -GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)' diff --git a/django/contrib/gis/db/backend/postgis/creation.py b/django/contrib/gis/db/backend/postgis/creation.py index 994edb10e7..a3884db187 100644 --- a/django/contrib/gis/db/backend/postgis/creation.py +++ b/django/contrib/gis/db/backend/postgis/creation.py @@ -45,6 +45,7 @@ def _create_with_cursor(db_name, verbosity=1, autoclobber=False): try: # Trying to create the database first. cursor.execute(create_sql) + #print create_sql except Exception, e: # Drop and recreate, if necessary. if not autoclobber: @@ -56,6 +57,7 @@ def _create_with_cursor(db_name, verbosity=1, autoclobber=False): cursor.execute(create_sql) else: raise Exception('Spatial Database Creation canceled.') +foo = _create_with_cursor created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists') def _create_with_shell(db_name, verbosity=1, autoclobber=False): diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py index 77712e7005..cb7efd5eaa 100644 --- a/django/contrib/gis/db/backend/postgis/field.py +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -1,11 +1,11 @@ from types import 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.util import get_srid, GeoFieldSQL +from django.contrib.gis.geos import GEOSGeometry +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 \ - DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM + DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM # Quotename & geographic quotename, respectively qn = connection.ops.quote_name diff --git a/django/contrib/gis/db/backend/postgis/models.py b/django/contrib/gis/db/backend/postgis/models.py index 5e45bd7ac4..e032da4d89 100644 --- a/django/contrib/gis/db/backend/postgis/models.py +++ b/django/contrib/gis/db/backend/postgis/models.py @@ -14,13 +14,13 @@ class GeometryColumns(models.Model): The 'geometry_columns' table from the PostGIS. See the PostGIS documentation at Ch. 4.2.2. """ - f_table_catalog = models.CharField(maxlength=256) - f_table_schema = models.CharField(maxlength=256) - f_table_name = models.CharField(maxlength=256) - f_geometry_column = models.CharField(maxlength=256) + f_table_catalog = models.CharField(max_length=256) + f_table_schema = models.CharField(max_length=256) + f_table_name = models.CharField(max_length=256) + f_geometry_column = models.CharField(max_length=256) coord_dimension = models.IntegerField() srid = models.IntegerField(primary_key=True) - type = models.CharField(maxlength=30) + type = models.CharField(max_length=30) class Meta: db_table = 'geometry_columns' @@ -41,10 +41,10 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin): documentaiton at Ch. 4.2.1. """ srid = models.IntegerField(primary_key=True) - auth_name = models.CharField(maxlength=256) + auth_name = models.CharField(max_length=256) auth_srid = models.IntegerField() - srtext = models.CharField(maxlength=2048) - proj4text = models.CharField(maxlength=2048) + srtext = models.CharField(max_length=2048) + proj4text = models.CharField(max_length=2048) class Meta: db_table = 'spatial_ref_sys' diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index 76d75b3536..8387022004 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -2,10 +2,12 @@ This module contains the spatial lookup types, and the get_geo_where_clause() routine for PostGIS. """ +import re from decimal import Decimal from django.db import connection from django.contrib.gis.measure import Distance from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple +from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction qn = connection.ops.quote_name # Getting the PostGIS version information @@ -17,60 +19,23 @@ POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1): raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION) -# PostGIS-specific operators. The commented descriptions of these -# operators come from Section 6.2.2 of the official PostGIS documentation. -POSTGIS_OPERATORS = { - # The "&<" operator returns true if A's bounding box overlaps or - # is to the left of B's bounding box. - 'overlaps_left' : '&<', - # The "&>" operator returns true if A's bounding box overlaps or - # is to the right of B's bounding box. - 'overlaps_right' : '&>', - # The "<<" operator returns true if A's bounding box is strictly - # to the left of B's bounding box. - 'left' : '<<', - # The ">>" operator returns true if A's bounding box is strictly - # to the right of B's bounding box. - 'right' : '>>', - # The "&<|" operator returns true if A's bounding box overlaps or - # is below B's bounding box. - 'overlaps_below' : '&<|', - # The "|&>" operator returns true if A's bounding box overlaps or - # is above B's bounding box. - 'overlaps_above' : '|&>', - # The "<<|" operator returns true if A's bounding box is strictly - # below B's bounding box. - 'strictly_below' : '<<|', - # The "|>>" operator returns true if A's bounding box is strictly - # above B's bounding box. - 'strictly_above' : '|>>', - # The "~=" operator is the "same as" operator. It tests actual - # geometric equality of two features. So if A and B are the same feature, - # vertex-by-vertex, the operator returns true. - 'same_as' : '~=', - 'exact' : '~=', - # The "@" operator returns true if A's bounding box is completely contained - # by B's bounding box. - 'contained' : '@', - # The "~" operator returns true if A's bounding box completely contains - # by B's bounding box. - 'bbcontains' : '~', - # The "&&" operator returns true if A's bounding box overlaps - # B's bounding box. - 'bboverlaps' : '&&', - } - # Versions of PostGIS >= 1.2.2 changed their naming convention to be -# 'SQL-MM-centric' to conform with the ISO standard. Practically, this +# 'SQL-MM-centric' to conform with the ISO standard. Practically, this # means that 'ST_' is prefixes geometry function names. GEOM_FUNC_PREFIX = '' if MAJOR_VERSION >= 1: - if (MINOR_VERSION1 > 2 or + if (MINOR_VERSION1 > 2 or (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)): GEOM_FUNC_PREFIX = 'ST_' - + def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func) + # 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 + # Functions used by the GeoManager & GeoQuerySet ASKML = get_func('AsKML') ASGML = get_func('AsGML') @@ -90,48 +55,115 @@ if MAJOR_VERSION >= 1: else: raise NotImplementedError('PostGIS versions < 1.0 are not supported.') +#### Classes used in constructing PostGIS spatial SQL #### +class PostGISOperator(SpatialOperation): + "For PostGIS operators (e.g. `&&`, `~`)." + def __init__(self, operator): + super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s') + +class PostGISFunction(SpatialFunction): + "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)." + def __init__(self, function, **kwargs): + super(PostGISFunction, self).__init__(get_func(function), **kwargs) + +class PostGISFunctionParam(PostGISFunction): + "For PostGIS functions that take another parameter (e.g. DWithin, Relate)." + def __init__(self, func): + super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)') + +class PostGISDistance(PostGISFunction): + "For PostGIS distance operations." + def __init__(self, operator): + super(PostGISDistance, self).__init__('Distance', end_subst=') %s %s', operator=operator, result='%%s') + +class PostGISRelate(PostGISFunctionParam): + "For PostGIS Relate(, ) calls." + pattern_regex = re.compile(r'^[012TF\*]{9}$') + def __init__(self, pattern): + if not self.pattern_regex.match(pattern): + raise ValueError('Invalid intersection matrix pattern "%s".' % pattern) + super(PostGISRelate, self).__init__('Relate') + +#### Lookup type mapping dictionaries of PostGIS operations. #### + +# PostGIS-specific operators. The commented descriptions of these +# operators come from Section 6.2.2 of the official PostGIS documentation. +POSTGIS_OPERATORS = { + # The "&<" operator returns true if A's bounding box overlaps or + # is to the left of B's bounding box. + 'overlaps_left' : PostGISOperator('&<'), + # The "&>" operator returns true if A's bounding box overlaps or + # is to the right of B's bounding box. + 'overlaps_right' : PostGISOperator('&>'), + # The "<<" operator returns true if A's bounding box is strictly + # to the left of B's bounding box. + 'left' : PostGISOperator('<<'), + # The ">>" operator returns true if A's bounding box is strictly + # to the right of B's bounding box. + 'right' : PostGISOperator('>>'), + # The "&<|" operator returns true if A's bounding box overlaps or + # is below B's bounding box. + 'overlaps_below' : PostGISOperator('&<|'), + # The "|&>" operator returns true if A's bounding box overlaps or + # is above B's bounding box. + 'overlaps_above' : PostGISOperator('|&>'), + # The "<<|" operator returns true if A's bounding box is strictly + # below B's bounding box. + 'strictly_below' : PostGISOperator('<<|'), + # The "|>>" operator returns true if A's bounding box is strictly + # above B's bounding box. + 'strictly_above' : PostGISOperator('|>>'), + # The "~=" operator is the "same as" operator. It tests actual + # geometric equality of two features. So if A and B are the same feature, + # vertex-by-vertex, the operator returns true. + 'same_as' : PostGISOperator('~='), + 'exact' : PostGISOperator('~='), + # The "@" operator returns true if A's bounding box is completely contained + # by B's bounding box. + 'contained' : PostGISOperator('@'), + # The "~" operator returns true if A's bounding box completely contains + # by B's bounding box. + 'bbcontains' : PostGISOperator('~'), + # The "&&" operator returns true if A's bounding box overlaps + # B's bounding box. + 'bboverlaps' : PostGISOperator('&&'), + } + # For PostGIS >= 1.2.2 the following lookup types will do a bounding box query # first before calling the more computationally expensive GEOS routines (called # "inline index magic"): # 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and # 'covers'. POSTGIS_GEOMETRY_FUNCTIONS = { - 'equals' : 'Equals', - 'disjoint' : 'Disjoint', - 'touches' : 'Touches', - 'crosses' : 'Crosses', - 'within' : 'Within', - 'overlaps' : 'Overlaps', - 'contains' : 'Contains', - 'intersects' : 'Intersects', - 'relate' : ('Relate', basestring), + 'equals' : PostGISFunction('Equals'), + 'disjoint' : PostGISFunction('Disjoint'), + 'touches' : PostGISFunction('Touches'), + 'crosses' : PostGISFunction('Crosses'), + 'within' : PostGISFunction('Within'), + 'overlaps' : PostGISFunction('Overlaps'), + 'contains' : PostGISFunction('Contains'), + 'intersects' : PostGISFunction('Intersects'), + 'relate' : (PostGISRelate, basestring), } # Valid distance types and substitutions dtypes = (Decimal, Distance, float, int) DISTANCE_FUNCTIONS = { - 'distance_gt' : ('>', dtypes), - 'distance_gte' : ('>=', dtypes), - 'distance_lt' : ('<', dtypes), - 'distance_lte' : ('<=', dtypes), + 'distance_gt' : (PostGISDistance('>'), dtypes), + 'distance_gte' : (PostGISDistance('>='), dtypes), + 'distance_lt' : (PostGISDistance('<'), dtypes), + 'distance_lte' : (PostGISDistance('<='), dtypes), } if GEOM_FUNC_PREFIX == 'ST_': - # Adding the GEOM_FUNC_PREFIX to the lookup functions. - for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items(): - if isinstance(f, tuple): - POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1]) - else: - POSTGIS_GEOMETRY_FUNCTIONS[lookup] = get_func(f) - # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+ POSTGIS_GEOMETRY_FUNCTIONS.update( - {'dwithin' : ('ST_DWithin', dtypes), - 'coveredby' : 'ST_CoveredBy', - 'covers' : 'ST_Covers', - } - ) + {'dwithin' : (PostGISFunctionParam('DWithin'), dtypes), + 'coveredby' : PostGISFunction('CoveredBy'), + 'covers' : PostGISFunction('Covers'), + }) +# Distance functions are a part of PostGIS geometry functions. POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) # Any other lookup types that do not require a mapping. @@ -144,33 +176,25 @@ POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Func POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable -### PostGIS-specific Methods ### -def get_geom_func(lookup_type): - func_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] - if isinstance(func_info, tuple): - return func_info[0] - else: - return func_info - +#### The `get_geo_where_clause` function for PostGIS. #### 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 = qn(table_prefix[:-1])+'.' - field_name = qn(field_name) - - # See if a PostGIS operator matches the lookup type first + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(table_prefix), qn(field_name)) if lookup_type in POSTGIS_OPERATORS: - return '%s%s %s %%s' % (table_prefix, field_name, POSTGIS_OPERATORS[lookup_type]) + # See if a PostGIS operator matches the lookup type. + return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col) + elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS: + # See if a PostGIS geometry function matches the lookup type. + tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] - # See if a PostGIS Geometry function matches the lookup type next - if lookup_type in POSTGIS_GEOMETRY_FUNCTIONS: - lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] # Lookup types that are tuples take tuple arguments, e.g., 'relate' and - # 'dwithin' lookup types. - if isinstance(lookup_info, tuple): - # First element of tuple is lookup type, second element is the type - # of the expected argument (e.g., str, float) - func, arg_type = lookup_info + # distance lookups. + if isinstance(tmp, tuple): + # First element of tuple is the PostGISOperation instance, and the + # second element is either the type or a tuple of acceptable types + # that may passed in as further parameters for the lookup type. + op, arg_type = tmp # Ensuring that a tuple _value_ was passed in from the user if not isinstance(value, tuple): @@ -182,24 +206,15 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value): if not isinstance(value[1], arg_type): raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))) - if lookup_type in DISTANCE_FUNCTIONS: - op = DISTANCE_FUNCTIONS[lookup_type][0] - return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op) - else: - return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name) + # For lookup type `relate`, the op instance is not yet created (has + # to be instantiated here to check the pattern parameter). + if lookup_type == 'relate': op = op(value[1]) else: - # Returning the SQL necessary for the geometry function call. For example: - # ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..)) - return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name) - - # Handling 'isnull' lookup type - if lookup_type == 'isnull': - return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) + op = tmp + # Calling the `as_sql` function on the operation instance. + return op.as_sql(geo_col) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) - -# 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 index b744d00304..384fe4de02 100644 --- a/django/contrib/gis/db/backend/util.py +++ b/django/contrib/gis/db/backend/util.py @@ -8,14 +8,52 @@ class GeoFieldSQL(object): self.params = params def __str__(self): - return self.where[0] % tuple(self.params) + return self.as_sql() -def get_srid(field, geom): + def as_sql(self, quote=False): + if not quote: + return self.where[0] % tuple(self.params) + else: + # Used for quoting WKT on certain backends. + tmp_params = ["'%s'" % self.params[0]] + tmp_params.extend(self.params[1:]) + return self.where[0] % tuple(tmp_params) + +class SpatialOperation(object): """ - Gets the SRID depending on the value of the SRID setting of the field - and that of the given geometry. + Base class for generating spatial SQL. """ - if geom.srid is None or (geom.srid == -1 and field._srid != -1): - return field._srid - else: - return geom.srid + def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''): + self.function = function + self.operator = operator + self.result = result + self.beg_subst = beg_subst + try: + # Try and put the operator and result into to the + # end substitution. + self.end_subst = end_subst % (operator, result) + except TypeError: + self.end_subst = end_subst + + @property + def sql_subst(self): + return ''.join([self.beg_subst, self.end_subst]) + + def as_sql(self, geo_col): + return self.sql_subst % self.params(geo_col) + + def params(self, geo_col): + return (geo_col, self.operator) + +class SpatialFunction(SpatialOperation): + """ + Base class for generating spatial SQL related to a function. + """ + def __init__(self, func, beg_subst='%s(%s, %%s', end_subst=')', result='', operator=''): + # Getting the function prefix. + kwargs = {'function' : func, 'operator' : operator, 'result' : result, + 'beg_subst' : beg_subst, 'end_subst' : end_subst,} + super(SpatialFunction, self).__init__(**kwargs) + + def params(self, geo_col): + return (self.function, geo_col) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 61893fba67..1e5a4f0f5e 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -7,11 +7,12 @@ 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, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION + ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION, VERSION from django.contrib.gis.geos import GEOSGeometry -# Flag indicating whether the backend is Oracle. +# Shortcut booleans for determining the backend. oracle = SPATIAL_BACKEND == 'oracle' +postgis = SPATIAL_BACKEND == 'postgis' class GeoQ(Q): "Geographical query encapsulation object." @@ -325,8 +326,14 @@ class GeoQuerySet(QuerySet): if oracle: gml_select = {'gml':'%s(%s)' % (ASGML, field_col)} - else: - gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)} + elif postgis: + # PostGIS AsGML() aggregate function parameter order depends on the + # version -- uggh. + major, minor1, minor2 = VERSION + if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)): + gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, field_col, precision)} + else: + gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)} # Adding GML function call to SELECT part of the SQL. return self.extra(select=gml_select)