2012-09-09 07:51:36 +08:00
|
|
|
from django.db.models.constants import LOOKUP_SEP
|
2011-07-13 17:35:51 +08:00
|
|
|
from django.db.models.fields import FieldDoesNotExist
|
2009-03-04 06:10:15 +08:00
|
|
|
from django.db.models.sql.expressions import SQLEvaluator
|
2009-12-22 23:18:51 +08:00
|
|
|
from django.db.models.sql.where import Constraint, WhereNode
|
2009-03-04 06:10:15 +08:00
|
|
|
from django.contrib.gis.db.models.fields import GeometryField
|
2008-08-06 02:13:06 +08:00
|
|
|
|
2013-11-03 01:18:46 +08:00
|
|
|
|
2009-12-22 23:18:51 +08:00
|
|
|
class GeoConstraint(Constraint):
|
2008-08-06 02:13:06 +08:00
|
|
|
"""
|
2009-12-22 23:18:51 +08:00
|
|
|
This subclass overrides `process` to better handle geographic SQL
|
|
|
|
construction.
|
2008-08-06 02:13:06 +08:00
|
|
|
"""
|
2009-12-22 23:18:51 +08:00
|
|
|
def __init__(self, init_constraint):
|
|
|
|
self.alias = init_constraint.alias
|
|
|
|
self.col = init_constraint.col
|
|
|
|
self.field = init_constraint.field
|
|
|
|
|
|
|
|
def process(self, lookup_type, value, connection):
|
|
|
|
if isinstance(value, SQLEvaluator):
|
|
|
|
# Make sure the F Expression destination field exists, and
|
|
|
|
# set an `srid` attribute with the same as that of the
|
|
|
|
# destination.
|
|
|
|
geo_fld = GeoWhereNode._check_geo_field(value.opts, value.expression.name)
|
|
|
|
if not geo_fld:
|
|
|
|
raise ValueError('No geographic field found in expression.')
|
|
|
|
value.srid = geo_fld.srid
|
|
|
|
db_type = self.field.db_type(connection=connection)
|
|
|
|
params = self.field.get_db_prep_lookup(lookup_type, value, connection=connection)
|
|
|
|
return (self.alias, self.col, db_type), params
|
2008-08-06 02:13:06 +08:00
|
|
|
|
2013-11-03 01:18:46 +08:00
|
|
|
|
2008-08-06 02:13:06 +08:00
|
|
|
class GeoWhereNode(WhereNode):
|
|
|
|
"""
|
|
|
|
Used to represent the SQL where-clause for spatial databases --
|
|
|
|
these are tied to the GeoQuery class that created it.
|
|
|
|
"""
|
Refactored qs.add_q() and utils/tree.py
The sql/query.py add_q method did a lot of where/having tree hacking to
get complex queries to work correctly. The logic was refactored so that
it should be simpler to understand. The new logic should also produce
leaner WHERE conditions.
The changes cascade somewhat, as some other parts of Django (like
add_filter() and WhereNode) expect boolean trees in certain format or
they fail to work. So to fix the add_q() one must fix utils/tree.py,
some things in add_filter(), WhereNode and so on.
This commit also fixed add_filter to see negate clauses up the path.
A query like .exclude(Q(reversefk__in=a_list)) didn't work similarly to
.filter(~Q(reversefk__in=a_list)). The reason for this is that only
the immediate parent negate clauses were seen by add_filter, and thus a
tree like AND: (NOT AND: (AND: condition)) will not be handled
correctly, as there is one intermediary AND node in the tree. The
example tree is generated by .exclude(~Q(reversefk__in=a_list)).
Still, aggregation lost connectors in OR cases, and F() objects and
aggregates in same filter clause caused GROUP BY problems on some
databases.
Fixed #17600, fixed #13198, fixed #17025, fixed #17000, fixed #11293.
2012-05-25 05:27:24 +08:00
|
|
|
|
|
|
|
def _prepare_data(self, data):
|
2009-12-22 23:18:51 +08:00
|
|
|
if isinstance(data, (list, tuple)):
|
|
|
|
obj, lookup_type, value = data
|
2013-10-15 03:13:14 +08:00
|
|
|
if (isinstance(obj, Constraint) and
|
2013-11-26 17:43:46 +08:00
|
|
|
isinstance(obj.field, GeometryField)):
|
2009-12-22 23:18:51 +08:00
|
|
|
data = (GeoConstraint(obj), lookup_type, value)
|
Refactored qs.add_q() and utils/tree.py
The sql/query.py add_q method did a lot of where/having tree hacking to
get complex queries to work correctly. The logic was refactored so that
it should be simpler to understand. The new logic should also produce
leaner WHERE conditions.
The changes cascade somewhat, as some other parts of Django (like
add_filter() and WhereNode) expect boolean trees in certain format or
they fail to work. So to fix the add_q() one must fix utils/tree.py,
some things in add_filter(), WhereNode and so on.
This commit also fixed add_filter to see negate clauses up the path.
A query like .exclude(Q(reversefk__in=a_list)) didn't work similarly to
.filter(~Q(reversefk__in=a_list)). The reason for this is that only
the immediate parent negate clauses were seen by add_filter, and thus a
tree like AND: (NOT AND: (AND: condition)) will not be handled
correctly, as there is one intermediary AND node in the tree. The
example tree is generated by .exclude(~Q(reversefk__in=a_list)).
Still, aggregation lost connectors in OR cases, and F() objects and
aggregates in same filter clause caused GROUP BY problems on some
databases.
Fixed #17600, fixed #13198, fixed #17025, fixed #17000, fixed #11293.
2012-05-25 05:27:24 +08:00
|
|
|
return super(GeoWhereNode, self)._prepare_data(data)
|
2009-12-22 23:18:51 +08:00
|
|
|
|
|
|
|
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)
|
2013-02-10 23:15:49 +08:00
|
|
|
spatial_sql, spatial_params = connection.ops.spatial_lookup_sql(
|
2013-10-20 07:33:10 +08:00
|
|
|
data, lookup_type, params_or_value, lvalue.field, qn)
|
2013-02-10 23:15:49 +08:00
|
|
|
return spatial_sql, spatial_params + params
|
2008-08-06 02:13:06 +08:00
|
|
|
else:
|
2009-12-22 23:18:51 +08:00
|
|
|
return super(GeoWhereNode, self).make_atom(child, qn, connection)
|
2009-03-04 06:10:15 +08:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _check_geo_field(cls, opts, lookup):
|
|
|
|
"""
|
2009-12-22 23:18:51 +08:00
|
|
|
Utility for checking the given lookup with the given model options.
|
2009-03-04 06:10:15 +08:00
|
|
|
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'.
|
|
|
|
|
|
|
|
If a GeometryField exists according to the given lookup on the model
|
|
|
|
options, it will be returned. Otherwise returns None.
|
|
|
|
"""
|
|
|
|
# This takes into account the situation where the lookup is a
|
|
|
|
# lookup to a related geographic field, e.g., 'address__point'.
|
|
|
|
field_list = lookup.split(LOOKUP_SEP)
|
|
|
|
|
|
|
|
# Reversing so list operates like a queue of related lookups,
|
|
|
|
# and popping the top lookup.
|
|
|
|
field_list.reverse()
|
|
|
|
fld_name = field_list.pop()
|
|
|
|
|
|
|
|
try:
|
|
|
|
geo_fld = opts.get_field(fld_name)
|
|
|
|
# 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
|
2009-12-22 23:18:51 +08:00
|
|
|
# model field associated with the next field in the list
|
2009-03-04 06:10:15 +08:00
|
|
|
# until there's no more left.
|
|
|
|
while len(field_list):
|
|
|
|
opts = geo_fld.rel.to._meta
|
|
|
|
geo_fld = opts.get_field(field_list.pop())
|
|
|
|
except (FieldDoesNotExist, AttributeError):
|
|
|
|
return False
|
|
|
|
|
|
|
|
# Finally, make sure we got a Geographic field and return.
|
|
|
|
if isinstance(geo_fld, GeometryField):
|
|
|
|
return geo_fld
|
|
|
|
else:
|
|
|
|
return False
|