Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

[soc2009/multidb] Fixed #11741 -- Updates to the spatial backends (e.…

…g., re-enabled POSTGIS_VERSION setting); added geometry backend module. Patch from Justin Bronn.

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11872 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 05b4d2f67bc3e75d2d1aada844d6031cc126e48e 1 parent 11c00d6
Alex Gaynor alex authored
47 django/contrib/gis/db/backends/base.py
View
@@ -1,5 +1,6 @@
"""
-
+Base/mixin classes for the spatial backend database operations and the
+`SpatialRefSys` model the backend.
"""
import re
from django.conf import settings
@@ -14,8 +15,9 @@ class BaseSpatialOperations(object):
distance_functions = {}
geometry_functions = {}
geometry_operators = {}
+ geography_operators = {}
+ geography_functions = {}
gis_terms = {}
- limited_where = {}
# Quick booleans for the type of this spatial backend, and
# an attribute for the spatial database version tuple (if applicable)
@@ -28,6 +30,9 @@ class BaseSpatialOperations(object):
# How the geometry column should be selected.
select = None
+ # Does the spatial database have a geography type?
+ geography = False
+
area = False
centroid = False
difference = False
@@ -37,11 +42,13 @@ class BaseSpatialOperations(object):
envelope = False
force_rhr = False
mem_size = False
+ bounding_circle = False
num_geom = False
num_points = False
perimeter = False
perimeter3d = False
point_on_surface = False
+ polygonize = False
scale = False
snap_to_grid = False
sym_difference = False
@@ -67,11 +74,6 @@ class BaseSpatialOperations(object):
from_text = False
from_wkb = False
- def geo_quote_name(self, name):
- if isinstance(name, unicode):
- name = name.encode('ascii')
- return "'%s'" % name
-
# Default conversion functions for aggregates; will be overridden if implemented
# for the spatial backend.
def convert_extent(self, box):
@@ -83,6 +85,37 @@ def convert_extent3d(self, box):
def convert_geom(self, geom_val, geom_field):
raise NotImplementedError('Aggregate method not implemented for this spatial backend.')
+ # For quoting column values, rather than columns.
+ def geo_quote_name(self, name):
+ if isinstance(name, unicode):
+ name = name.encode('ascii')
+ return "'%s'" % name
+
+ # GeometryField operations
+ def geo_db_type(self, f):
+ """
+ Returns the database column type for the geometry field on
+ the spatial backend.
+ """
+ raise NotImplementedError
+
+ def get_distance(self, f, value, lookup_type):
+ """
+ Returns the distance parameters for the given geometry field,
+ lookup value, and lookup type.
+ """
+ raise NotImplementedError('Distance operations not available on this spatial backend.')
+
+ def get_geom_placeholder(self, f, value):
+ """
+ Returns the placeholder for the given geometry field with the given
+ value. Depending on the spatial backend, the placeholder may contain a
+ stored procedure call to the transformation function of the spatial
+ backend.
+ """
+ raise NotImplementedError
+
+ # Spatial SQL Construction
def spatial_aggregate_sql(self, agg):
raise NotImplementedError('Aggregate support not implemented for this spatial backend.')
6 django/contrib/gis/db/backends/mysql/operations.py
View
@@ -31,6 +31,9 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
gis_terms = dict([(term, None) for term in geometry_functions.keys() + ['isnull']])
+ def geo_db_type(self, f):
+ return f.geom_type
+
def get_geom_placeholder(self, value, srid):
"""
The placeholder here has to include MySQL's WKT constructor. Because
@@ -43,8 +46,7 @@ def get_geom_placeholder(self, value, srid):
placeholder = '%s(%%s)' % self.from_text
return placeholder
- def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
- qn = self.quote_name
+ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
alias, col, db_type = lvalue
geo_col = '%s.%s' % (qn(alias), qn(col))
2  django/contrib/gis/db/backends/oracle/base.py
View
@@ -7,4 +7,4 @@ class DatabaseWrapper(OracleDatabaseWrapper):
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)
self.creation = OracleCreation(self)
- self.ops = OracleOperations()
+ self.ops = OracleOperations(self)
24 django/contrib/gis/db/backends/oracle/compiler.py
View
@@ -7,7 +7,29 @@ class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler):
pass
class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler):
- pass
+ def placeholder(self, field, val):
+ if field is None:
+ # A field value of None means the value is raw.
+ return val
+ elif hasattr(field, 'get_placeholder'):
+ # Some fields (e.g. geo fields) need special munging before
+ # they can be inserted.
+ ph = field.get_placeholder(val, self.connection)
+ if ph == 'NULL':
+ # If the placeholder returned is 'NULL', then we need to
+ # to remove None from the Query parameters. Specifically,
+ # cx_Oracle will assume a CHAR type when a placeholder ('%s')
+ # is used for columns of MDSYS.SDO_GEOMETRY. Thus, we use
+ # 'NULL' for the value, and remove None from the query params.
+ # See also #10888.
+ param_idx = self.query.columns.index(field.column)
+ params = list(self.query.params)
+ params.pop(param_idx)
+ self.query.params = tuple(params)
+ return ph
+ else:
+ # Return the common case for the placeholder
+ return '%s'
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler):
pass
66 django/contrib/gis/db/backends/oracle/operations.py
View
@@ -14,7 +14,7 @@
from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter
from django.contrib.gis.db.backends.util import SpatialFunction
-from django.contrib.gis.geometry import Geometry
+from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance
class SDOOperation(SpatialFunction):
@@ -91,7 +91,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations):
sym_difference = 'SDO_GEOM.SDO_XOR'
transform = 'SDO_CS.TRANSFORM'
union = 'SDO_GEOM.SDO_UNION'
- unionagg = 'SDO_AGGR_UNION'
+ unionagg = 'SDO_AGGR_UNION'
# We want to get SDO Geometries as WKT because it is much easier to
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
@@ -128,6 +128,10 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations):
gis_terms += geometry_functions.keys()
gis_terms = dict([(term, None) for term in gis_terms])
+ def __init__(self, connection):
+ super(OracleOperations, self).__init__()
+ self.connection = connection
+
def convert_extent(self, clob):
if clob:
# Generally, Oracle returns a polygon for the extent -- however,
@@ -156,7 +160,40 @@ def convert_geom(self, clob, geo_field):
else:
return None
- def get_geom_placeholder(self, value, srid):
+ def geo_db_type(self, f):
+ """
+ Returns the geometry database type for Oracle. Unlike other spatial
+ backends, no stored procedure is necessary and it's the same for all
+ geometry types.
+ """
+ return 'MDSYS.SDO_GEOMETRY'
+
+ def get_distance(self, f, value, lookup_type):
+ """
+ Returns the distance parameters given the value and the lookup type.
+ On Oracle, geometry columns with a geodetic coordinate system behave
+ implicitly like a geography column, and thus meters will be used as
+ the distance parameter on them.
+ """
+ if not value:
+ return []
+ value = value[0]
+ if isinstance(value, Distance):
+ if f.geodetic(self.connection):
+ dist_param = value.m
+ else:
+ dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
+ else:
+ dist_param = value
+
+ # dwithin lookups on oracle require a special string parameter
+ # that starts with "distance=".
+ if lookup_type == 'dwithin':
+ dist_param = 'distance=%s' % dist_param
+
+ return [dist_param]
+
+ def get_geom_placeholder(self, f, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
@@ -165,26 +202,25 @@ def get_geom_placeholder(self, value, srid):
if value is None:
return 'NULL'
- def transform_value(value, srid):
- return value.srid != srid
+ def transform_value(val, srid):
+ return val.srid != srid
if hasattr(value, 'expression'):
- if transform_value(value, srid):
- placeholder = '%s(%%s, %s)' % (self.transform, srid)
+ if transform_value(value, f.srid):
+ placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
else:
placeholder = '%s'
# No geometry value used for F expression, substitue in
# the column name instead.
return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
else:
- if transform_value(value, srid):
- return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, srid)
+ if transform_value(value, f.srid):
+ return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid)
else:
- return 'SDO_GEOMETRY(%%s, %s)' % srid
+ return 'SDO_GEOMETRY(%%s, %s)' % f.srid
- def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
+ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
- qn = self.quote_name
alias, col, db_type = lvalue
# Getting the quoted table name as `geo_col`.
@@ -214,15 +250,15 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
if lookup_type == 'relate':
# The SDORelate class handles construction for these queries,
# and verifies the mask argument.
- return sdo_op(value[1]).as_sql(geo_col, self.get_geom_placeholder(geom, field.srid))
+ return sdo_op(value[1]).as_sql(geo_col, self.get_geom_placeholder(field, geom))
else:
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
- return sdo_op.as_sql(geo_col, self.get_geom_placeholder(geom, field.srid))
+ return sdo_op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
else:
# Lookup info is a SDOOperation instance, whose `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(geo_col, self.get_geom_placeholder(value, field.srid))
+ return lookup_info.as_sql(geo_col, self.get_geom_placeholder(field, value))
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
45 django/contrib/gis/db/backends/postgis/creation.py
View
@@ -16,32 +16,43 @@ def sql_indexes_for_field(self, model, f, style):
qn = self.connection.ops.quote_name
db_table = model._meta.db_table
- output.append(style.SQL_KEYWORD('SELECT ') +
- style.SQL_TABLE('AddGeometryColumn') + '(' +
- style.SQL_TABLE(gqn(db_table)) + ', ' +
- style.SQL_FIELD(gqn(f.column)) + ', ' +
- style.SQL_FIELD(str(f.srid)) + ', ' +
- style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' +
- style.SQL_KEYWORD(str(f.dim)) + ');')
-
- if not f.null:
- # Add a NOT NULL constraint to the field
- output.append(style.SQL_KEYWORD('ALTER TABLE ') +
- style.SQL_TABLE(qn(db_table)) +
- style.SQL_KEYWORD(' ALTER ') +
- style.SQL_FIELD(qn(f.column)) +
- style.SQL_KEYWORD(' SET NOT NULL') + ';')
+ if f.geography:
+ # Geogrophy columns are created normally.
+ pass
+ else:
+ # Geometry columns are created by `AddGeometryColumn`
+ # stored procedure.
+ output.append(style.SQL_KEYWORD('SELECT ') +
+ style.SQL_TABLE('AddGeometryColumn') + '(' +
+ style.SQL_TABLE(gqn(db_table)) + ', ' +
+ style.SQL_FIELD(gqn(f.column)) + ', ' +
+ style.SQL_FIELD(str(f.srid)) + ', ' +
+ style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' +
+ style.SQL_KEYWORD(str(f.dim)) + ');')
+
+ if not f.null:
+ # Add a NOT NULL constraint to the field
+ output.append(style.SQL_KEYWORD('ALTER TABLE ') +
+ style.SQL_TABLE(qn(db_table)) +
+ style.SQL_KEYWORD(' ALTER ') +
+ style.SQL_FIELD(qn(f.column)) +
+ style.SQL_KEYWORD(' SET NOT NULL') + ';')
if f.spatial_index:
+ # Spatial indexes created the same way for both Geometry and
+ # Geography columns
+ if f.geography:
+ index_opts = ''
+ else:
+ index_opts = ' ' + style.SQL_KEYWORD(self.geom_index_opts)
output.append(style.SQL_KEYWORD('CREATE INDEX ') +
style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) +
style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) +
style.SQL_KEYWORD(' USING ') +
style.SQL_COLTYPE(self.geom_index_type) + ' ( ' +
- style.SQL_FIELD(qn(f.column)) + ' ' +
- style.SQL_KEYWORD(self.geom_index_opts) + ' );')
+ style.SQL_FIELD(qn(f.column)) + index_opts + ' );')
return output
def sql_table_creation_suffix(self):
208 django/contrib/gis/db/backends/postgis/operations.py
View
@@ -1,12 +1,15 @@
import re
from decimal import Decimal
-from django.db.backends.postgresql.operations import DatabaseOperations
+from django.conf import settings
from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
-from django.contrib.gis.geometry import Geometry
+from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance
+from django.core.exceptions import ImproperlyConfigured
+from django.db.backends.postgresql.operations import DatabaseOperations
+from django.db.backends.postgresql_psycopg2.base import Database
#### Classes used in constructing PostGIS spatial SQL ####
class PostGISOperator(SpatialOperation):
@@ -68,23 +71,48 @@ def __init__(self, connection):
super(PostGISOperations, self).__init__(connection)
# Trying to get the PostGIS version because the function
- # signatures will depend on the version used.
+ # signatures will depend on the version used. The cost
+ # here is a database query to determine the version, which
+ # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
+ # comprising user-supplied values for the major, minor, and
+ # subminor revision of PostGIS.
try:
- vtup = self.postgis_version_tuple()
- version = vtup[1:]
+ if hasattr(settings, 'POSTGIS_VERSION'):
+ vtup = settings.POSTGIS_VERSION
+ if len(vtup) == 3:
+ # The user-supplied PostGIS version.
+ version = vtup
+ else:
+ # This was the old documented way, but it's stupid to
+ # include the string.
+ version = vtup[1:4]
+ else:
+ vtup = self.postgis_version_tuple()
+ version = vtup[1:]
+
+ # Getting the prefix -- even though we don't officially support
+ # PostGIS 1.2 anymore, keeping it anyway in case a prefix change
+ # for something else is necessary.
if version >= (1, 2, 2):
prefix = 'ST_'
else:
prefix = ''
+
self.geom_func_prefix = prefix
self.spatial_version = version
+ except Database.ProgrammingError:
+ raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". '
+ 'GeoDjango requires at least PostGIS version 1.3. '
+ 'Was the database created from a spatial database '
+ 'template?' % self.connection.settings_dict['NAME']
+ )
except Exception, e:
- # TODO: Plain raising right now.
+ # TODO: Raise helpful exceptions as they become known.
raise
# PostGIS-specific operators. The commented descriptions of these
# operators come from Section 7.6 of the PostGIS 1.4 documentation.
- self.spatial_operators = {
+ self.geometry_operators = {
# The "&<" operator returns true if A's bounding box overlaps or
# is to the left of B's bounding box.
'overlaps_left' : PostGISOperator('&<'),
@@ -166,19 +194,6 @@ def get_dist_ops(operator):
# Adding the distance functions to the geometries lookup.
self.geometry_functions.update(self.distance_functions)
- # ST_ContainsProperly and GeoHash serialization added in 1.4.
- if version >= (1, 4, 0):
- GEOHASH = 'ST_GeoHash'
- self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
- else:
- GEOHASH = False
-
- # Creating a dictionary lookup of all GIS terms for PostGIS.
- gis_terms = ['isnull']
- gis_terms += self.spatial_operators.keys()
- gis_terms += self.geometry_functions.keys()
- self.gis_terms = dict([(term, None) for term in gis_terms])
-
# The union aggregate and topology operation use the same signature
# in versions 1.3+.
if version < (1, 3, 0):
@@ -194,7 +209,40 @@ def get_dist_ops(operator):
else:
GEOJSON = prefix + 'AsGeoJson'
+ # ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
+ if version >= (1, 4, 0):
+ GEOHASH = 'ST_GeoHash'
+ MAKELINE = 'ST_MakeLine'
+ BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
+ self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
+ else:
+ GEOHASH, MAKELINE, BOUNDINGCIRCLE = False, False, False
+
+ # Geography type support added in 1.5.
+ if version >= (1, 5, 0):
+ self.geography = True
+ # Only a subset of the operators and functions are available
+ # for the geography type.
+ self.geography_functions = self.distance_functions.copy()
+ self.geography_functions.update({
+ 'coveredby' : self.geometry_functions['coveredby'],
+ 'covers' : self.geometry_functions['covers'],
+ 'intersects' : self.geometry_functions['intersects'],
+ })
+ self.geography_operators = {
+ 'bboverlaps' : PostGISOperator('&&'),
+ 'exact' : PostGISOperator('~='),
+ 'same_as' : PostGISOperator('~='),
+ }
+
+ # Creating a dictionary lookup of all GIS terms for PostGIS.
+ gis_terms = ['isnull']
+ gis_terms += self.geometry_operators.keys()
+ gis_terms += self.geometry_functions.keys()
+ self.gis_terms = dict([(term, None) for term in gis_terms])
+
self.area = prefix + 'Area'
+ self.bounding_circle = BOUNDINGCIRCLE
self.centroid = prefix + 'Centroid'
self.collect = prefix + 'Collect'
self.difference = prefix + 'Difference'
@@ -212,13 +260,14 @@ def get_dist_ops(operator):
self.length = prefix + 'Length'
self.length3d = prefix + 'Length3D'
self.length_spheroid = prefix + 'length_spheroid'
- self.makeline = prefix + 'MakeLine'
+ self.makeline = MAKELINE
self.mem_size = prefix + 'mem_size'
self.num_geom = prefix + 'NumGeometries'
self.num_points =prefix + 'npoints'
self.perimeter = prefix + 'Perimeter'
self.perimeter3d = prefix + 'Perimeter3D'
self.point_on_surface = prefix + 'PointOnSurface'
+ self.polygonize = prefix + 'Polygonize'
self.scale = prefix + 'Scale'
self.snap_to_grid = prefix + 'SnapToGrid'
self.svg = prefix + 'AsSVG'
@@ -237,16 +286,22 @@ def check_aggregate_support(self, aggregate):
return agg_name in self.valid_aggregates
def convert_extent(self, box):
- # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
- # parsing out and returning as a 4-tuple.
+ """
+ Returns a 4-tuple extent for the `Extent` aggregate by converting
+ the bounding box text returned by PostGIS (`box` argument), for
+ example: "BOX(-90.0 30.0, -85.0 40.0)".
+ """
ll, ur = box[4:-1].split(',')
xmin, ymin = map(float, ll.split())
xmax, ymax = map(float, ur.split())
return (xmin, ymin, xmax, ymax)
def convert_extent3d(self, box3d):
- # Box text will be something like "BOX3D(-90.0 30.0 1, -85.0 40.0 2)";
- # parsing out and returning as a 4-tuple.
+ """
+ Returns a 6-tuple extent for the `Extent3D` aggregate by converting
+ the 3d bounding-box text returnded by PostGIS (`box3d` argument), for
+ example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
+ """
ll, ur = box3d[6:-1].split(',')
xmin, ymin, zmin = map(float, ll.split())
xmax, ymax, zmax = map(float, ur.split())
@@ -261,17 +316,78 @@ def convert_geom(self, hex, geo_field):
else:
return None
- def get_geom_placeholder(self, value, srid):
+ def geo_db_type(self, f):
+ """
+ Return the database field type for the given geometry field.
+ Typically this is `None` because geometry columns are added via
+ the `AddGeometryColumn` stored procedure, unless the field
+ has been specified to be of geography type instead.
+ """
+ if f.geography:
+ if not self.geography:
+ raise NotImplementedError('PostGIS 1.5 required for geography column support.')
+
+ if f.srid != 4326:
+ raise NotImplementedError('PostGIS 1.5 supports geography columns '
+ 'only with an SRID of 4326.')
+
+ return 'geography(%s,%d)'% (f.geom_type, f.srid)
+ else:
+ return None
+
+ def get_distance(self, f, dist_val, lookup_type):
+ """
+ Retrieve the distance parameters for the given geometry field,
+ distance lookup value, and the distance lookup type.
+
+ This is the most complex implementation of the spatial backends due to
+ what is supported on geodetic geometry columns vs. what's available on
+ projected geometry columns. In addition, it has to take into account
+ the newly introduced geography column type introudced in PostGIS 1.5.
+ """
+ # Getting the distance parameter and any options.
+ if len(dist_val) == 1:
+ value, option = dist_val[0], None
+ else:
+ value, option = dist_val
+
+ # Shorthand boolean flags.
+ geodetic = f.geodetic(self.connection)
+ geography = f.geography and self.geography
+
+ if isinstance(value, Distance):
+ if geography:
+ dist_param = value.m
+ elif geodetic:
+ if lookup_type == 'dwithin':
+ raise ValueError('Only numeric values of degree units are '
+ 'allowed on geographic DWithin queries.')
+ dist_param = value.m
+ else:
+ dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
+ else:
+ # Assuming the distance is in the units of the field.
+ dist_param = value
+
+ if (not geography and geodetic and lookup_type != 'dwithin'
+ and option == 'spheroid'):
+ # using distance_spheroid requires the spheroid of the field as
+ # a parameter.
+ return [f._spheroid, dist_param]
+ else:
+ return [dist_param]
+
+ def get_geom_placeholder(self, f, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
ST_Transform() function call.
"""
- if value is None or value.srid == srid:
+ if value is None or value.srid == f.srid:
placeholder = '%s'
else:
# Adding Transform() to the SQL placeholder.
- placeholder = '%s(%%s, %s)' % (self.transform, srid)
+ placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
if hasattr(value, 'expression'):
# If this is an F expression, then we don't really want
@@ -290,7 +406,7 @@ def _get_postgis_func(self, func):
cursor.execute('SELECT %s()' % func)
row = cursor.fetchone()
except:
- # TODO: raise helpful exception here.
+ # Responsibility of callers to perform error handling.
raise
finally:
cursor.close()
@@ -334,32 +450,42 @@ def postgis_version_tuple(self):
return (version, major, minor1, minor2)
- def num_params(self, lookup_type, val):
- def exactly_two(val): return val == 2
- def two_to_three(val): return val >= 2 and val <=3
+ def num_params(self, lookup_type, num_param):
+ """
+ Helper routine that returns a boolean indicating whether the number of
+ parameters is correct for the lookup type.
+ """
+ def exactly_two(np): return np == 2
+ def two_to_three(np): return np >= 2 and np <=3
if (lookup_type in self.distance_functions and
lookup_type != 'dwithin'):
- return two_to_three(val)
+ return two_to_three(num_param)
else:
- return exactly_two(val)
+ return exactly_two(num_param)
- def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
+ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
"""
Constructs spatial SQL from the given lookup value tuple a
(alias, col, db_type), the lookup type string, lookup value, and
the geometry field.
"""
- qn = self.quote_name
alias, col, db_type = lvalue
# Getting the quoted geometry column.
geo_col = '%s.%s' % (qn(alias), qn(col))
- if lookup_type in self.spatial_operators:
+ if lookup_type in self.geometry_operators:
+ if field.geography and not lookup_type in self.geography_operators:
+ raise ValueError('PostGIS geography does not support the '
+ '"%s" lookup.' % lookup_type)
# Handling a PostGIS operator.
- op = self.spatial_operators[lookup_type]
- return op.as_sql(geo_col, self.get_geom_placeholder(value, field.srid))
+ op = self.geometry_operators[lookup_type]
+ return op.as_sql(geo_col, self.get_geom_placeholder(field, value))
elif lookup_type in self.geometry_functions:
+ if field.geography and not lookup_type in self.geography_functions:
+ raise ValueError('PostGIS geography type does not support the '
+ '"%s" lookup.' % lookup_type)
+
# See if a PostGIS geometry function matches the lookup type.
tmp = self.geometry_functions[lookup_type]
@@ -392,7 +518,7 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
if lookup_type == 'relate':
op = op(self.geom_func_prefix, value[1])
elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
- if field.geodetic(self.connection):
+ if not field.geography and field.geodetic(self.connection):
# Geodetic distances are only availble from Points to PointFields.
if field.geom_type != 'POINT':
raise ValueError('PostGIS spherical operations are only valid on PointFields.')
@@ -412,7 +538,7 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
geom = value
# Calling the `as_sql` function on the operation instance.
- return op.as_sql(geo_col, self.get_geom_placeholder(geom, field.srid))
+ return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
60 django/contrib/gis/db/backends/spatialite/operations.py
View
@@ -4,7 +4,7 @@
from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
-from django.contrib.gis.geometry import Geometry
+from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.sqlite3.base import DatabaseOperations
@@ -119,11 +119,17 @@ def __init__(self, connection):
try:
vtup = self.spatialite_version_tuple()
version = vtup[1:]
- self.spatial_version = version
if version < (2, 3, 1):
- raise Exception('GeoDjango only supports SpatiaLite versions 2.3.1+')
- except Exception, e:
+ raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions '
+ '2.3.1 and above')
+ self.spatial_version = version
+ except ImproperlyConfigured:
raise
+ except Exception, msg:
+ raise ImproperlyConfigured('Cannot determine the SpatiaLite version for the "%s" '
+ 'database (error was "%s"). Was the SpatiaLite initialization '
+ 'SQL loaded on this database?' %
+ (self.connection.settings_dict['NAME'], msg))
# Creating the GIS terms dictionary.
gis_terms = ['isnull']
@@ -147,7 +153,36 @@ def convert_geom(self, wkt, geo_field):
else:
return None
- def get_geom_placeholder(self, value, srid):
+ def geo_db_type(self, f):
+ """
+ Returns None because geometry columnas are added via the
+ `AddGeometryColumn` stored procedure on SpatiaLite.
+ """
+ return None
+
+ def get_distance(self, f, value, lookup_type):
+ """
+ Returns the distance parameters for the given geometry field,
+ lookup value, and lookup type. SpatiaLite only supports regular
+ cartesian-based queries (no spheroid/sphere calculations for point
+ geometries like PostGIS).
+ """
+ if not value:
+ return []
+ value = value[0]
+ if isinstance(value, Distance):
+ if f.geodetic(self.connection):
+ raise ValueError('SpatiaLite does not support distance queries on '
+ 'geometry fields with a geodetic coordinate system. '
+ 'Distance objects; use a numeric value of your '
+ 'distance in degrees instead.')
+ else:
+ dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
+ else:
+ dist_param = value
+ return [dist_param]
+
+ def get_geom_placeholder(self, f, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
@@ -156,19 +191,19 @@ def get_geom_placeholder(self, value, srid):
def transform_value(value, srid):
return not (value is None or value.srid == srid)
if hasattr(value, 'expression'):
- if transform_value(value, srid):
- placeholder = '%s(%%s, %s)' % (self.transform, srid)
+ if transform_value(value, f.srid):
+ placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
else:
placeholder = '%s'
# No geometry value used for F expression, substitue in
# the column name instead.
return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
else:
- if transform_value(value, srid):
+ if transform_value(value, f.srid):
# Adding Transform() to the SQL placeholder.
- return '%s(%s(%%s,%s), %s)' % (self.transform, self.from_text, value.srid, srid)
+ return '%s(%s(%%s,%s), %s)' % (self.transform, self.from_text, value.srid, f.srid)
else:
- return '%s(%%s,%s)' % (self.from_text, srid)
+ return '%s(%%s,%s)' % (self.from_text, f.srid)
def _get_spatialite_func(self, func):
"""
@@ -229,13 +264,12 @@ def spatial_aggregate_sql(self, agg):
sql_function = getattr(self, agg_name)
return sql_template, sql_function
- def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
+ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
"""
Returns the SpatiaLite-specific SQL for the given lookup value
[a tuple of (alias, column, db_type)], lookup type, lookup
value, and the model field.
"""
- qn = self.quote_name
alias, col, db_type = lvalue
# Getting the quoted field as `geo_col`.
@@ -278,7 +312,7 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
op = tmp
geom = value
# Calling the `as_sql` function on the operation instance.
- return op.as_sql(geo_col, self.get_geom_placeholder(geom, field.srid))
+ return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
69 django/contrib/gis/db/models/fields.py
View
@@ -1,7 +1,7 @@
from django.db.models.fields import Field
from django.contrib.gis import forms
from django.contrib.gis.db.models.proxy import GeometryProxy
-from django.contrib.gis.geometry import Geometry, GeometryException
+from django.contrib.gis.geometry.backend import Geometry, GeometryException
from django.contrib.gis.measure import Distance
from django.db.models.sql.expressions import SQLEvaluator
@@ -40,8 +40,8 @@ def get_srid_info(srid, connection):
return _srid_cache[name][srid]
-class GeometryField(SpatialBackend.Field):
- """The base GIS field -- maps to the OpenGIS Specification Geometry type."""
+class GeometryField(Field):
+ "The base GIS field -- maps to the OpenGIS Specification Geometry type."
# The OpenGIS Geometry name.
geom_type = 'GEOMETRY'
@@ -50,7 +50,7 @@ class GeometryField(SpatialBackend.Field):
geodetic_units = ('Decimal Degree', 'degree')
def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2,
- **kwargs):
+ geography=False, **kwargs):
"""
The initialization function for geometry fields. Takes the following
as keyword arguments:
@@ -67,8 +67,14 @@ def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2,
dim:
The number of dimensions for this geometry. Defaults to 2.
- Oracle-specific keywords:
- extent, tolerance.
+ extent:
+ Customize the extent, in a 4-tuple of WGS 84 coordinates, for the
+ geometry field entry in the `USER_SDO_GEOM_METADATA` table. Defaults
+ to (-180.0, -90.0, 180.0, 90.0).
+
+ tolerance:
+ Define the tolerance, in meters, to use for the geometry field
+ entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05.
"""
# Setting the index flag with the value of the `spatial_index` keyword.
@@ -85,6 +91,9 @@ def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2,
# first parameter, so this works like normal fields.
kwargs['verbose_name'] = verbose_name
+ # Is this a geography rather than a geometry column?
+ self.geography = geography
+
# Oracle-specific private attributes for creating the entrie in
# `USER_SDO_GEOM_METADATA`
self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0))
@@ -121,17 +130,13 @@ def geodetic(self, connection):
"""
return self.units_name(connection) in self.geodetic_units
- def get_distance(self, dist_val, lookup_type, connection):
+ def get_distance(self, value, lookup_type, connection):
"""
Returns a distance number in units of the field. For example, if
`D(km=1)` was passed in and the units of the field were in meters,
then 1000 would be returned.
"""
- # Getting the distance parameter and any options.
- if len(dist_val) == 1:
- dist, option = dist_val[0], None
- else:
- dist, option = dist_val
+ return connection.ops.get_distance(self, value, lookup_type)
if isinstance(dist, Distance):
if self.geodetic(connection):
@@ -149,7 +154,7 @@ def get_distance(self, dist_val, lookup_type, connection):
if connection.ops.oracle and lookup_type == 'dwithin':
dist_param = 'distance=%s' % dist_param
-
+
if connection.ops.postgis and self.geodetic(connection) and lookup_type != 'dwithin' and option == 'spheroid':
# On PostGIS, by default `ST_distance_sphere` is used; but if the
# accuracy of `ST_distance_spheroid` is needed than the spheroid
@@ -179,11 +184,11 @@ def get_prep_value(self, value):
# from the given string input.
if isinstance(geom, Geometry):
pass
- elif isinstance(geom, basestring):
+ elif isinstance(geom, basestring) or hasattr(geom, '__geo_interface__'):
try:
geom = Geometry(geom)
except GeometryException:
- raise ValueError('Could not create geometry from lookup value: %s' % str(value))
+ raise ValueError('Could not create geometry from lookup value.')
else:
raise ValueError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
@@ -217,17 +222,7 @@ def contribute_to_class(self, cls, name):
setattr(cls, self.attname, GeometryProxy(Geometry, self))
def db_type(self, connection):
- if (connection.ops.postgis or
- connection.ops.spatialite):
- # Geometry columns on these spatial backends are initialized via
- # the `AddGeometryColumn` stored procedure.
- return None
- elif connection.ops.mysql:
- return self.geom_type
- elif connection.ops.oracle:
- return 'MDSYS.SDO_GEOMETRY'
- else:
- raise NotImplementedError
+ return connection.ops.geo_db_type(self)
def formfield(self, **kwargs):
defaults = {'form_class' : forms.GeometryField,
@@ -240,7 +235,11 @@ def formfield(self, **kwargs):
def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
"""
- XXX: Document me.
+ Prepare for the database lookup, and return any spatial parameters
+ necessary for the query. This includes wrapping any geometry
+ parameters with a backend-specific adapter and formatting any distance
+ parameters into the correct units for the coordinate system of the
+ field.
"""
if lookup_type in connection.ops.gis_terms:
# special case for isnull lookup
@@ -254,8 +253,6 @@ def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
if lookup_type in connection.ops.distance_functions:
# Getting the distance parameter in the units of the field.
params += self.get_distance(value[1:], lookup_type, connection)
- elif lookup_type in connection.ops.limited_where:
- pass
else:
params += value[1:]
elif isinstance(value, SQLEvaluator):
@@ -281,33 +278,31 @@ def get_db_prep_save(self, value, connection):
return connection.ops.Adapter(self.get_prep_value(value))
def get_placeholder(self, value, connection):
- return connection.ops.get_geom_placeholder(value, self.srid)
+ """
+ Returns the placeholder for the geometry column for the
+ given value.
+ """
+ return connection.ops.get_geom_placeholder(self, value)
# The OpenGIS Geometry Type Fields
class PointField(GeometryField):
- """Point"""
geom_type = 'POINT'
class LineStringField(GeometryField):
- """Line string"""
geom_type = 'LINESTRING'
class PolygonField(GeometryField):
- """Polygon"""
geom_type = 'POLYGON'
class MultiPointField(GeometryField):
- """Multi-point"""
geom_type = 'MULTIPOINT'
class MultiLineStringField(GeometryField):
- """Multi-line string"""
geom_type = 'MULTILINESTRING'
class MultiPolygonField(GeometryField):
- """Multi polygon"""
geom_type = 'MULTIPOLYGON'
class GeometryCollectionField(GeometryField):
- """Geometry collection"""
geom_type = 'GEOMETRYCOLLECTION'
+
6 django/contrib/gis/db/models/manager.py
View
@@ -1,6 +1,5 @@
from django.db.models.manager import Manager
from django.contrib.gis.db.models.query import GeoQuerySet
-from django.contrib.gis.db.models.sql.subqueries import insert_query
class GeoManager(Manager):
"Overrides Manager to return Geographic QuerySets."
@@ -54,7 +53,7 @@ def length(self, *args, **kwargs):
def make_line(self, *args, **kwargs):
return self.get_query_set().make_line(*args, **kwargs)
-
+
def mem_size(self, *args, **kwargs):
return self.get_query_set().mem_size(*args, **kwargs)
@@ -93,6 +92,3 @@ def union(self, *args, **kwargs):
def unionagg(self, *args, **kwargs):
return self.get_query_set().unionagg(*args, **kwargs)
-
- def _insert(self, values, **kwargs):
- return insert_query(self.model, values, **kwargs)
10 django/contrib/gis/db/models/query.py
View
@@ -4,7 +4,7 @@
from django.contrib.gis.db.models import aggregates
from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField
from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
-from django.contrib.gis.geometry import Geometry
+from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Area, Distance
class GeoQuerySet(QuerySet):
@@ -542,6 +542,7 @@ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, *
# units of the geometry field.
connection = connections[self.db]
geodetic = geo_field.geodetic(connection)
+ geography = geo_field.geography
if geodetic:
dist_att = 'm'
@@ -569,7 +570,8 @@ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, *
# keyword or when calculating the length of geodetic field, make
# sure the 'spheroid' distance setting string is passed in so we
# get the correct spatial stored procedure.
- if spheroid or (backend.postgis and geodetic and length):
+ if spheroid or (backend.postgis and geodetic and
+ (not geography) and length):
lookup_params.append('spheroid')
lookup_params = geo_field.get_prep_value(lookup_params)
params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection)
@@ -625,7 +627,7 @@ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, *
# `transform()` was not used on this GeoQuerySet.
procedure_fmt = '%(geo_col)s,%(geom)s'
- if geodetic:
+ if not geography and geodetic:
# Spherical distance calculation is needed (because the geographic
# field is geodetic). However, the PostGIS ST_distance_sphere/spheroid()
# procedures may only do queries from point columns to point geometries
@@ -644,7 +646,7 @@ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, *
procedure_args.update({'function' : backend.distance_sphere})
elif length or perimeter:
procedure_fmt = '%(geo_col)s'
- if geodetic and length:
+ if not geography and geodetic and length:
# There's no `length_sphere`, and `length_spheroid` also
# works on 3D geometries.
procedure_fmt += ",'%(spheroid)s'"
2  django/contrib/gis/db/models/sql/query.py
View
@@ -5,7 +5,7 @@
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField
from django.contrib.gis.db.models.sql.where import GeoWhereNode
-from django.contrib.gis.geometry import Geometry
+from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Area, Distance
36 django/contrib/gis/db/models/sql/subqueries.py
View
@@ -1,36 +0,0 @@
-from django.db import connections
-from django.db.models.sql.subqueries import InsertQuery
-
-class GeoInsertQuery(InsertQuery):
- def insert_values(self, insert_values, raw_values=False):
- """
- Set up the insert query from the 'insert_values' dictionary. The
- dictionary gives the model field names and their target values.
-
- If 'raw_values' is True, the values in the 'insert_values' dictionary
- are inserted directly into the query, rather than passed as SQL
- parameters. This provides a way to insert NULL and DEFAULT keywords
- into the query, for example.
- """
- placeholders, values = [], []
- for field, val in insert_values:
- placeholders.append((field, val))
- self.columns.append(field.column)
-
- if not placeholders[-1] == 'NULL':
- values.append(val)
- if raw_values:
- self.values.extend([(None, v) for v in values])
- else:
- self.params += tuple(values)
- self.values.extend(placeholders)
-
-def insert_query(model, values, return_id=False, raw_values=False, using=None):
- """
- Inserts a new record for the given model. This provides an interface to
- the InsertQuery class and is how Model.save() is implemented. It is not
- part of the public API.
- """
- query = GeoInsertQuery(model)
- query.insert_values(values, raw_values)
- return query.get_compiler(using=using).execute_sql(return_id)
6 django/contrib/gis/db/models/sql/where.py
View
@@ -44,7 +44,7 @@ def make_atom(self, child, qn, connection):
lvalue, lookup_type, value_annot, params_or_value = child
if isinstance(lvalue, GeoConstraint):
data, params = lvalue.process(lookup_type, params_or_value, connection)
- spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field)
+ spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn)
return spatial_sql, params
else:
return super(GeoWhereNode, self).make_atom(child, qn, connection)
@@ -52,7 +52,7 @@ def make_atom(self, child, qn, connection):
@classmethod
def _check_geo_field(cls, opts, lookup):
"""
- Utility for checking the given lookup with the given model options.
+ Utility for checking the given lookup with the given model options.
The lookup is a string either specifying the geographic field, e.g.
'point, 'the_geom', or a related lookup on a geographic field like
'address__point'.
@@ -74,7 +74,7 @@ def _check_geo_field(cls, opts, lookup):
# If the field list is still around, then it means that the
# lookup was for a geometry field across a relationship --
# thus we keep on getting the related model options and the
- # model field associated with the next field in the list
+ # model field associated with the next field in the list
# until there's no more left.
while len(field_list):
opts = geo_fld.rel.to._meta
9 django/contrib/gis/geometry/__init__.py
View
@@ -1,9 +0,0 @@
-from django.conf import settings
-
-__all__ = ['Geometry', 'GeometryException']
-
-from django.contrib.gis.geos import GEOSGeometry, GEOSException
-
-Geometry = GEOSGeometry
-GeometryException = GEOSException
-
21 django/contrib/gis/geometry/backend/__init__.py
View
@@ -0,0 +1,21 @@
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos')
+
+try:
+ module = import_module('.%s' % geom_backend, 'django.contrib.gis.geometry.backend')
+except ImportError, e:
+ try:
+ module = import_module(geom_backend)
+ except ImportError, e_user:
+ raise ImproperlyConfigured('Could not import user-defined GEOMETRY_BACKEND '
+ '"%s".' % geom_backend)
+
+try:
+ Geometry = module.Geometry
+ GeometryException = module.GeometryException
+except AttributeError:
+ raise ImproperlyConfigured('Cannot import Geometry from the "%s" '
+ 'geometry backend.' % geom_backend)
3  django/contrib/gis/geometry/backend/geos.py
View
@@ -0,0 +1,3 @@
+from django.contrib.gis.geos import \
+ GEOSGeometry as Geometry, \
+ GEOSException as GeometryException
2  django/contrib/gis/tests/relatedapp/tests.py
View
@@ -1,7 +1,7 @@
import os, unittest
from django.contrib.gis.geos import *
from django.contrib.gis.db.models import Collect, Count, Extent, F, Union
-from django.contrib.gis.geometry import Geometry
+from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.tests.utils import mysql, oracle, postgis, spatialite, no_mysql, no_oracle, no_spatialite
from django.conf import settings
from models import City, Location, DirectoryEntry, Parcel, Book, Author
Please sign in to comment.
Something went wrong with that request. Please try again.