Skip to content

Commit 9a4db09

Browse files
Ariana BarzinpourMagnar Eivind Martinsen
authored andcommitted
add optional undefined_as_null param in to_filter
1 parent d51dbb0 commit 9a4db09

3 files changed

Lines changed: 39 additions & 16 deletions

File tree

pygeofilter/backends/sqlalchemy/evaluate.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99

1010
class SQLAlchemyFilterEvaluator(Evaluator):
11-
def __init__(self, field_mapping):
11+
def __init__(self, field_mapping, undefined_as_null):
1212
self.field_mapping = field_mapping
13+
self.undefined_as_null = undefined_as_null
1314

1415
@handle(ast.Not)
1516
def not_(self, node, sub):
@@ -105,7 +106,7 @@ def bbox(self, node, lhs):
105106

106107
@handle(ast.Attribute)
107108
def attribute(self, node):
108-
return filters.attribute(node.name, self.field_mapping)
109+
return filters.attribute(node.name, self.field_mapping, self.undefined_as_null)
109110

110111
@handle(ast.Arithmetic, subclasses=True)
111112
def arithmetic(self, node, lhs, rhs):
@@ -133,15 +134,13 @@ def envelope(self, node):
133134
return filters.parse_bbox([node.x1, node.y1, node.x2, node.y2])
134135

135136

136-
def to_filter(ast, field_mapping=None):
137-
"""Helper function to translate ECQL AST to Django Query expressions.
137+
def to_filter(ast, field_mapping={}, undefined_as_null=None):
138+
"""Helper function to translate ECQL AST to SQLAlchemy Query expressions.
138139
139140
:param ast: the abstract syntax tree
140-
:param field_mapping: a dict mapping from the filter name to the Django
141+
:param field_mapping: a dict mapping from the filter name to the SQLAlchemy
141142
field lookup.
142-
:param mapping_choices: a dict mapping field lookups to choices.
143143
:type ast: :class:`Node`
144-
:returns: a Django query object
145-
:rtype: :class:`django.db.models.Q`
144+
:returns: a SQLAlchemy query object
146145
"""
147-
return SQLAlchemyFilterEvaluator(field_mapping).evaluate(ast)
146+
return SQLAlchemyFilterEvaluator(field_mapping, undefined_as_null).evaluate(ast)

pygeofilter/backends/sqlalchemy/filters.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Callable, Dict, Optional
55

66
from pygeoif import shape
7-
from sqlalchemy import and_, func, not_, or_
7+
from sqlalchemy import and_, func, not_, or_, null
88

99

1010
def parse_bbox(box, srid: Optional[int] = None):
@@ -257,15 +257,21 @@ def bbox(lhs, minx, miny, maxx, maxy, crs=4326):
257257
return lhs.ST_Intersects(parse_bbox([minx, miny, maxx, maxy], crs))
258258

259259

260-
def attribute(name, field_mapping=None):
260+
def attribute(name, field_mapping={}, undefined_as_null: bool = None):
261261
"""Create an attribute lookup expression using a field mapping dictionary.
262262
263263
:param name: the field filter name
264264
:param field_mapping: the dictionary to use as a lookup.
265+
:param undefined_as_null: how to handle a name not present in field_mapping
266+
(None (default) - leave as-is; True - treat as null; False - throw error)
265267
"""
266-
field = field_mapping.get(name, name)
267-
268-
return field
268+
if undefined_as_null is None:
269+
return field_mapping.get(name, name)
270+
if undefined_as_null:
271+
# return null object if name is not found in field_mapping
272+
return field_mapping.get(name, null())
273+
# undefined_as_null is False, so raise KeyError if name not found
274+
return field_mapping[name]
269275

270276

271277
def literal(value):

tests/backends/sqlalchemy/test_evaluate.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ def db_session(setup_database, connection):
152152
transaction.rollback()
153153

154154

155-
def evaluate(session, cql_expr, expected_ids):
155+
def evaluate(session, cql_expr, expected_ids, filter_option=None):
156156
ast = parse(cql_expr)
157-
filters = to_filter(ast, FIELD_MAPPING)
157+
filters = to_filter(ast, FIELD_MAPPING, filter_option)
158158

159159
q = session.query(Record).join(RecordMeta).filter(filters)
160160
results = [row.identifier for row in q]
@@ -415,3 +415,21 @@ def test_arith_field_plus_mul_1(db_session):
415415

416416
def test_arith_field_plus_mul_2(db_session):
417417
evaluate(db_session, "intMetaAttribute = 5 + intAttribute * 1.5", ("A",))
418+
419+
420+
# handling undefined/invalid attributes
421+
422+
423+
def test_undef_comp(db_session):
424+
# treat undefined/invalid attribute as null
425+
evaluate(db_session, "missingAttribute > 10", (), True)
426+
427+
428+
def test_undef_isnull(db_session):
429+
evaluate(db_session, "missingAttribute IS NULL", ("A", "B"), True)
430+
431+
432+
def test_undef_comp_error(db_session):
433+
# error if undefined/invalid attribute
434+
with pytest.raises(KeyError):
435+
evaluate(db_session, "missingAttribute > 10", (), False)

0 commit comments

Comments
 (0)