Skip to content

Commit 21a0196

Browse files
committed
gis: Fixed 6414, and applied DRY to spatial backend internals. Changes include:
(1) Support for distance calculations on geometry fields with geodetic coordinate systems (e.g., WGS84, the default). (2) The `get_db_prep_save` and `get_db_prep_lookup` have been moved from the spatial backends to common implementations in `GeometryField`. (3) Simplified SQL construction for `GeoQuerySet` methods. (4) `SpatialBackend` now contains all spatial backend dependent settings. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7104 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent d06e33a commit 21a0196

File tree

23 files changed

+461
-428
lines changed

23 files changed

+461
-428
lines changed

django/contrib/gis/db/backend/__init__.py

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,66 +9,101 @@
99
the backend.
1010
(3) The `parse_lookup` function, used for spatial SQL construction by
1111
the GeoQuerySet.
12-
(4) The `create_spatial_db`, and `get_geo_where_clause`
13-
routines (needed by `parse_lookup`).
12+
(4) The `create_spatial_db`, and `get_geo_where_clause`
13+
(needed by `parse_lookup`) functions.
1414
(5) The `SpatialBackend` object, which contains information specific
1515
to the spatial backend.
1616
"""
17-
from types import StringType, UnicodeType
1817
from django.conf import settings
1918
from django.db import connection
2019
from django.db.models.query import field_choices, find_field, get_where_clause, \
2120
FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS
2221
from django.utils.datastructures import SortedDict
23-
from django.contrib.gis.geos import GEOSGeometry
22+
from django.contrib.gis.db.backend.util import gqn
2423

2524
# These routines (needed by GeoManager), default to False.
26-
ASGML, ASKML, DISTANCE, EXTENT, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False, False)
25+
ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, EXTENT, TRANSFORM, UNION, VERSION = tuple(False for i in range(8))
2726

27+
# Lookup types in which the rest of the parameters are not
28+
# needed to be substitute in the WHERE SQL (e.g., the 'relate'
29+
# operation on Oracle does not need the mask substituted back
30+
# into the query SQL.).
31+
LIMITED_WHERE = []
32+
33+
# Retrieving the necessary settings from the backend.
2834
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
29-
# PostGIS is the spatial database, getting the rquired modules,
30-
# renaming as necessary.
31-
from django.contrib.gis.db.backend.postgis import \
32-
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
33-
create_spatial_db, get_geo_where_clause, \
34-
ASGML, ASKML, DISTANCE, EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
35+
from django.contrib.gis.db.backend.postgis.adaptor import \
36+
PostGISAdaptor as GeoAdaptor
37+
from django.contrib.gis.db.backend.postgis.field import \
38+
PostGISField as GeoBackendField
39+
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
40+
from django.contrib.gis.db.backend.postgis.query import \
41+
get_geo_where_clause, POSTGIS_TERMS as GIS_TERMS, \
42+
ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, DISTANCE_FUNCTIONS, \
43+
EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
3544
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
45+
# PostGIS version info is needed to determine calling order of some
46+
# stored procedures (e.g., AsGML()).
3647
VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
3748
SPATIAL_BACKEND = 'postgis'
3849
elif settings.DATABASE_ENGINE == 'oracle':
39-
from django.contrib.gis.db.backend.oracle import \
40-
OracleSpatialField as GeoBackendField, \
41-
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
42-
create_spatial_db, get_geo_where_clause, \
43-
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
50+
from django.contrib.gis.db.backend.oracle.adaptor import \
51+
OracleSpatialAdaptor as GeoAdaptor
52+
from django.contrib.gis.db.backend.oracle.field import \
53+
OracleSpatialField as GeoBackendField
54+
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
55+
from django.contrib.gis.db.backend.oracle.query import \
56+
get_geo_where_clause, ORACLE_SPATIAL_TERMS as GIS_TERMS, \
57+
ASGML, DISTANCE, DISTANCE_FUNCTIONS, GEOM_SELECT, TRANSFORM, UNION
4458
SPATIAL_BACKEND = 'oracle'
59+
LIMITED_WHERE = ['relate']
4560
elif settings.DATABASE_ENGINE == 'mysql':
46-
from django.contrib.gis.db.backend.mysql import \
47-
MySQLGeoField as GeoBackendField, \
48-
MYSQL_GIS_TERMS as GIS_TERMS, \
49-
create_spatial_db, get_geo_where_clause, \
50-
GEOM_SELECT
61+
from django.contrib.gis.db.backend.mysql.adaptor import \
62+
MySQLAdaptor as GeoAdaptor
63+
from django.contrib.gis.db.backend.mysql.field import \
64+
MySQLGeoField as GeoBackendField
65+
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
66+
from django.contrib.gis.db.backend.mysql.query import \
67+
get_geo_where_clause, MYSQL_GIS_TERMS as GIS_TERMS, GEOM_SELECT
68+
DISTANCE_FUNCTIONS = {}
5169
SPATIAL_BACKEND = 'mysql'
5270
else:
5371
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
5472

5573
class SpatialBackend(object):
56-
"A container for properties of the Spatial Backend."
74+
"A container for properties of the SpatialBackend."
75+
# Stored procedure names used by the `GeoManager`.
5776
as_kml = ASKML
5877
as_gml = ASGML
5978
distance = DISTANCE
79+
distance_spheroid = DISTANCE_SPHEROID
6080
extent = EXTENT
6181
name = SPATIAL_BACKEND
6282
select = GEOM_SELECT
6383
transform = TRANSFORM
6484
union = UNION
85+
86+
# Version information, if defined.
6587
version = VERSION
88+
89+
# All valid GIS lookup terms, and distance functions.
90+
gis_terms = GIS_TERMS
91+
distance_functions = DISTANCE_FUNCTIONS
92+
93+
# Lookup types where additional WHERE parameters are excluded.
94+
limited_where = LIMITED_WHERE
95+
96+
# Class for the backend field.
97+
Field = GeoBackendField
98+
99+
# Adaptor class used for quoting GEOS geometries in the database.
100+
Adaptor = GeoAdaptor
66101

67102
#### query.py overloaded functions ####
68103
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
69104
# counterparts to support constructing SQL for geographic queries.
70105
#
71-
# Status: Synced with r5982.
106+
# Status: Synced with r7098.
72107
#
73108
def parse_lookup(kwarg_items, opts):
74109
# Helper function that handles converting API kwargs
@@ -290,16 +325,17 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
290325
# If the field is a geometry field, then the WHERE clause will need to be obtained
291326
# with the get_geo_where_clause()
292327
if hasattr(field, '_geom'):
293-
# Getting the preparation SQL object from the field.
294-
geo_prep = field.get_db_prep_lookup(lookup_type, value)
328+
# Getting additional SQL WHERE and params arrays associated with
329+
# the geographic field.
330+
geo_where, geo_params = field.get_db_prep_lookup(lookup_type, value)
295331

296-
# Getting the adapted geometry from the field.
297-
gwc = get_geo_where_clause(lookup_type, current_table, column, value)
332+
# Getting the geographic WHERE clause.
333+
gwc = get_geo_where_clause(lookup_type, current_table, field, value)
298334

299-
# Substituting in the the where parameters into the geographic where
300-
# clause, and extending the parameters.
301-
where.append(gwc % tuple(geo_prep.where))
302-
params.extend(geo_prep.params)
335+
# Appending the geographic WHERE componnents and parameters onto
336+
# the where and params arrays.
337+
where.append(gwc % tuple(geo_where))
338+
params.extend(geo_params)
303339
else:
304340
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
305341
params.extend(field.get_db_prep_lookup(lookup_type, value))
Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1 @@
1-
"""
2-
The MySQL spatial database backend module.
31

4-
Please note that MySQL only supports bounding box queries, also
5-
known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
6-
indices may only be used on MyISAM tables -- if you need
7-
transactions, take a look at PostGIS.
8-
"""
9-
10-
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
11-
from django.contrib.gis.db.backend.mysql.field import MySQLGeoField, gqn
12-
from django.contrib.gis.db.backend.mysql.query import get_geo_where_clause, MYSQL_GIS_TERMS, GEOM_SELECT
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
This object provides quoting for GEOS geometries into MySQL.
3+
"""
4+
class MySQLAdaptor(object):
5+
def __init__(self, geom):
6+
self.wkt = geom.wkt
7+
8+
def __str__(self):
9+
"WKT is used as for the substitution value for the geometry."
10+
return self.wkt
Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
import re
2-
from types import StringType, UnicodeType
31
from django.db import connection
42
from django.db.models.fields import Field # Django base Field class
5-
from django.contrib.gis.geos import GEOSGeometry
6-
from django.contrib.gis.db.backend.util import GeoFieldSQL
7-
from django.contrib.gis.db.backend.mysql.query import MYSQL_GIS_TERMS, GEOM_FROM_TEXT
3+
from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT
84

95
# Quotename & geographic quotename, respectively.
106
qn = connection.ops.quote_name
11-
def gqn(value):
12-
if isinstance(value, UnicodeType): value = value.encode('ascii')
13-
return "'%s'" % value
147

158
class MySQLGeoField(Field):
169
"""
@@ -23,7 +16,7 @@ def _geom_index(self, style, db_table):
2316
used an R-Tree index is created, otherwise a B-Tree index is created.
2417
Thus, for best spatial performance, you should use MyISAM tables
2518
(which do not support transactions). For more information, see Ch.
26-
17.6.1 of the MySQL 5.0 documentation.
19+
16.6.1 of the MySQL 5.0 documentation.
2720
"""
2821

2922
# Getting the index name.
@@ -50,43 +43,11 @@ def _post_create_sql(self, style, db_table):
5043
def db_type(self):
5144
"The OpenGIS name is returned for the MySQL database column type."
5245
return self._geom
53-
54-
def get_db_prep_lookup(self, lookup_type, value):
55-
"""
56-
Returns field's value prepared for database lookup, accepts WKT and
57-
GEOS Geometries for the value.
58-
"""
59-
if lookup_type in MYSQL_GIS_TERMS:
60-
# special case for isnull lookup
61-
if lookup_type == 'isnull': return GeoFieldSQL([], [])
62-
63-
# When the input is not a GEOS geometry, attempt to construct one
64-
# from the given string input.
65-
if isinstance(value, GEOSGeometry):
66-
pass
67-
elif isinstance(value, (StringType, UnicodeType)):
68-
try:
69-
value = GEOSGeometry(value)
70-
except GEOSException:
71-
raise TypeError("Could not create geometry from lookup value: %s" % str(value))
72-
else:
73-
raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
74-
75-
return GeoFieldSQL(['%s(%%s)' % GEOM_FROM_TEXT], [value])
76-
77-
else:
78-
raise TypeError("Field has invalid lookup: %s" % lookup_type)
79-
80-
def get_db_prep_save(self, value):
81-
"Prepares the value for saving in the database."
82-
if not bool(value): return None
83-
if isinstance(value, GEOSGeometry):
84-
return value
85-
else:
86-
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
8746

8847
def get_placeholder(self, value):
8948
"""
90-
Nothing special happens here because MySQL does not support transformations.
49+
The placeholder here has to include MySQL's WKT constructor. Because
50+
MySQL does not support spatial transformations, there is no need to
51+
modify the placeholder based on the contents of the given value.
9152
"""
9253
return '%s(%%s)' % GEOM_FROM_TEXT

django/contrib/gis/db/backend/mysql/query.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
"""
2-
This module contains the spatial lookup types, and the get_geo_where_clause()
3-
routine for MySQL
2+
This module contains the spatial lookup types, and the `get_geo_where_clause`
3+
routine for MySQL.
4+
5+
Please note that MySQL only supports bounding box queries, also
6+
known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
7+
indices may only be used on MyISAM tables -- if you need
8+
transactions, take a look at PostGIS.
49
"""
510
from django.db import connection
611
qn = connection.ops.quote_name
@@ -34,10 +39,10 @@
3439
MYSQL_GIS_TERMS += MISC_TERMS
3540
MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable
3641

37-
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
42+
def get_geo_where_clause(lookup_type, table_prefix, field, value):
3843
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
3944
# Getting the quoted field as `geo_col`.
40-
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
45+
geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
4146

4247
# See if a MySQL Geometry function matches the lookup type next
4348
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +0,0 @@
1-
"""
2-
The Oracle spatial database backend module.
3-
4-
Please note that WKT support is broken on the XE version, and thus
5-
this backend will not work on such platforms. Specifically, XE lacks
6-
support for an internal JVM, and Java libraries are required to use
7-
the WKT constructors.
8-
"""
9-
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
10-
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField, gqn
11-
from django.contrib.gis.db.backend.oracle.query import \
12-
get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
13-
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
14-
Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""
22
This object provides the database adaptor for Oracle geometries.
33
"""
4-
from cx_Oracle import CLOB
5-
64
class OracleSpatialAdaptor(object):
75
def __init__(self, geom):
86
"Initializes only on the geometry object."
@@ -11,11 +9,3 @@ def __init__(self, geom):
119
def __str__(self):
1210
"WKT is used for the substitution value of the geometry."
1311
return self.wkt
14-
15-
def oracle_type(self):
16-
"""
17-
The parameter type is a CLOB because no string (VARCHAR2) greater
18-
than 4000 characters will be accepted through the Oracle database
19-
API and/or SQL*Plus.
20-
"""
21-
return CLOB

0 commit comments

Comments
 (0)