From b9a085467d044330109dd272f7d0de01ba04e6d1 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 8 Jun 2026 15:40:47 +0200 Subject: [PATCH] feat(cql2-json): support CQL2 spec function format in parser and encoder Add support for the standard CQL2-JSON function call format where functions are expressed as {"op": "func_name", "args": [...]} at the top level, in addition to the existing {"function": {"name": ..., "arguments": [...]}} backward-compatible format. Changes: - parser.py: add else branch after BINARY_OP_PREDICATES_MAP check to treat unknown ops as Function nodes instead of raising ValueError - evaluate.py: simplify Function handler to always emit {"op": name, "args": [...]} (spec-compliant format), removing non-standard lower/upper/function variants - test_parser.py: add three new tests covering spec-format parsing, old-format backward compatibility, and spec-format encoding --- pygeofilter/backends/cql2_json/evaluate.py | 9 +-------- pygeofilter/parsers/cql2_json/parser.py | 6 ++++++ tests/parsers/cql2_json/test_parser.py | 23 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pygeofilter/backends/cql2_json/evaluate.py b/pygeofilter/backends/cql2_json/evaluate.py index 20b0751a..3ca1161b 100644 --- a/pygeofilter/backends/cql2_json/evaluate.py +++ b/pygeofilter/backends/cql2_json/evaluate.py @@ -82,14 +82,7 @@ def isnull(self, node, arg): @handle(ast.Function) def function(self, node, *args): - name = node.name.lower() - if name == "lower": - ret = {"lower": args[0]} - elif name == "upper": - ret = {"upper": args[0]} - else: - ret = {"function": name, "args": [*args]} - return ret + return {"op": node.name, "args": [*args]} @handle(ast.In) def in_(self, node, lhs, *options): diff --git a/pygeofilter/parsers/cql2_json/parser.py b/pygeofilter/parsers/cql2_json/parser.py index e3c2e9a7..53f9bf47 100644 --- a/pygeofilter/parsers/cql2_json/parser.py +++ b/pygeofilter/parsers/cql2_json/parser.py @@ -165,6 +165,12 @@ def walk_cql_json(node: JsonType): # noqa: C901 args = [cast(ast.Node, walk_cql_json(arg)) for arg in args] return BINARY_OP_PREDICATES_MAP[op](*args) + else: + return ast.Function( + op, + [walk_cql_json(arg) for arg in args], + ) + raise ValueError(f"Unable to parse expression node {node!r}") diff --git a/tests/parsers/cql2_json/test_parser.py b/tests/parsers/cql2_json/test_parser.py index 01f2474a..f94a1b14 100644 --- a/tests/parsers/cql2_json/test_parser.py +++ b/tests/parsers/cql2_json/test_parser.py @@ -32,6 +32,7 @@ from pygeoif import geometry from pygeofilter import ast, values +from pygeofilter.backends.cql2_json.evaluate import CQL2Evaluator from pygeofilter.parsers.cql2_json import parse @@ -717,3 +718,25 @@ def test_function_attr_string_arg(): ], ), ) + + +def test_function_spec_format(): + """Parse spec-format function: {"op": "my_func", "args": [...]}""" + result = parse({"op": "my_func", "args": [{"property": "attr"}, 42]}) + assert result == ast.Function("my_func", [ast.Attribute("attr"), 42]) + + +def test_function_old_format_still_works(): + """Parse old format: {"function": {"name": "...", "arguments": [...]}}""" + result = parse( + {"function": {"name": "my_func", "arguments": [{"property": "attr"}]}} + ) + assert result == ast.Function("my_func", [ast.Attribute("attr")]) + + +def test_function_encode_spec_format(): + """Encode Function node to spec format: {"op": ..., "args": [...]}""" + evaluator = CQL2Evaluator(None, None) + node = ast.Function("my_func", [ast.Attribute("attr")]) + result = evaluator.evaluate(node) + assert result == {"op": "my_func", "args": [{"property": "attr"}]}