summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Paroz <claude@2xlibre.net>2014-05-22 21:51:30 +0200
committerClaude Paroz <claude@2xlibre.net>2014-10-09 21:38:50 +0200
commit2bd1bbc42430f9815d16b779735b77c35ea4cde7 (patch)
treebad0ecbb04a6ba8f45a68b8842453fa3c80828f1
parent4ef9618e12510a84a4d21440b1e18ab704ee6d73 (diff)
Converted GIS lookups to use the new Lookup API
Thanks Tim Graham, Anssi Kääriäinen and Marc Tamlyn for the reviews.
-rw-r--r--django/contrib/gis/db/backends/base.py17
-rw-r--r--django/contrib/gis/db/backends/mysql/operations.py47
-rw-r--r--django/contrib/gis/db/backends/oracle/operations.py6
-rw-r--r--django/contrib/gis/db/backends/postgis/operations.py291
-rw-r--r--django/contrib/gis/db/backends/spatialite/operations.py149
-rw-r--r--django/contrib/gis/db/backends/utils.py51
-rw-r--r--django/contrib/gis/db/models/constants.py15
-rw-r--r--django/contrib/gis/db/models/fields.py20
-rw-r--r--django/contrib/gis/db/models/lookups.py290
-rw-r--r--django/contrib/gis/db/models/sql/query.py4
-rw-r--r--docs/releases/1.8.txt3
11 files changed, 408 insertions, 485 deletions
diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py
index a132e80661..c6e48ce0ef 100644
--- a/django/contrib/gis/db/backends/base.py
+++ b/django/contrib/gis/db/backends/base.py
@@ -42,19 +42,19 @@ class BaseSpatialFeatures(object):
@property
def supports_bbcontains_lookup(self):
- return 'bbcontains' in self.connection.ops.gis_terms
+ return 'bbcontains' in self.connection.ops.gis_operators
@property
def supports_contained_lookup(self):
- return 'contained' in self.connection.ops.gis_terms
+ return 'contained' in self.connection.ops.gis_operators
@property
def supports_dwithin_lookup(self):
- return 'dwithin' in self.connection.ops.distance_functions
+ return 'dwithin' in self.connection.ops.gis_operators
@property
def supports_relate_lookup(self):
- return 'relate' in self.connection.ops.gis_terms
+ return 'relate' in self.connection.ops.gis_operators
# For each of those methods, the class will have a property named
# `has_<name>_method` (defined in __init__) which accesses connection.ops
@@ -97,12 +97,6 @@ class BaseSpatialOperations(object):
instantiated by each spatial database backend with the features
it has.
"""
- distance_functions = {}
- geometry_functions = {}
- geometry_operators = {}
- geography_operators = {}
- geography_functions = {}
- gis_terms = set()
truncate_params = {}
# Quick booleans for the type of this spatial backend, and
@@ -215,9 +209,6 @@ class BaseSpatialOperations(object):
def spatial_aggregate_sql(self, agg):
raise NotImplementedError('Aggregate support not implemented for this spatial backend.')
- def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
- raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_lookup_sql() method')
-
# Routines for getting the OGC-compliant models.
def geometry_columns(self):
raise NotImplementedError('subclasses of BaseSpatialOperations must a provide geometry_columns() method')
diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py
index a9e264cb02..bcbd634fd8 100644
--- a/django/contrib/gis/db/backends/mysql/operations.py
+++ b/django/contrib/gis/db/backends/mysql/operations.py
@@ -2,6 +2,7 @@ from django.db.backends.mysql.base import DatabaseOperations
from django.contrib.gis.db.backends.adapter import WKTAdapter
from django.contrib.gis.db.backends.base import BaseSpatialOperations
+from django.contrib.gis.db.backends.utils import SpatialOperator
class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
@@ -16,27 +17,25 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
Adapter = WKTAdapter
Adaptor = Adapter # Backwards-compatibility alias.
- geometry_functions = {
- 'bbcontains': 'MBRContains', # For consistency w/PostGIS API
- 'bboverlaps': 'MBROverlaps', # .. ..
- 'contained': 'MBRWithin', # .. ..
- 'contains': 'MBRContains',
- 'disjoint': 'MBRDisjoint',
- 'equals': 'MBREqual',
- 'exact': 'MBREqual',
- 'intersects': 'MBRIntersects',
- 'overlaps': 'MBROverlaps',
- 'same_as': 'MBREqual',
- 'touches': 'MBRTouches',
- 'within': 'MBRWithin',
+ gis_operators = {
+ 'bbcontains': SpatialOperator(func='MBRContains'), # For consistency w/PostGIS API
+ 'bboverlaps': SpatialOperator(func='MBROverlaps'), # .. ..
+ 'contained': SpatialOperator(func='MBRWithin'), # .. ..
+ 'contains': SpatialOperator(func='MBRContains'),
+ 'disjoint': SpatialOperator(func='MBRDisjoint'),
+ 'equals': SpatialOperator(func='MBREqual'),
+ 'exact': SpatialOperator(func='MBREqual'),
+ 'intersects': SpatialOperator(func='MBRIntersects'),
+ 'overlaps': SpatialOperator(func='MBROverlaps'),
+ 'same_as': SpatialOperator(func='MBREqual'),
+ 'touches': SpatialOperator(func='MBRTouches'),
+ 'within': SpatialOperator(func='MBRWithin'),
}
- gis_terms = set(geometry_functions) | {'isnull'}
-
def geo_db_type(self, f):
return f.geom_type
- def get_geom_placeholder(self, value, srid):
+ def get_geom_placeholder(self, f, value):
"""
The placeholder here has to include MySQL's WKT constructor. Because
MySQL does not support spatial transformations, there is no need to
@@ -47,19 +46,3 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
else:
placeholder = '%s(%%s)' % self.from_text
return placeholder
-
- def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
- geo_col, db_type = lvalue
-
- lookup_info = self.geometry_functions.get(lookup_type, False)
- if lookup_info:
- sql = "%s(%s, %s)" % (lookup_info, geo_col,
- self.get_geom_placeholder(value, field.srid))
- return sql, []
-
- # TODO: Is this really necessary? MySQL can't handle NULL geometries
- # in its spatial indexes anyways.
- if lookup_type == 'isnull':
- return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
-
- raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py
index 45ebdf32d3..a305f81f58 100644
--- a/django/contrib/gis/db/backends/oracle/operations.py
+++ b/django/contrib/gis/db/backends/oracle/operations.py
@@ -244,6 +244,12 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations):
else:
return 'SDO_GEOMETRY(%%s, %s)' % f.srid
+ def check_relate_argument(self, arg):
+ masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
+ mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
+ if not self.mask_regex.match(arg):
+ raise ValueError('Invalid SDO_RELATE mask: "%s"' % (self.relate_func, arg))
+
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
geo_col, db_type = lvalue
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index c41748cca2..690194fa9e 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -1,73 +1,46 @@
import re
-from decimal import Decimal
from django.conf import settings
from django.contrib.gis.db.backends.base import BaseSpatialOperations
-from django.contrib.gis.db.backends.utils import SpatialOperation, SpatialFunction
from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
+from django.contrib.gis.db.backends.utils import SpatialOperator
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_psycopg2.base import DatabaseOperations
from django.db.utils import ProgrammingError
-from django.utils import six
from django.utils.functional import cached_property
from .models import PostGISGeometryColumns, PostGISSpatialRefSys
-#### Classes used in constructing PostGIS spatial SQL ####
-class PostGISOperator(SpatialOperation):
- "For PostGIS operators (e.g. `&&`, `~`)."
- def __init__(self, operator):
- super(PostGISOperator, self).__init__(operator=operator)
-
-
-class PostGISFunction(SpatialFunction):
- "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
- def __init__(self, prefix, function, **kwargs):
- super(PostGISFunction, self).__init__(prefix + function, **kwargs)
-
-
-class PostGISFunctionParam(PostGISFunction):
- "For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
- sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
-
-
-class PostGISDistance(PostGISFunction):
- "For PostGIS distance operations."
- dist_func = 'Distance'
- sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
-
- def __init__(self, prefix, operator):
- super(PostGISDistance, self).__init__(prefix, self.dist_func,
- operator=operator)
-
-
-class PostGISSpheroidDistance(PostGISFunction):
- "For PostGIS spherical distance operations (using the spheroid)."
- dist_func = 'distance_spheroid'
- sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s'
-
- def __init__(self, prefix, operator):
- # An extra parameter in `end_subst` is needed for the spheroid string.
- super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func,
- operator=operator)
-
+class PostGISOperator(SpatialOperator):
+ def __init__(self, geography=False, **kwargs):
+ # Only a subset of the operators and functions are available
+ # for the geography type.
+ self.geography = geography
+ super(PostGISOperator, self).__init__(**kwargs)
-class PostGISSphereDistance(PostGISDistance):
- "For PostGIS spherical distance operations."
- dist_func = 'distance_sphere'
+ def as_sql(self, connection, lookup, *args):
+ if lookup.lhs.source.geography and not self.geography:
+ raise ValueError('PostGIS geography does not support the "%s" '
+ 'function/operator.' % (self.func or self.op,))
+ return super(PostGISOperator, self).as_sql(connection, lookup, *args)
-class PostGISRelate(PostGISFunctionParam):
- "For PostGIS Relate(<geom>, <pattern>) calls."
- pattern_regex = re.compile(r'^[012TF\*]{9}$')
+class PostGISDistanceOperator(PostGISOperator):
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %%s'
- def __init__(self, prefix, pattern):
- if not self.pattern_regex.match(pattern):
- raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
- super(PostGISRelate, self).__init__(prefix, 'Relate')
+ def as_sql(self, connection, lookup, template_params, sql_params):
+ if not lookup.lhs.source.geography and lookup.lhs.source.geodetic(connection):
+ sql_template = self.sql_template
+ if len(lookup.rhs) == 3 and lookup.rhs[-1] == 'spheroid':
+ template_params.update({'op': self.op, 'func': 'ST_Distance_Spheroid'})
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s) %(op)s %%s'
+ else:
+ template_params.update({'op': self.op, 'func': 'ST_Distance_Sphere'})
+ return sql_template % template_params, sql_params
+ return super(PostGISDistanceOperator, self).as_sql(connection, lookup, template_params, sql_params)
class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
@@ -82,104 +55,43 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
Adapter = PostGISAdapter
Adaptor = Adapter # Backwards-compatibility alias.
+ gis_operators = {
+ 'bbcontains': PostGISOperator(op='~'),
+ 'bboverlaps': PostGISOperator(op='&&', geography=True),
+ 'contained': PostGISOperator(op='@'),
+ 'contains': PostGISOperator(func='ST_Contains'),
+ 'overlaps_left': PostGISOperator(op='&<'),
+ 'overlaps_right': PostGISOperator(op='&>'),
+ 'overlaps_below': PostGISOperator(op='&<|'),
+ 'overlaps_above': PostGISOperator(op='|&>'),
+ 'left': PostGISOperator(op='<<'),
+ 'right': PostGISOperator(op='>>'),
+ 'strictly_below': PostGISOperator(op='<<|'),
+ 'stricly_above': PostGISOperator(op='|>>'),
+ 'same_as': PostGISOperator(op='~='),
+ 'exact': PostGISOperator(op='~='), # alias of same_as
+ 'contains_properly': PostGISOperator(func='ST_ContainsProperly'),
+ 'coveredby': PostGISOperator(func='ST_CoveredBy', geography=True),
+ 'covers': PostGISOperator(func='ST_Covers', geography=True),
+ 'crosses': PostGISOperator(func='ST_Crosses)'),
+ 'disjoint': PostGISOperator(func='ST_Disjoint'),
+ 'equals': PostGISOperator(func='ST_Equals'),
+ 'intersects': PostGISOperator(func='ST_Intersects', geography=True),
+ 'overlaps': PostGISOperator(func='ST_Overlaps'),
+ 'relate': PostGISOperator(func='ST_Relate'),
+ 'touches': PostGISOperator(func='ST_Touches'),
+ 'within': PostGISOperator(func='ST_Within'),
+ 'dwithin': PostGISOperator(func='ST_DWithin', geography=True),
+ 'distance_gt': PostGISDistanceOperator(func='ST_Distance', op='>', geography=True),
+ 'distance_gte': PostGISDistanceOperator(func='ST_Distance', op='>=', geography=True),
+ 'distance_lt': PostGISDistanceOperator(func='ST_Distance', op='<', geography=True),
+ 'distance_lte': PostGISDistanceOperator(func='ST_Distance', op='<=', geography=True),
+ }
+
def __init__(self, connection):
super(PostGISOperations, self).__init__(connection)
prefix = self.geom_func_prefix
- # PostGIS-specific operators. The commented descriptions of these
- # operators come from Section 7.6 of the PostGIS 1.4 documentation.
- 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('&<'),
- # The "&>" operator returns true if A's bounding box overlaps or
- # is to the right of B's bounding box.
- 'overlaps_right': PostGISOperator('&>'),
- # The "<<" operator returns true if A's bounding box is strictly
- # to the left of B's bounding box.
- 'left': PostGISOperator('<<'),
- # The ">>" operator returns true if A's bounding box is strictly
- # to the right of B's bounding box.
- 'right': PostGISOperator('>>'),
- # The "&<|" operator returns true if A's bounding box overlaps or
- # is below B's bounding box.
- 'overlaps_below': PostGISOperator('&<|'),
- # The "|&>" operator returns true if A's bounding box overlaps or
- # is above B's bounding box.
- 'overlaps_above': PostGISOperator('|&>'),
- # The "<<|" operator returns true if A's bounding box is strictly
- # below B's bounding box.
- 'strictly_below': PostGISOperator('<<|'),
- # The "|>>" operator returns true if A's bounding box is strictly
- # above B's bounding box.
- 'strictly_above': PostGISOperator('|>>'),
- # The "~=" operator is the "same as" operator. It tests actual
- # geometric equality of two features. So if A and B are the same feature,
- # vertex-by-vertex, the operator returns true.
- 'same_as': PostGISOperator('~='),
- 'exact': PostGISOperator('~='),
- # The "@" operator returns true if A's bounding box is completely contained
- # by B's bounding box.
- 'contained': PostGISOperator('@'),
- # The "~" operator returns true if A's bounding box completely contains
- # by B's bounding box.
- 'bbcontains': PostGISOperator('~'),
- # The "&&" operator returns true if A's bounding box overlaps
- # B's bounding box.
- 'bboverlaps': PostGISOperator('&&'),
- }
-
- self.geometry_functions = {
- 'equals': PostGISFunction(prefix, 'Equals'),
- 'disjoint': PostGISFunction(prefix, 'Disjoint'),
- 'touches': PostGISFunction(prefix, 'Touches'),
- 'crosses': PostGISFunction(prefix, 'Crosses'),
- 'within': PostGISFunction(prefix, 'Within'),
- 'overlaps': PostGISFunction(prefix, 'Overlaps'),
- 'contains': PostGISFunction(prefix, 'Contains'),
- 'intersects': PostGISFunction(prefix, 'Intersects'),
- 'relate': (PostGISRelate, six.string_types),
- 'coveredby': PostGISFunction(prefix, 'CoveredBy'),
- 'covers': PostGISFunction(prefix, 'Covers'),
- 'contains_properly': PostGISFunction(prefix, 'ContainsProperly'),
- }
-
- # Valid distance types and substitutions
- dtypes = (Decimal, Distance, float) + six.integer_types
-
- def get_dist_ops(operator):
- "Returns operations for both regular and spherical distances."
- return {'cartesian': PostGISDistance(prefix, operator),
- 'sphere': PostGISSphereDistance(prefix, operator),
- 'spheroid': PostGISSpheroidDistance(prefix, operator),
- }
- self.distance_functions = {
- 'distance_gt': (get_dist_ops('>'), dtypes),
- 'distance_gte': (get_dist_ops('>='), dtypes),
- 'distance_lt': (get_dist_ops('<'), dtypes),
- 'distance_lte': (get_dist_ops('<='), dtypes),
- 'dwithin': (PostGISFunctionParam(prefix, 'DWithin'), dtypes)
- }
-
- # Adding the distance functions to the geometries lookup.
- self.geometry_functions.update(self.distance_functions)
-
- # 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('&&'),
- }
-
- # Creating a dictionary lookup of all GIS terms for PostGIS.
- self.gis_terms = {'isnull'}
- self.gis_terms.update(self.geometry_operators)
- self.gis_terms.update(self.geometry_functions)
self.area = prefix + 'Area'
self.bounding_circle = prefix + 'MinimumBoundingCircle'
@@ -452,95 +364,6 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
else:
raise Exception('Could not determine PROJ.4 version from PostGIS.')
- 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(num_param)
- else:
- return exactly_two(num_param)
-
- 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.
- """
- geo_col, db_type = lvalue
-
- if lookup_type in self.geometry_operators:
- if field.geography and lookup_type not in self.geography_operators:
- raise ValueError('PostGIS geography does not support the '
- '"%s" lookup.' % lookup_type)
- # Handling a PostGIS operator.
- 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 lookup_type not 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]
-
- # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
- # distance lookups.
- if isinstance(tmp, tuple):
- # First element of tuple is the PostGISOperation instance, and the
- # second element is either the type or a tuple of acceptable types
- # that may passed in as further parameters for the lookup type.
- op, arg_type = tmp
-
- # Ensuring that a tuple _value_ was passed in from the user
- if not isinstance(value, (tuple, list)):
- raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
-
- # Geometry is first element of lookup tuple.
- geom = value[0]
-
- # Number of valid tuple parameters depends on the lookup type.
- nparams = len(value)
- if not self.num_params(lookup_type, nparams):
- raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
-
- # Ensuring the argument type matches what we expect.
- if not isinstance(value[1], arg_type):
- raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
-
- # For lookup type `relate`, the op instance is not yet created (has
- # to be instantiated here to check the pattern parameter).
- if lookup_type == 'relate':
- op = op(self.geom_func_prefix, value[1])
- elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
- if not field.geography and field.geodetic(self.connection):
- # Setting up the geodetic operation appropriately.
- if nparams == 3 and value[2] == 'spheroid':
- op = op['spheroid']
- else:
- op = op['sphere']
- else:
- op = op['cartesian']
- else:
- op = tmp
- geom = value
-
- # Calling the `as_sql` function on the operation instance.
- 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, ('' if value else 'NOT ')), []
-
- raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
-
def spatial_aggregate_sql(self, agg):
"""
Returns the spatial aggregate SQL template and function for the
diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py
index b051b250a4..d95b2f6a13 100644
--- a/django/contrib/gis/db/backends/spatialite/operations.py
+++ b/django/contrib/gis/db/backends/spatialite/operations.py
@@ -1,9 +1,8 @@
import re
import sys
-from decimal import Decimal
from django.contrib.gis.db.backends.base import BaseSpatialOperations
-from django.contrib.gis.db.backends.utils import SpatialOperation, SpatialFunction
+from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance
@@ -14,52 +13,6 @@ from django.utils import six
from django.utils.functional import cached_property
-class SpatiaLiteOperator(SpatialOperation):
- "For SpatiaLite operators (e.g. `&&`, `~`)."
- def __init__(self, operator):
- super(SpatiaLiteOperator, self).__init__(operator=operator)
-
-
-class SpatiaLiteFunction(SpatialFunction):
- "For SpatiaLite function calls."
- def __init__(self, function, **kwargs):
- super(SpatiaLiteFunction, self).__init__(function, **kwargs)
-
-
-class SpatiaLiteFunctionParam(SpatiaLiteFunction):
- "For SpatiaLite functions that take another parameter."
- sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
-
-
-class SpatiaLiteDistance(SpatiaLiteFunction):
- "For SpatiaLite distance operations."
- dist_func = 'Distance'
- sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
-
- def __init__(self, operator):
- super(SpatiaLiteDistance, self).__init__(self.dist_func,
- operator=operator)
-
-
-class SpatiaLiteRelate(SpatiaLiteFunctionParam):
- "For SpatiaLite Relate(<geom>, <pattern>) calls."
- pattern_regex = re.compile(r'^[012TF\*]{9}$')
-
- def __init__(self, pattern):
- if not self.pattern_regex.match(pattern):
- raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
- super(SpatiaLiteRelate, self).__init__('Relate')
-
-
-# Valid distance types and substitutions
-dtypes = (Decimal, Distance, float) + six.integer_types
-
-
-def get_dist_ops(operator):
- "Returns operations for regular distances; spherical distances are not currently supported."
- return (SpatiaLiteDistance(operator),)
-
-
class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
name = 'spatialite'
@@ -101,41 +54,31 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
from_wkb = 'GeomFromWKB'
select = 'AsText(%s)'
- geometry_functions = {
- 'equals': SpatiaLiteFunction('Equals'),
- 'disjoint': SpatiaLiteFunction('Disjoint'),
- 'touches': SpatiaLiteFunction('Touches'),
- 'crosses': SpatiaLiteFunction('Crosses'),
- 'within': SpatiaLiteFunction('Within'),
- 'overlaps': SpatiaLiteFunction('Overlaps'),
- 'contains': SpatiaLiteFunction('Contains'),
- 'intersects': SpatiaLiteFunction('Intersects'),
- 'relate': (SpatiaLiteRelate, six.string_types),
+ gis_operators = {
+ 'equals': SpatialOperator(func='Equals'),
+ 'disjoint': SpatialOperator(func='Disjoint'),
+ 'touches': SpatialOperator(func='Touches'),
+ 'crosses': SpatialOperator(func='Crosses'),
+ 'within': SpatialOperator(func='Within'),
+ 'overlaps': SpatialOperator(func='Overlaps'),
+ 'contains': SpatialOperator(func='Contains'),
+ 'intersects': SpatialOperator(func='Intersects'),
+ 'relate': SpatialOperator(func='Relate'),
# Returns true if B's bounding box completely contains A's bounding box.
- 'contained': SpatiaLiteFunction('MbrWithin'),
+ 'contained': SpatialOperator(func='MbrWithin'),
# Returns true if A's bounding box completely contains B's bounding box.
- 'bbcontains': SpatiaLiteFunction('MbrContains'),
+ 'bbcontains': SpatialOperator(func='MbrContains'),
# Returns true if A's bounding box overlaps B's bounding box.
- 'bboverlaps': SpatiaLiteFunction('MbrOverlaps'),
+ 'bboverlaps': SpatialOperator(func='MbrOverlaps'),
# These are implemented here as synonyms for Equals
- 'same_as': SpatiaLiteFunction('Equals'),
- 'exact': SpatiaLiteFunction('Equals'),
- }
+ 'same_as': SpatialOperator(func='Equals'),
+ 'exact': SpatialOperator(func='Equals'),
- distance_functions = {
- 'distance_gt': (get_dist_ops('>'), dtypes),
- 'distance_gte': (get_dist_ops('>='), dtypes),
- 'distance_lt': (get_dist_ops('<'), dtypes),
- 'distance_lte': (get_dist_ops('<='), dtypes),
+ 'distance_gt': SpatialOperator(func='Distance', op='>'),
+ 'distance_gte': SpatialOperator(func='Distance', op='>='),
+ 'distance_lt': SpatialOperator(func='Distance', op='<'),
+ 'distance_lte': SpatialOperator(func='Distance', op='<='),
}
- geometry_functions.update(distance_functions)
-
- def __init__(self, connection):
- super(DatabaseOperations, self).__init__(connection)
-
- # Creating the GIS terms dictionary.
- self.gis_terms = {'isnull'}
- self.gis_terms.update(self.geometry_functions)
@cached_property
def spatial_version(self):
@@ -316,58 +259,6 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
sql_function = getattr(self, agg_name)
return sql_template, sql_function
- 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, the model field, and the quoting function.
- """
- geo_col, db_type = lvalue
-
- if lookup_type in self.geometry_functions:
- # See if a SpatiaLite geometry function matches the lookup type.
- tmp = self.geometry_functions[lookup_type]
-
- # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
- # distance lookups.
- if isinstance(tmp, tuple):
- # First element of tuple is the SpatiaLiteOperation instance, and the
- # second element is either the type or a tuple of acceptable types
- # that may passed in as further parameters for the lookup type.
- op, arg_type = tmp
-
- # Ensuring that a tuple _value_ was passed in from the user
- if not isinstance(value, (tuple, list)):
- raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
-
- # Geometry is first element of lookup tuple.
- geom = value[0]
-
- # Number of valid tuple parameters depends on the lookup type.
- if len(value) != 2:
- raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
-
- # Ensuring the argument type matches what we expect.
- if not isinstance(value[1], arg_type):
- raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
-
- # For lookup type `relate`, the op instance is not yet created (has
- # to be instantiated here to check the pattern parameter).
- if lookup_type == 'relate':
- op = op(value[1])
- elif lookup_type in self.distance_functions:
- op = op[0]
- else:
- op = tmp
- geom = value
- # Calling the `as_sql` function on the operation instance.
- 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, ('' if value else 'NOT ')), []
-
- raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
-
# Routines for getting the OGC-compliant models.
def geometry_columns(self):
from django.contrib.gis.db.backends.spatialite.models import SpatialiteGeometryColumns
diff --git a/django/contrib/gis/db/backends/utils.py b/django/contrib/gis/db/backends/utils.py
index b8b5b9525f..9c3b700852 100644
--- a/django/contrib/gis/db/backends/utils.py
+++ b/django/contrib/gis/db/backends/utils.py
@@ -4,43 +4,24 @@ backends.
"""
-class SpatialOperation(object):
+class SpatialOperator(object):
"""
- Base class for generating spatial SQL.
+ Class encapsulating the behavior specific to a GIS operation (used by lookups).
"""
- sql_template = '%(geo_col)s %(operator)s %(geometry)s'
+ sql_template = None
- def __init__(self, function='', operator='', result='', **kwargs):
- self.function = function
- self.operator = operator
- self.result = result
- self.extra = kwargs
+ def __init__(self, op=None, func=None):
+ self.op = op
+ self.func = func
- def as_sql(self, geo_col, geometry='%s'):
- return self.sql_template % self.params(geo_col, geometry), []
+ @property
+ def default_template(self):
+ if self.func:
+ return '%(func)s(%(lhs)s, %(rhs)s)'
+ else:
+ return '%(lhs)s %(op)s %(rhs)s'
- def params(self, geo_col, geometry):
- params = {'function': self.function,
- 'geo_col': geo_col,
- 'geometry': geometry,
- 'operator': self.operator,
- 'result': self.result,
- }
- params.update(self.extra)
- return params
-
-
-class SpatialFunction(SpatialOperation):
- """
- Base class for generating spatial SQL related to a function.
- """
- sql_template = '%(function)s(%(geo_col)s, %(geometry)s)'
-
- def __init__(self, func, result='', operator='', **kwargs):
- # Getting the function prefix.
- default = {'function': func,
- 'operator': operator,
- 'result': result
- }
- kwargs.update(default)
- super(SpatialFunction, self).__init__(**kwargs)
+ def as_sql(self, connection, lookup, template_params, sql_params):
+ sql_template = self.sql_template or lookup.sql_template or self.default_template
+ template_params.update({'op': self.op, 'func': self.func})
+ return sql_template % template_params, sql_params
diff --git a/django/contrib/gis/db/models/constants.py b/django/contrib/gis/db/models/constants.py
deleted file mode 100644
index 4ece41415c..0000000000
--- a/django/contrib/gis/db/models/constants.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.db.models.sql.constants import QUERY_TERMS
-
-GIS_LOOKUPS = {
- 'bbcontains', 'bboverlaps', 'contained', 'contains',
- 'contains_properly', 'coveredby', 'covers', 'crosses', 'disjoint',
- 'distance_gt', 'distance_gte', 'distance_lt', 'distance_lte',
- 'dwithin', 'equals', 'exact',
- 'intersects', 'overlaps', 'relate', 'same_as', 'touches', 'within',
- 'left', 'right', 'overlaps_left', 'overlaps_right',
- 'overlaps_above', 'overlaps_below',
- 'strictly_above', 'strictly_below'
-}
-ALL_TERMS = GIS_LOOKUPS | QUERY_TERMS
-
-__all__ = ['ALL_TERMS', 'GIS_LOOKUPS']
diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py
index c79548ed8a..e260f67cbf 100644
--- a/django/contrib/gis/db/models/fields.py
+++ b/django/contrib/gis/db/models/fields.py
@@ -2,8 +2,7 @@ from django.db.models.fields import Field
from django.db.models.sql.expressions import SQLEvaluator
from django.utils.translation import ugettext_lazy as _
from django.contrib.gis import forms
-from django.contrib.gis.db.models.constants import GIS_LOOKUPS
-from django.contrib.gis.db.models.lookups import GISLookup
+from django.contrib.gis.db.models.lookups import gis_lookups
from django.contrib.gis.db.models.proxy import GeometryProxy
from django.contrib.gis.geometry.backend import Geometry, GeometryException
from django.utils import six
@@ -243,16 +242,15 @@ class GeometryField(Field):
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
- if lookup_type == 'isnull':
- return []
-
+ # special case for isnull lookup
+ if lookup_type == 'isnull':
+ return []
+ elif lookup_type in self.class_lookups:
# Populating the parameters list, and wrapping the Geometry
# with the Adapter of the spatial backend.
if isinstance(value, (tuple, list)):
params = [connection.ops.Adapter(value[0])]
- if lookup_type in connection.ops.distance_functions:
+ if self.class_lookups[lookup_type].distance:
# 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.truncate_params:
@@ -291,9 +289,9 @@ class GeometryField(Field):
"""
return connection.ops.get_geom_placeholder(self, value)
-for lookup_name in GIS_LOOKUPS:
- lookup = type(lookup_name, (GISLookup,), {'lookup_name': lookup_name})
- GeometryField.register_lookup(lookup)
+
+for klass in gis_lookups.values():
+ GeometryField.register_lookup(klass)
# The OpenGIS Geometry Type Fields
diff --git a/django/contrib/gis/db/models/lookups.py b/django/contrib/gis/db/models/lookups.py
index 9552881c6c..d3b53b32f0 100644
--- a/django/contrib/gis/db/models/lookups.py
+++ b/django/contrib/gis/db/models/lookups.py
@@ -1,10 +1,20 @@
+from __future__ import unicode_literals
+import re
+
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields import FieldDoesNotExist
from django.db.models.lookups import Lookup
from django.db.models.sql.expressions import SQLEvaluator
+from django.utils import six
+
+gis_lookups = {}
class GISLookup(Lookup):
+ sql_template = None
+ transform_func = None
+ distance = False
+
@classmethod
def _check_geo_field(cls, opts, lookup):
"""
@@ -45,10 +55,19 @@ class GISLookup(Lookup):
else:
return False
- def as_sql(self, qn, connection):
- # We use the same approach as was used by GeoWhereNode. It would
- # be a good idea to upgrade GIS to use similar code that is used
- # for other lookups.
+ def get_db_prep_lookup(self, value, connection):
+ # get_db_prep_lookup is called by process_rhs from super class
+ if isinstance(value, (tuple, list)):
+ # First param is assumed to be the geometric object
+ params = [connection.ops.Adapter(value[0])] + list(value)[1:]
+ else:
+ params = [connection.ops.Adapter(value)]
+ return ('%s', params)
+
+ def process_rhs(self, qn, connection):
+ rhs, rhs_params = super(GISLookup, self).process_rhs(qn, connection)
+
+ geom = self.rhs
if isinstance(self.rhs, SQLEvaluator):
# Make sure the F Expression destination field exists, and
# set an `srid` attribute with the same as that of the
@@ -57,13 +76,256 @@ class GISLookup(Lookup):
if not geo_fld:
raise ValueError('No geographic field found in expression.')
self.rhs.srid = geo_fld.srid
- db_type = self.lhs.output_field.db_type(connection=connection)
- params = self.lhs.output_field.get_db_prep_lookup(
- self.lookup_name, self.rhs, connection=connection)
- lhs_sql, lhs_params = self.process_lhs(qn, connection)
- # lhs_params not currently supported.
- assert not lhs_params
- data = (lhs_sql, db_type)
- spatial_sql, spatial_params = connection.ops.spatial_lookup_sql(
- data, self.lookup_name, self.rhs, self.lhs.output_field, qn)
- return spatial_sql, spatial_params + params
+ elif isinstance(self.rhs, (list, tuple)):
+ geom = self.rhs[0]
+
+ rhs = connection.ops.get_geom_placeholder(self.lhs.source, geom)
+ return rhs, rhs_params
+
+ def as_sql(self, qn, connection):
+ lhs_sql, sql_params = self.process_lhs(qn, connection)
+ rhs_sql, rhs_params = self.process_rhs(qn, connection)
+ sql_params.extend(rhs_params)
+
+ template_params = {'lhs': lhs_sql, 'rhs': rhs_sql}
+ backend_op = connection.ops.gis_operators[self.lookup_name]
+ return backend_op.as_sql(connection, self, template_params, sql_params)
+
+
+# ------------------
+# Geometry operators
+# ------------------
+
+class OverlapsLeftLookup(GISLookup):
+ """
+ The overlaps_left operator returns true if A's bounding box overlaps or is to the
+ left of B's bounding box.
+ """
+ lookup_name = 'overlaps_left'
+gis_lookups['overlaps_left'] = OverlapsLeftLookup
+
+
+class OverlapsRightLookup(GISLookup):
+ """
+ The 'overlaps_right' operator returns true if A's bounding box overlaps or is to the
+ right of B's bounding box.
+ """
+ lookup_name = 'overlaps_right'
+gis_lookups['overlaps_right'] = OverlapsRightLookup
+
+
+class OverlapsBelowLookup(GISLookup):
+ """
+ The 'overlaps_below' operator returns true if A's bounding box overlaps or is below
+ B's bounding box.
+ """
+ lookup_name = 'overlaps_below'
+gis_lookups['overlaps_below'] = OverlapsBelowLookup
+
+
+class OverlapsAboveLookup(GISLookup):
+ """
+ The 'overlaps_above' operator returns true if A's bounding box overlaps or is above
+ B's bounding box.
+ """
+ lookup_name = 'overlaps_above'
+gis_lookups['overlaps_above'] = OverlapsAboveLookup
+
+
+class LeftLookup(GISLookup):
+ """
+ The 'left' operator returns true if A's bounding box is strictly to the left
+ of B's bounding box.
+ """
+ lookup_name = 'left'
+gis_lookups['left'] = LeftLookup
+
+
+class RightLookup(GISLookup):
+ """
+ The 'right' operator returns true if A's bounding box is strictly to the right
+ of B's bounding box.
+ """
+ lookup_name = 'right'
+gis_lookups['right'] = RightLookup
+
+
+class StrictlyBelowLookup(GISLookup):
+ """
+ The 'strictly_below' operator returns true if A's bounding box is strictly below B's
+ bounding box.
+ """
+ lookup_name = 'strictly_below'
+gis_lookups['strictly_below'] = StrictlyBelowLookup
+
+
+class StrictlyAboveLookup(GISLookup):
+ """
+ The 'strictly_above' operator returns true if A's bounding box is strictly above B's
+ bounding box.
+ """
+ lookup_name = 'strictly_above'
+gis_lookups['strictly_above'] = StrictlyAboveLookup
+
+
+class SameAsLookup(GISLookup):
+ """
+ The "~=" operator is the "same as" operator. It tests actual geometric
+ equality of two features. So if A and B are the same feature,
+ vertex-by-vertex, the operator returns true.
+ """
+ lookup_name = 'same_as'
+gis_lookups['same_as'] = SameAsLookup
+
+
+class ExactLookup(SameAsLookup):
+ # Alias of same_as
+ lookup_name = 'exact'
+gis_lookups['exact'] = ExactLookup
+
+
+class BBContainsLookup(GISLookup):
+ """
+ The 'bbcontains' operator returns true if A's bounding box completely contains
+ by B's bounding box.
+ """
+ lookup_name = 'bbcontains'
+gis_lookups['bbcontains'] = BBContainsLookup
+
+
+class BBOverlapsLookup(GISLookup):
+ """
+ The 'bboverlaps' operator returns true if A's bounding box overlaps B's bounding box.
+ """
+ lookup_name = 'bboverlaps'
+gis_lookups['bboverlaps'] = BBOverlapsLookup
+
+
+class ContainedLookup(GISLookup):
+ """
+ The 'contained' operator returns true if A's bounding box is completely contained
+ by B's bounding box.
+ """
+ lookup_name = 'contained'
+gis_lookups['contained'] = ContainedLookup
+
+
+# ------------------
+# Geometry functions
+# ------------------
+
+class ContainsLookup(GISLookup):
+ lookup_name = 'contains'
+gis_lookups['contains'] = ContainsLookup
+
+
+class ContainsProperlyLookup(GISLookup):
+ lookup_name = 'contains_properly'
+gis_lookups['contains_properly'] = ContainsProperlyLookup
+
+
+class CoveredByLookup(GISLookup):
+ lookup_name = 'coveredby'
+gis_lookups['coveredby'] = CoveredByLookup
+
+
+class CoversLookup(GISLookup):
+ lookup_name = 'covers'
+gis_lookups['covers'] = CoversLookup
+
+
+class CrossesLookup(GISLookup):
+ lookup_name = 'crosses'
+gis_lookups['crosses'] = CrossesLookup
+
+
+class DisjointLookup(GISLookup):
+ lookup_name = 'disjoint'
+gis_lookups['disjoint'] = DisjointLookup
+
+
+class EqualsLookup(GISLookup):
+ lookup_name = 'equals'
+gis_lookups['equals'] = EqualsLookup
+
+
+class IntersectsLookup(GISLookup):
+ lookup_name = 'intersects'
+gis_lookups['intersects'] = IntersectsLookup
+
+
+class OverlapsLookup(GISLookup):
+ lookup_name = 'overlaps'
+gis_lookups['overlaps'] = OverlapsLookup
+
+
+class RelateLookup(GISLookup):
+ lookup_name = 'relate'
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s)'
+ pattern_regex = re.compile(r'^[012TF\*]{9}$')
+
+ def get_db_prep_lookup(self, value, connection):
+ if len(value) != 2:
+ raise ValueError('relate must be passed a two-tuple')
+ # Check the pattern argument
+ backend_op = connection.ops.gis_operators[self.lookup_name]
+ if hasattr(backend_op, 'check_relate_argument'):
+ backend_op.check_relate_argument(value[1])
+ else:
+ pattern = value[1]
+ if not isinstance(pattern, six.string_types) or not self.pattern_regex.match(pattern):
+ raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
+ return super(RelateLookup, self).get_db_prep_lookup(value, connection)
+gis_lookups['relate'] = RelateLookup
+
+
+class TouchesLookup(GISLookup):
+ lookup_name = 'touches'
+gis_lookups['touches'] = TouchesLookup
+
+
+class WithinLookup(GISLookup):
+ lookup_name = 'within'
+gis_lookups['within'] = WithinLookup
+
+
+class DistanceLookupBase(GISLookup):
+ distance = True
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %%s'
+
+ def get_db_prep_lookup(self, value, connection):
+ if isinstance(value, (tuple, list)):
+ if not 2 <= len(value) <= 3:
+ raise ValueError("2 or 3-element tuple required for '%s' lookup." % self.lookup_name)
+ params = [connection.ops.Adapter(value[0])]
+ # Getting the distance parameter in the units of the field.
+ params += connection.ops.get_distance(self.lhs.output_field, value[1:], self.lookup_name)
+ return ('%s', params)
+ else:
+ return super(DistanceLookupBase, self).get_db_prep_lookup(value, connection)
+
+
+class DWithinLookup(DistanceLookupBase):
+ lookup_name = 'dwithin'
+ sql_template = '%(func)s(%(lhs)s, %(rhs)s, %%s)'
+gis_lookups['dwithin'] = DWithinLookup
+
+
+class DistanceGTLookup(DistanceLookupBase):
+ lookup_name = 'distance_gt'
+gis_lookups['distance_gt'] = DistanceGTLookup
+
+
+class DistanceGTELookup(DistanceLookupBase):
+ lookup_name = 'distance_gte'
+gis_lookups['distance_gte'] = DistanceGTELookup
+
+
+class DistanceLTLookup(DistanceLookupBase):
+ lookup_name = 'distance_lt'
+gis_lookups['distance_lt'] = DistanceLTLookup
+
+
+class DistanceLTELookup(DistanceLookupBase):
+ lookup_name = 'distance_lte'
+gis_lookups['distance_lte'] = DistanceLTELookup
diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py
index e82f17b8a3..d5a7f819c8 100644
--- a/django/contrib/gis/db/models/sql/query.py
+++ b/django/contrib/gis/db/models/sql/query.py
@@ -1,7 +1,7 @@
from django.db import connections
from django.db.models.query import sql
+from django.db.models.sql.constants import QUERY_TERMS
-from django.contrib.gis.db.models.constants import ALL_TERMS
from django.contrib.gis.db.models.fields import GeometryField
from django.contrib.gis.db.models.lookups import GISLookup
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates
@@ -13,7 +13,7 @@ class GeoQuery(sql.Query):
A single spatial SQL query.
"""
# Overridding the valid query terms.
- query_terms = ALL_TERMS
+ query_terms = QUERY_TERMS | set(GeometryField.class_lookups.keys())
aggregates_module = gis_aggregates
compiler = 'GeoSQLCompiler'
diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index 631b0bd2f0..db117e917e 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -612,6 +612,9 @@ Miscellaneous
:func:`~django.core.urlresolvers.reverse_lazy` now return Unicode strings
instead of byte strings.
+* GIS-specific lookups have been refactored to use the
+ :class:`django.db.models.Lookup` API.
+
.. _deprecated-features-1.8:
Features deprecated in 1.8