Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

gis: Applied DRY to spatial SQL generation in anticipation of queryse…

…t-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
  • Loading branch information...
commit 5799c2e048ff829300af88ae839de20e1763ee1d 1 parent 9270d47
Justin Bronn authored December 15, 2007
8  django/contrib/gis/db/backend/__init__.py
@@ -26,7 +26,7 @@
26 26
 from django.contrib.gis.geos import GEOSGeometry
27 27
 
28 28
 # These routines (needed by GeoManager), default to False.
29  
-ASGML, ASKML, DISTANCE, TRANSFORM, UNION= (False, False, False, False, False)
  29
+ASGML, ASKML, DISTANCE, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False)
30 30
 
31 31
 if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
32 32
     # PostGIS is the spatial database, getting the rquired modules, 
@@ -34,7 +34,9 @@
34 34
     from django.contrib.gis.db.backend.postgis import \
35 35
         PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
36 36
         create_spatial_db, get_geo_where_clause, \
37  
-        ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
  37
+        ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION, \
  38
+        MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
  39
+    VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
38 40
     SPATIAL_BACKEND = 'postgis'
39 41
 elif settings.DATABASE_ENGINE == 'oracle':
40 42
     from django.contrib.gis.db.backend.oracle import \
@@ -283,7 +285,7 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
283 285
             geo_prep = field.get_db_prep_lookup(lookup_type, value)
284 286
             
285 287
             # Getting the adapted geometry from the field.
286  
-            gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value)
  288
+            gwc = get_geo_where_clause(lookup_type, current_table, column, value)
287 289
 
288 290
             # Substituting in the the where parameters into the geographic where
289 291
             # clause, and extending the parameters.
19  django/contrib/gis/db/backend/mysql/query.py
@@ -5,6 +5,11 @@
5 5
 from django.db import connection
6 6
 qn = connection.ops.quote_name
7 7
 
  8
+# To ease implementation, WKT is passed to/from MySQL.
  9
+GEOM_FROM_TEXT = 'GeomFromText'
  10
+GEOM_FROM_WKB = 'GeomFromWKB'
  11
+GEOM_SELECT = 'AsText(%s)'
  12
+
8 13
 # WARNING: MySQL is NOT compliant w/the OpenGIS specification and
9 14
 # _every_ one of these lookup types is on the _bounding box_ only.
10 15
 MYSQL_GIS_FUNCTIONS = {
@@ -31,24 +36,18 @@
31 36
 
32 37
 def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
33 38
     "Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
34  
-    if table_prefix.endswith('.'):
35  
-        table_prefix = qn(table_prefix[:-1])+'.'
36  
-    field_name = qn(field_name)
  39
+    # Getting the quoted field as `geo_col`.
  40
+    geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
37 41
 
38 42
     # See if a MySQL Geometry function matches the lookup type next
39 43
     lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
40 44
     if lookup_info:
41  
-        return "%s(%s, %%s)" % (lookup_info, table_prefix + field_name)
  45
+        return "%s(%s, %%s)" % (lookup_info, geo_col)
42 46
     
43 47
     # Handling 'isnull' lookup type
44 48
     # TODO: Is this needed because MySQL cannot handle NULL
45 49
     # geometries in its spatial indices.
46 50
     if lookup_type == 'isnull':
47  
-        return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
  51
+        return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
48 52
 
49 53
     raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
50  
-
51  
-# To ease implementation, WKT is passed to/from MySQL.
52  
-GEOM_FROM_TEXT = 'GeomFromText'
53  
-GEOM_FROM_WKB = 'GeomFromWKB'
54  
-GEOM_SELECT = 'AsText(%s)'
2  django/contrib/gis/db/backend/oracle/field.py
@@ -4,7 +4,7 @@
4 4
 from django.db.backends.util import truncate_name
5 5
 from django.db.models.fields import Field # Django base Field class
6 6
 from django.contrib.gis.geos import GEOSGeometry
7  
-from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
  7
+from django.contrib.gis.db.backend.util import GeoFieldSQL
8 8
 from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
9 9
 from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM
10 10
 
10  django/contrib/gis/db/backend/oracle/models.py
@@ -12,8 +12,8 @@
12 12
 
13 13
 class GeometryColumns(models.Model):
14 14
     "Maps to the Oracle USER_SDO_GEOM_METADATA table."
15  
-    table_name = models.CharField(maxlength=32)
16  
-    column_name = models.CharField(maxlength=1024)
  15
+    table_name = models.CharField(max_length=32)
  16
+    column_name = models.CharField(max_length=1024)
17 17
     srid = models.IntegerField(primary_key=True)
18 18
     # TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
19 19
     class Meta:
@@ -28,11 +28,11 @@ def __unicode__(self):
28 28
 
29 29
 class SpatialRefSys(models.Model, SpatialRefSysMixin):
30 30
     "Maps to the Oracle MDSYS.CS_SRS table."
31  
-    cs_name = models.CharField(maxlength=68)
  31
+    cs_name = models.CharField(max_length=68)
32 32
     srid = models.IntegerField(primary_key=True)
33 33
     auth_srid = models.IntegerField()
34  
-    auth_name = models.CharField(maxlength=256)
35  
-    wktext = models.CharField(maxlength=2046)
  34
+    auth_name = models.CharField(max_length=256)
  35
+    wktext = models.CharField(max_length=2046)
36 36
     #cs_bounds = models.GeometryField()
37 37
 
38 38
     class Meta:
104  django/contrib/gis/db/backend/oracle/query.py
@@ -5,6 +5,7 @@
5 5
 import re
6 6
 from decimal import Decimal
7 7
 from django.db import connection
  8
+from django.contrib.gis.db.backend.util import SpatialFunction
8 9
 from django.contrib.gis.measure import Distance
9 10
 qn = connection.ops.quote_name
10 11
 
@@ -14,61 +15,44 @@
14 15
 TRANSFORM = 'SDO_CS.TRANSFORM'
15 16
 UNION = 'SDO_AGGR_UNION'
16 17
 
17  
-class SDOOperation(object):
18  
-    "Base class for SDO* Oracle operations."
19  
-
20  
-    def __init__(self, lookup, subst='', operator='=', result="'TRUE'",
21  
-                 beg_subst='%s(%s%s, %%s'):
22  
-        self.lookup = lookup
23  
-        self.subst = subst
24  
-        self.operator = operator
25  
-        self.result = result
26  
-        self.beg_subst = beg_subst
27  
-        self.end_subst = ') %s %s' % (self.operator, self.result)
28  
-
29  
-    @property
30  
-    def sql_subst(self):
31  
-        return ''.join([self.beg_subst, self.subst, self.end_subst])
32  
-
33  
-    def as_sql(self, table, field):
34  
-        return self.sql_subst % self.params(table, field)
  18
+# We want to get SDO Geometries as WKT because it is much easier to 
  19
+# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.  
  20
+# However, this adversely affects performance (i.e., Java is called 
  21
+# to convert to WKT on every query).  If someone wishes to write a 
  22
+# SDO_GEOMETRY(...) parser in Python, let me know =)
  23
+GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
35 24
 
36  
-    def params(self, table, field):
37  
-        return (self.lookup, table, field)
  25
+#### Classes used in constructing Oracle spatial SQL ####
  26
+class SDOOperation(SpatialFunction):
  27
+    "Base class for SDO* Oracle operations."
  28
+    def __init__(self, func, end_subst=") %s '%s'"):
  29
+        super(SDOOperation, self).__init__(func, end_subst=end_subst, operator='=', result='TRUE')
38 30
 
39  
-class SDODistance(SDOOperation):
  31
+class SDODistance(SpatialFunction):
40 32
     "Class for Distance queries."
41 33
     def __init__(self, op, tolerance=0.05):
42  
-        super(SDODistance, self).__init__(DISTANCE, subst=", %s", operator=op, result='%%s')
43  
-        self.tolerance = tolerance
44  
-
45  
-    def params(self, table, field):
46  
-        return (self.lookup, table, field, self.tolerance)
  34
+        super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance, 
  35
+                                          operator=op, result='%%s')
47 36
 
48  
-class SDOGeomRelate(SDOOperation):
  37
+class SDOGeomRelate(SpatialFunction):
49 38
     "Class for using SDO_GEOM.RELATE."
50 39
     def __init__(self, mask, tolerance=0.05):
51  
-        super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE',  beg_subst="%s(%s%s, '%s'",
52  
-                                            subst=", %%s, %s", result="'%s'" % mask)
53  
-        self.mask = mask
54  
-        self.tolerance = tolerance
55  
-
56  
-    def params(self, table, field):
57  
-        return (self.lookup, table, field, self.mask, self.tolerance)
  40
+        # SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance.
  41
+        # Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE').
  42
+        end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask)
  43
+        beg_subst = "%%s(%%s, '%s'" % mask 
  44
+        super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst)
58 45
 
59  
-class SDORelate(SDOOperation):
  46
+class SDORelate(SpatialFunction):
60 47
     "Class for using SDO_RELATE."
61 48
     masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
62 49
     mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
63  
-    
64  
-    def __init__(self, mask, **kwargs):
65  
-        super(SDORelate, self).__init__('SDO_RELATE',  subst=", 'mask=%s'", **kwargs)
  50
+    def __init__(self, mask):
66 51
         if not self.mask_regex.match(mask):
67 52
             raise ValueError('Invalid %s mask: "%s"' % (self.lookup, mask))
68  
-        self.mask = mask
  53
+        super(SDORelate, self).__init__('SDO_RELATE', end_subst=", 'mask=%s') = 'TRUE'" % mask)
69 54
 
70  
-    def params(self, table, field):
71  
-        return (self.lookup, table, field, self.mask)
  55
+#### Lookup type mapping dictionaries of Oracle spatial operations ####
72 56
 
73 57
 # Valid distance types and substitutions
74 58
 dtypes = (Decimal, Distance, float, int)
@@ -84,7 +68,7 @@ def params(self, table, field):
84 68
     'coveredby' : SDOOperation('SDO_COVEREDBY'),
85 69
     'covers' : SDOOperation('SDO_COVERS'),
86 70
     'disjoint' : SDOGeomRelate('DISJOINT'),
87  
-    'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', "%%s, 'distance=%%s'"), dtypes),
  71
+    'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', end_subst=", %%s, 'distance=%%s') %s '%s'"), dtypes),
88 72
     'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
89 73
     'equals' : SDOOperation('SDO_EQUAL'),
90 74
     'exact' : SDOOperation('SDO_EQUAL'),
@@ -104,20 +88,20 @@ def params(self, table, field):
104 88
 ORACLE_SPATIAL_TERMS += MISC_TERMS
105 89
 ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
106 90
 
  91
+#### The `get_geo_where_clause` function for Oracle ####
107 92
 def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
108 93
     "Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
109  
-    if table_prefix.endswith('.'):
110  
-        table_prefix = qn(table_prefix[:-1])+'.'
111  
-    field_name = qn(field_name)
  94
+    # Getting the quoted table name as `geo_col`.
  95
+    geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
112 96
 
113 97
     # See if a Oracle Geometry function matches the lookup type next
114 98
     lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
115 99
     if lookup_info:
116 100
         # Lookup types that are tuples take tuple arguments, e.g., 'relate' and 
117  
-        #  'dwithin' lookup types.
  101
+        # 'dwithin' lookup types.
118 102
         if isinstance(lookup_info, tuple):
119 103
             # First element of tuple is lookup type, second element is the type
120  
-            #  of the expected argument (e.g., str, float)
  104
+            # of the expected argument (e.g., str, float)
121 105
             sdo_op, arg_type = lookup_info
122 106
 
123 107
             # 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):
131 115
                 raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
132 116
 
133 117
             if lookup_type == 'relate':
134  
-                # The SDORelate class handles construction for these queries, and verifies
135  
-                # the mask argument.
136  
-                return sdo_op(value[1]).as_sql(table_prefix, field_name)
137  
-            elif lookup_type in DISTANCE_FUNCTIONS:
138  
-                op = DISTANCE_FUNCTIONS[lookup_type][0]
139  
-                return op.as_sql(table_prefix, field_name)
140  
-                #    return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
  118
+                # The SDORelate class handles construction for these queries, 
  119
+                # and verifies the mask argument.
  120
+                return sdo_op(value[1]).as_sql(geo_col)
141 121
             else:
142  
-                return sdo_op.as_sql(table_prefix, field_name)
  122
+                # Otherwise, just call the `as_sql` method on the SDOOperation instance.
  123
+                return sdo_op.as_sql(geo_col)
143 124
         else:
144 125
             # Lookup info is a SDOOperation instance, whos `as_sql` method returns
145 126
             # the SQL necessary for the geometry function call. For example:  
146 127
             #  SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
147  
-            return lookup_info.as_sql(table_prefix, field_name)
148  
-    
149  
-    # Handling 'isnull' lookup type
150  
-    if lookup_type == 'isnull':
151  
-        return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
  128
+            return lookup_info.as_sql(geo_col)
  129
+    elif lookup_type == 'isnull':
  130
+        # Handling 'isnull' lookup type
  131
+        return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
152 132
 
153 133
     raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
154  
-
155  
-# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
156  
-# from WKT than SDO_GEOMETRY(...) strings ;)
157  
-GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
2  django/contrib/gis/db/backend/postgis/creation.py
@@ -45,6 +45,7 @@ def _create_with_cursor(db_name, verbosity=1, autoclobber=False):
45 45
     try:
46 46
         # Trying to create the database first.
47 47
         cursor.execute(create_sql)
  48
+        #print create_sql
48 49
     except Exception, e:
49 50
         # Drop and recreate, if necessary.
50 51
         if not autoclobber:
@@ -56,6 +57,7 @@ def _create_with_cursor(db_name, verbosity=1, autoclobber=False):
56 57
             cursor.execute(create_sql)
57 58
         else:
58 59
             raise Exception('Spatial Database Creation canceled.')
  60
+foo = _create_with_cursor
59 61
     
60 62
 created_regex = re.compile(r'^createdb: database creation failed: ERROR:  database ".+" already exists')
61 63
 def _create_with_shell(db_name, verbosity=1, autoclobber=False):
6  django/contrib/gis/db/backend/postgis/field.py
... ...
@@ -1,11 +1,11 @@
1 1
 from types import UnicodeType
2 2
 from django.db import connection
3 3
 from django.db.models.fields import Field # Django base Field class
4  
-from django.contrib.gis.geos import GEOSGeometry, GEOSException 
5  
-from django.contrib.gis.db.backend.util import get_srid, GeoFieldSQL
  4
+from django.contrib.gis.geos import GEOSGeometry
  5
+from django.contrib.gis.db.backend.util import GeoFieldSQL
6 6
 from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
7 7
 from django.contrib.gis.db.backend.postgis.query import \
8  
-    DISTANCE, DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
  8
+    DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
9 9
 
10 10
 # Quotename & geographic quotename, respectively
11 11
 qn = connection.ops.quote_name
16  django/contrib/gis/db/backend/postgis/models.py
@@ -14,13 +14,13 @@ class GeometryColumns(models.Model):
14 14
     The 'geometry_columns' table from the PostGIS. See the PostGIS
15 15
     documentation at Ch. 4.2.2.
16 16
     """
17  
-    f_table_catalog = models.CharField(maxlength=256)
18  
-    f_table_schema = models.CharField(maxlength=256)
19  
-    f_table_name = models.CharField(maxlength=256)
20  
-    f_geometry_column = models.CharField(maxlength=256)
  17
+    f_table_catalog = models.CharField(max_length=256)
  18
+    f_table_schema = models.CharField(max_length=256)
  19
+    f_table_name = models.CharField(max_length=256)
  20
+    f_geometry_column = models.CharField(max_length=256)
21 21
     coord_dimension = models.IntegerField()
22 22
     srid = models.IntegerField(primary_key=True)
23  
-    type = models.CharField(maxlength=30)
  23
+    type = models.CharField(max_length=30)
24 24
 
25 25
     class Meta:
26 26
         db_table = 'geometry_columns'
@@ -41,10 +41,10 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
41 41
     documentaiton at Ch. 4.2.1.
42 42
     """
43 43
     srid = models.IntegerField(primary_key=True)
44  
-    auth_name = models.CharField(maxlength=256)
  44
+    auth_name = models.CharField(max_length=256)
45 45
     auth_srid = models.IntegerField()
46  
-    srtext = models.CharField(maxlength=2048)
47  
-    proj4text = models.CharField(maxlength=2048)
  46
+    srtext = models.CharField(max_length=2048)
  47
+    proj4text = models.CharField(max_length=2048)
48 48
 
49 49
     class Meta:
50 50
         db_table = 'spatial_ref_sys'
231  django/contrib/gis/db/backend/postgis/query.py
@@ -2,10 +2,12 @@
2 2
  This module contains the spatial lookup types, and the get_geo_where_clause()
3 3
  routine for PostGIS.
4 4
 """
  5
+import re
5 6
 from decimal import Decimal
6 7
 from django.db import connection
7 8
 from django.contrib.gis.measure import Distance
8 9
 from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
  10
+from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
9 11
 qn = connection.ops.quote_name
10 12
 
11 13
 # Getting the PostGIS version information
@@ -17,121 +19,151 @@
17 19
 if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
18 20
     raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION)
19 21
 
  22
+# Versions of PostGIS >= 1.2.2 changed their naming convention to be
  23
+#  'SQL-MM-centric' to conform with the ISO standard. Practically, this
  24
+#  means that 'ST_' is prefixes geometry function names.
  25
+GEOM_FUNC_PREFIX = ''
  26
+if MAJOR_VERSION >= 1:
  27
+    if (MINOR_VERSION1 > 2 or
  28
+        (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
  29
+        GEOM_FUNC_PREFIX = 'ST_'
  30
+
  31
+    def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
  32
+
  33
+    # Custom selection not needed for PostGIS since GEOS geometries may be
  34
+    # instantiated directly from the HEXEWKB returned by default.  If
  35
+    # WKT is needed for some reason in the future, this value may be changed,
  36
+    # 'AsText(%s)'
  37
+    GEOM_SELECT = None
  38
+
  39
+    # Functions used by the GeoManager & GeoQuerySet
  40
+    ASKML = get_func('AsKML')
  41
+    ASGML = get_func('AsGML')
  42
+    DISTANCE = get_func('Distance')
  43
+    GEOM_FROM_TEXT = get_func('GeomFromText')
  44
+    GEOM_FROM_WKB = get_func('GeomFromWKB')
  45
+    TRANSFORM = get_func('Transform')
  46
+
  47
+    # Special cases for union and KML methods.
  48
+    if MINOR_VERSION1 < 3:
  49
+        UNION = 'GeomUnion'
  50
+    else:
  51
+        UNION = 'ST_Union'
  52
+
  53
+    if MINOR_VERSION1 == 1:
  54
+        ASKML = False
  55
+else:
  56
+    raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
  57
+
  58
+#### Classes used in constructing PostGIS spatial SQL ####
  59
+class PostGISOperator(SpatialOperation):
  60
+    "For PostGIS operators (e.g. `&&`, `~`)."
  61
+    def __init__(self, operator):
  62
+        super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s')
  63
+
  64
+class PostGISFunction(SpatialFunction):
  65
+    "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
  66
+    def __init__(self, function, **kwargs):
  67
+        super(PostGISFunction, self).__init__(get_func(function), **kwargs)
  68
+
  69
+class PostGISFunctionParam(PostGISFunction):
  70
+    "For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
  71
+    def __init__(self, func):
  72
+        super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)')
  73
+
  74
+class PostGISDistance(PostGISFunction):
  75
+    "For PostGIS distance operations."
  76
+    def __init__(self, operator):
  77
+        super(PostGISDistance, self).__init__('Distance', end_subst=') %s %s', operator=operator, result='%%s')
  78
+
  79
+class PostGISRelate(PostGISFunctionParam):
  80
+    "For PostGIS Relate(<geom>, <pattern>) calls."
  81
+    pattern_regex = re.compile(r'^[012TF\*]{9}$')
  82
+    def __init__(self, pattern):
  83
+        if not self.pattern_regex.match(pattern):
  84
+            raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
  85
+        super(PostGISRelate, self).__init__('Relate')
  86
+
  87
+#### Lookup type mapping dictionaries of PostGIS operations. ####
  88
+
20 89
 # PostGIS-specific operators. The commented descriptions of these
21 90
 # operators come from Section 6.2.2 of the official PostGIS documentation.
22 91
 POSTGIS_OPERATORS = {
23 92
     # The "&<" operator returns true if A's bounding box overlaps or
24 93
     #  is to the left of B's bounding box.
25  
-    'overlaps_left' : '&<',
  94
+    'overlaps_left' : PostGISOperator('&<'),
26 95
     # The "&>" operator returns true if A's bounding box overlaps or
27 96
     #  is to the right of B's bounding box.
28  
-    'overlaps_right' : '&>',
  97
+    'overlaps_right' : PostGISOperator('&>'),
29 98
     # The "<<" operator returns true if A's bounding box is strictly
30 99
     #  to the left of B's bounding box.
31  
-    'left' : '<<',
  100
+    'left' : PostGISOperator('<<'),
32 101
     # The ">>" operator returns true if A's bounding box is strictly
33 102
     #  to the right of B's bounding box.
34  
-    'right' : '>>',
  103
+    'right' : PostGISOperator('>>'),
35 104
     # The "&<|" operator returns true if A's bounding box overlaps or
36 105
     #  is below B's bounding box.
37  
-    'overlaps_below' : '&<|',
  106
+    'overlaps_below' : PostGISOperator('&<|'),
38 107
     # The "|&>" operator returns true if A's bounding box overlaps or
39 108
     #  is above B's bounding box.
40  
-    'overlaps_above' : '|&>',
  109
+    'overlaps_above' : PostGISOperator('|&>'),
41 110
     # The "<<|" operator returns true if A's bounding box is strictly
42 111
     #  below B's bounding box.
43  
-    'strictly_below' : '<<|',
  112
+    'strictly_below' : PostGISOperator('<<|'),
44 113
     # The "|>>" operator returns true if A's bounding box is strictly
45 114
     # above B's bounding box.
46  
-    'strictly_above' : '|>>',
  115
+    'strictly_above' : PostGISOperator('|>>'),
47 116
     # The "~=" operator is the "same as" operator. It tests actual
48 117
     #  geometric equality of two features. So if A and B are the same feature,
49 118
     #  vertex-by-vertex, the operator returns true.
50  
-    'same_as' : '~=',
51  
-    'exact' : '~=',
  119
+    'same_as' : PostGISOperator('~='),
  120
+    'exact' : PostGISOperator('~='),
52 121
     # The "@" operator returns true if A's bounding box is completely contained
53 122
     #  by B's bounding box.
54  
-    'contained' : '@',
  123
+    'contained' : PostGISOperator('@'),
55 124
     # The "~" operator returns true if A's bounding box completely contains
56 125
     #  by B's bounding box.
57  
-    'bbcontains' : '~',
  126
+    'bbcontains' : PostGISOperator('~'),
58 127
     # The "&&" operator returns true if A's bounding box overlaps
59 128
     #  B's bounding box.
60  
-    'bboverlaps' : '&&',
  129
+    'bboverlaps' : PostGISOperator('&&'),
61 130
     }
62 131
 
63  
-# Versions of PostGIS >= 1.2.2 changed their naming convention to be
64  
-#  'SQL-MM-centric' to conform with the ISO standard. Practically, this 
65  
-#  means that 'ST_' is prefixes geometry function names.
66  
-GEOM_FUNC_PREFIX = ''
67  
-if MAJOR_VERSION >= 1:
68  
-    if (MINOR_VERSION1 > 2 or 
69  
-        (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
70  
-        GEOM_FUNC_PREFIX = 'ST_'
71  
-    
72  
-    def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
73  
-
74  
-    # Functions used by the GeoManager & GeoQuerySet
75  
-    ASKML = get_func('AsKML')
76  
-    ASGML = get_func('AsGML')
77  
-    DISTANCE = get_func('Distance')
78  
-    GEOM_FROM_TEXT = get_func('GeomFromText')
79  
-    GEOM_FROM_WKB = get_func('GeomFromWKB')
80  
-    TRANSFORM = get_func('Transform')
81  
-
82  
-    # Special cases for union and KML methods.
83  
-    if MINOR_VERSION1 < 3:
84  
-        UNION = 'GeomUnion'
85  
-    else:
86  
-        UNION = 'ST_Union'
87  
-
88  
-    if MINOR_VERSION1 == 1:
89  
-        ASKML = False
90  
-else:
91  
-    raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
92  
-
93 132
 # For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
94 133
 # first before calling the more computationally expensive GEOS routines (called
95 134
 # "inline index magic"):
96 135
 # 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
97 136
 # 'covers'.
98 137
 POSTGIS_GEOMETRY_FUNCTIONS = {
99  
-    'equals' : 'Equals',
100  
-    'disjoint' : 'Disjoint',
101  
-    'touches' : 'Touches',
102  
-    'crosses' : 'Crosses',
103  
-    'within' : 'Within',
104  
-    'overlaps' : 'Overlaps',
105  
-    'contains' : 'Contains',
106  
-    'intersects' : 'Intersects',
107  
-    'relate' : ('Relate', basestring),
  138
+    'equals' : PostGISFunction('Equals'),
  139
+    'disjoint' : PostGISFunction('Disjoint'),
  140
+    'touches' : PostGISFunction('Touches'),
  141
+    'crosses' : PostGISFunction('Crosses'),
  142
+    'within' : PostGISFunction('Within'),
  143
+    'overlaps' : PostGISFunction('Overlaps'),
  144
+    'contains' : PostGISFunction('Contains'),
  145
+    'intersects' : PostGISFunction('Intersects'),
  146
+    'relate' : (PostGISRelate, basestring),
108 147
     }
109 148
 
110 149
 # Valid distance types and substitutions
111 150
 dtypes = (Decimal, Distance, float, int)
112 151
 DISTANCE_FUNCTIONS = {
113  
-    'distance_gt' : ('>', dtypes),
114  
-    'distance_gte' : ('>=', dtypes),
115  
-    'distance_lt' : ('<', dtypes),
116  
-    'distance_lte' : ('<=', dtypes),
  152
+    'distance_gt' : (PostGISDistance('>'), dtypes),
  153
+    'distance_gte' : (PostGISDistance('>='), dtypes),
  154
+    'distance_lt' : (PostGISDistance('<'), dtypes),
  155
+    'distance_lte' : (PostGISDistance('<='), dtypes),
117 156
     }
118 157
 
119 158
 if GEOM_FUNC_PREFIX == 'ST_':
120  
-    # Adding the GEOM_FUNC_PREFIX to the lookup functions.
121  
-    for lookup, f in POSTGIS_GEOMETRY_FUNCTIONS.items():
122  
-        if isinstance(f, tuple):
123  
-            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (get_func(f[0]), f[1])
124  
-        else:
125  
-            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = get_func(f)
126  
-
127 159
     # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
128 160
     POSTGIS_GEOMETRY_FUNCTIONS.update(
129  
-        {'dwithin' : ('ST_DWithin', dtypes),
130  
-         'coveredby' : 'ST_CoveredBy',
131  
-         'covers' : 'ST_Covers',
132  
-         }
133  
-        )
  161
+        {'dwithin' : (PostGISFunctionParam('DWithin'), dtypes),
  162
+         'coveredby' : PostGISFunction('CoveredBy'),
  163
+         'covers' : PostGISFunction('Covers'),
  164
+         })
134 165
 
  166
+# Distance functions are a part of PostGIS geometry functions.
135 167
 POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
136 168
 
137 169
 # Any other lookup types that do not require a mapping.
@@ -144,33 +176,25 @@ def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
144 176
 POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
145 177
 POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
146 178
 
147  
-### PostGIS-specific Methods ###
148  
-def get_geom_func(lookup_type):
149  
-    func_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
150  
-    if isinstance(func_info, tuple):
151  
-        return func_info[0]
152  
-    else:
153  
-        return func_info
154  
-
  179
+#### The `get_geo_where_clause` function for PostGIS. ####
155 180
 def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
156 181
     "Returns the SQL WHERE clause for use in PostGIS SQL construction."
157  
-    if table_prefix.endswith('.'):
158  
-        table_prefix = qn(table_prefix[:-1])+'.'
159  
-    field_name = qn(field_name)
160  
-
161  
-    # See if a PostGIS operator matches the lookup type first
  182
+    # Getting the quoted field as `geo_col`.
  183
+    geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
162 184
     if lookup_type in POSTGIS_OPERATORS:
163  
-        return '%s%s %s %%s' % (table_prefix, field_name, POSTGIS_OPERATORS[lookup_type])
  185
+        # See if a PostGIS operator matches the lookup type.
  186
+        return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
  187
+    elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS:
  188
+        # See if a PostGIS geometry function matches the lookup type.
  189
+        tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
164 190
 
165  
-    # See if a PostGIS Geometry function matches the lookup type next
166  
-    if lookup_type in POSTGIS_GEOMETRY_FUNCTIONS:
167  
-        lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
168 191
         # Lookup types that are tuples take tuple arguments, e.g., 'relate' and 
169  
-        #  'dwithin' lookup types.
170  
-        if isinstance(lookup_info, tuple):
171  
-            # First element of tuple is lookup type, second element is the type
172  
-            #  of the expected argument (e.g., str, float)
173  
-            func, arg_type = lookup_info
  192
+        # distance lookups.
  193
+        if isinstance(tmp, tuple):
  194
+            # First element of tuple is the PostGISOperation instance, and the
  195
+            # second element is either the type or a tuple of acceptable types
  196
+            # that may passed in as further parameters for the lookup type.
  197
+            op, arg_type = tmp
174 198
 
175 199
             # Ensuring that a tuple _value_ was passed in from the user
176 200
             if not isinstance(value, tuple): 
@@ -182,24 +206,15 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
182 206
             if not isinstance(value[1], arg_type):
183 207
                 raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
184 208
 
185  
-            if lookup_type in DISTANCE_FUNCTIONS:
186  
-                op = DISTANCE_FUNCTIONS[lookup_type][0]
187  
-                return '%s(%s%s, %%s) %s %%s' % (DISTANCE, table_prefix, field_name, op)
188  
-            else:
189  
-                return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name)
  209
+            # For lookup type `relate`, the op instance is not yet created (has
  210
+            # to be instantiated here to check the pattern parameter).
  211
+            if lookup_type == 'relate': op = op(value[1])
190 212
         else:
191  
-            # Returning the SQL necessary for the geometry function call. For example: 
192  
-            #  ST_Contains("geoapp_country"."poly", ST_GeomFromWKB(..))
193  
-            return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name)
194  
-
195  
-    # Handling 'isnull' lookup type
196  
-    if lookup_type == 'isnull':
197  
-        return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
  213
+            op = tmp
  214
+        # Calling the `as_sql` function on the operation instance.
  215
+        return op.as_sql(geo_col)
  216
+    elif lookup_type == 'isnull':
  217
+        # Handling 'isnull' lookup type
  218
+        return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
198 219
 
199 220
     raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
200  
-
201  
-# Custom selection not needed for PostGIS since GEOS geometries may be
202  
-# instantiated directly from the HEXEWKB returned by default.  If
203  
-# WKT is needed for some reason in the future, this value may be changed,
204  
-# 'AsText(%s)'
205  
-GEOM_SELECT = None
54  django/contrib/gis/db/backend/util.py
@@ -8,14 +8,52 @@ def __init__(self, where=[], params=[]):
8 8
         self.params = params
9 9
 
10 10
     def __str__(self):
11  
-        return self.where[0] % tuple(self.params)
  11
+        return self.as_sql()
12 12
 
13  
-def get_srid(field, geom):
  13
+    def as_sql(self, quote=False):
  14
+        if not quote:
  15
+            return self.where[0] % tuple(self.params)
  16
+        else:
  17
+            # Used for quoting WKT on certain backends.
  18
+            tmp_params = ["'%s'" % self.params[0]]
  19
+            tmp_params.extend(self.params[1:])
  20
+            return self.where[0] % tuple(tmp_params)
  21
+
  22
+class SpatialOperation(object):
  23
+    """
  24
+    Base class for generating spatial SQL.
  25
+    """
  26
+    def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''):
  27
+        self.function = function
  28
+        self.operator = operator
  29
+        self.result = result
  30
+        self.beg_subst = beg_subst
  31
+        try:
  32
+            # Try and put the operator and result into to the
  33
+            # end substitution.
  34
+            self.end_subst = end_subst % (operator, result)
  35
+        except TypeError:
  36
+            self.end_subst = end_subst
  37
+
  38
+    @property
  39
+    def sql_subst(self):
  40
+        return ''.join([self.beg_subst, self.end_subst])
  41
+
  42
+    def as_sql(self, geo_col):
  43
+        return self.sql_subst % self.params(geo_col)
  44
+
  45
+    def params(self, geo_col):
  46
+        return (geo_col, self.operator)
  47
+
  48
+class SpatialFunction(SpatialOperation):
14 49
     """
15  
-    Gets the SRID depending on the value of the SRID setting of the field
16  
-    and that of the given geometry.
  50
+    Base class for generating spatial SQL related to a function.
17 51
     """
18  
-    if geom.srid is None or (geom.srid == -1 and field._srid != -1):
19  
-        return field._srid
20  
-    else:
21  
-        return geom.srid
  52
+    def __init__(self, func, beg_subst='%s(%s, %%s', end_subst=')', result='', operator=''):
  53
+        # Getting the function prefix.
  54
+        kwargs = {'function' : func, 'operator' : operator, 'result' : result,
  55
+                  'beg_subst' : beg_subst, 'end_subst' : end_subst,}
  56
+        super(SpatialFunction, self).__init__(**kwargs)
  57
+
  58
+    def params(self, geo_col):
  59
+        return (self.function, geo_col)
15  django/contrib/gis/db/models/query.py
@@ -7,11 +7,12 @@
7 7
 from django.contrib.gis.db.models.fields import GeometryField
8 8
 # parse_lookup depends on the spatial database backend.
9 9
 from django.contrib.gis.db.backend import parse_lookup, \
10  
-    ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
  10
+    ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION, VERSION
11 11
 from django.contrib.gis.geos import GEOSGeometry
12 12
 
13  
-# Flag indicating whether the backend is Oracle.
  13
+# Shortcut booleans for determining the backend.
14 14
 oracle = SPATIAL_BACKEND == 'oracle'
  15
+postgis = SPATIAL_BACKEND == 'postgis'
15 16
 
16 17
 class GeoQ(Q):
17 18
     "Geographical query encapsulation object."
@@ -325,8 +326,14 @@ def gml(self, field_name=None, precision=8, version=2):
325 326
         
326 327
         if oracle:
327 328
             gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
328  
-        else:
329  
-            gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
  329
+        elif postgis:
  330
+            # PostGIS AsGML() aggregate function parameter order depends on the
  331
+            # version -- uggh.
  332
+            major, minor1, minor2 = VERSION
  333
+            if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
  334
+                gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, field_col, precision)}
  335
+            else:
  336
+                gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
330 337
 
331 338
         # Adding GML function call to SELECT part of the SQL.
332 339
         return self.extra(select=gml_select)

0 notes on commit 5799c2e

Please sign in to comment.
Something went wrong with that request. Please try again.