diff options
| author | Justin Bronn <jbronn@gmail.com> | 2007-12-15 00:30:48 +0000 |
|---|---|---|
| committer | Justin Bronn <jbronn@gmail.com> | 2007-12-15 00:30:48 +0000 |
| commit | 5799c2e048ff829300af88ae839de20e1763ee1d (patch) | |
| tree | 793f707e394361f18453f29857d3a9e61010ab55 | |
| parent | 9270d47d80a4ffa09659d26ae71559c20384ffed (diff) | |
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
| -rw-r--r-- | django/contrib/gis/db/backend/__init__.py | 8 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/mysql/query.py | 19 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/oracle/field.py | 2 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/oracle/models.py | 10 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/oracle/query.py | 104 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/postgis/creation.py | 2 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/postgis/field.py | 6 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/postgis/models.py | 16 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/postgis/query.py | 231 | ||||
| -rw-r--r-- | django/contrib/gis/db/backend/util.py | 54 | ||||
| -rw-r--r-- | django/contrib/gis/db/models/query.py | 15 |
11 files changed, 253 insertions, 214 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):
- "Base class for SDO* Oracle operations."
-
- 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)
+# 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)'
- def params(self, table, field):
- return (self.lookup, table, field)
+#### 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')
-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
-
- def params(self, table, field):
- return (self.lookup, table, field, self.tolerance)
+ super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance,
+ operator=op, result='%%s')
-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
-
- def params(self, table, field):
- return (self.lookup, table, field, self.mask, self.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)
-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,121 +19,151 @@ 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) +# Versions of PostGIS >= 1.2.2 changed their naming convention to be +# '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 + (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') + DISTANCE = get_func('Distance') + GEOM_FROM_TEXT = get_func('GeomFromText') + GEOM_FROM_WKB = get_func('GeomFromWKB') + TRANSFORM = get_func('Transform') + + # Special cases for union and KML methods. + if MINOR_VERSION1 < 3: + UNION = 'GeomUnion' + else: + UNION = 'ST_Union' + + if MINOR_VERSION1 == 1: + ASKML = False +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(<geom>, <pattern>) 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' : '&<', + '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' : '&>', + 'overlaps_right' : PostGISOperator('&>'), # The "<<" operator returns true if A's bounding box is strictly # to the left of B's bounding box. - 'left' : '<<', + 'left' : PostGISOperator('<<'), # The ">>" operator returns true if A's bounding box is strictly # to the right of B's bounding box. - 'right' : '>>', + 'right' : PostGISOperator('>>'), # The "&<|" operator returns true if A's bounding box overlaps or # is below B's bounding box. - 'overlaps_below' : '&<|', + 'overlaps_below' : PostGISOperator('&<|'), # The "|&>" operator returns true if A's bounding box overlaps or # is above B's bounding box. - 'overlaps_above' : '|&>', + 'overlaps_above' : PostGISOperator('|&>'), # The "<<|" operator returns true if A's bounding box is strictly # below B's bounding box. - 'strictly_below' : '<<|', + 'strictly_below' : PostGISOperator('<<|'), # The "|>>" operator returns true if A's bounding box is strictly # above B's bounding box. - 'strictly_above' : '|>>', + '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' : '~=', - 'exact' : '~=', + 'same_as' : PostGISOperator('~='), + 'exact' : PostGISOperator('~='), # The "@" operator returns true if A's bounding box is completely contained # by B's bounding box. - 'contained' : '@', + 'contained' : PostGISOperator('@'), # The "~" operator returns true if A's bounding box completely contains # by B's bounding box. - 'bbcontains' : '~', + 'bbcontains' : PostGISOperator('~'), # The "&&" operator returns true if A's bounding box overlaps # B's bounding box. - 'bboverlaps' : '&&', + 'bboverlaps' : PostGISOperator('&&'), } -# Versions of PostGIS >= 1.2.2 changed their naming convention to be -# '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 - (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)): - GEOM_FUNC_PREFIX = 'ST_' - - def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func) - - # Functions used by the GeoManager & GeoQuerySet - ASKML = get_func('AsKML') - ASGML = get_func('AsGML') - DISTANCE = get_func('Distance') - GEOM_FROM_TEXT = get_func('GeomFromText') - GEOM_FROM_WKB = get_func('GeomFromWKB') - TRANSFORM = get_func('Transform') - - # Special cases for union and KML methods. - if MINOR_VERSION1 < 3: - UNION = 'GeomUnion' - else: - UNION = 'ST_Union' - - if MINOR_VERSION1 == 1: - ASKML = False -else: - raise NotImplementedError('PostGIS versions < 1.0 are not supported.') - # 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): + """ + Base class for generating spatial SQL. + """ + 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): """ - 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 related to a function. """ - if geom.srid is None or (geom.srid == -1 and field._srid != -1): - return field._srid - else: - return geom.srid + 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) |
