Repository: geopython/pygeofilter (language: python)

Issue:
cql-text
I see that there is a branch with a base implementation for cql-text.

When I try to use it, I get import errors.

What is the road map for this branch? Do you have a timeline? Is it actively being developed?



Discussion / hints:
@rsmith013 

Yes, the CQL Text branch is not yet production ready and needs additional care to be operational. Also, many changes in the OGC API Features spec will also affect CQL Text, so waiting until it has stabilized might be the bes course in order to avoid too many changes.
we would also like to support cql-text in our project.

so I also tried to use the pygeofilter branch, and alo ran into various issues. such as: certain lark syntax not accepted by lark (don't know with which lark version it was made?), no support for qualified identifiers (such as: 'eo:cover') and finally some lark errors I could not get around..

what we've done for now is extend the pygeofilter ecql parser to support qualified identifiers and only advertise support for 'basic CQL', which seems the same between ecql and cql-text..

not really sure what the further differences are between ecql and cql-text?

note that the stac filter fragment documentation currently states:

"This extension references behavior defined in the OGC API - Features - Part 3: Filtering and the Common Query Language (CQL2) specification. As of August 2021, this specification is in draft status. Several behaviors have changed since the last published draft, so this spec references the latest revision in the spec's GitHub repo. There are no major anticipated changes, so implementers are encouraged to move ahead with implementation, and to simply be aware that minor changes may need to be made in the future."

I'd be more than happy to test after any changes you might make.
i runned in to the same issue as described by @srepmub

If i use constraint with dublin core notation `dc:title LIKE '%ips%' AND abstract LIKE '%pharetra%'` the parser will run into the following exception.

```exception
lark.exceptions.UnexpectedCharacters: No terminal matches ':' in the current parser context, at line 1 col 3

dc:title LIKE '%ips%' AND abstract LIKE '%
  ^
Expected one of: 
        * MORETHAN
        * AFTER
        * STAR
        * LESSTHAN
        * AND
        * NOT
        * ILIKE
        * IN
        * "<="
        * "<>"
        * EXISTS
        * OR
        * IS
        * DURING
        * SLASH
        * BETWEEN
        * ">="
        * PLUS
        * LPAR
        * RPAR
        * LIKE
        * EQUAL
        * MINUS
        * BEFORE
        * "DOES-NOT-EXIST"
        * COMMA

Previous tokens: Token('NAME', 'dc')

```
This is caused by the [attribute](https://github.com/geopython/pygeofilter/blob/9a2d0ea43ad15032cbe92208612cb5a5ffb990a9/pygeofilter/parsers/ecql/grammar.lark#L91) definition inside the grammer. There is no definition for `NAME ":" NAME`

If i change the attribute definition to 
```lark
attribute: NAME
         | DOUBLE_QUOTED
         | NAME ":" NAME
```

it produces the following exception:

```exception
STATE STACK DUMP
----------------
0) {<wkt__multipoint :  * MULTIPOINT LPAR LPAR wkt__coordinate RPAR RPAR>, <wkt__multipoint :  * MULTIPOINT LPAR wkt__coordinate_list RPAR>, <_binary_spatial_predicate_func :  * TOUCHES>, <temporal_predicate :  * expression AFTER DATETIME>, <ewkt_geometry :  * wkt__geometry>, <wkt__geometry :  * wkt__multipolygon>, <atom :  * NAME LPAR RPAR>, <product :  * product SLASH atom>, <predicate :  * expression ILIKE SINGLE_QUOTED>, <atom :  * NAME LPAR expression __predicate_star_4 RPAR>, <predicate :  * expression LIKE SINGLE_QUOTED>, <wkt__geometry :  * wkt__multilinestring>, <condition :  * condition AND condition_1>, <envelope :  * ENVELOPE LPAR number number number number RPAR>, <_binary_spatial_predicate_func :  * EQUALS>, <expression :  * sum>, <condition_1 :  * LPAR condition RPAR>, <predicate :  * expression NOT LIKE SINGLE_QUOTED>, <_distance_spatial_predicate_func :  * DWITHIN>, <condition :  * condition OR condition_1>, <wkt__geometry :  * wkt__multipoint>, <wkt__multipolygon :  * MULTIPOLYGON LPAR LPAR wkt__coordinate_lists RPAR RPAR>, <predicate :  * expression NOT BETWEEN expression AND expression>, <atom :  * NAME LPAR expression RPAR>, <wkt__multipolygon :  * MULTIPOLYGON LPAR LPAR wkt__coordinate_lists RPAR __wkt__multipolygon_star_2 RPAR>, <literal :  * number>, <spatial_predicate :  * RELATE LPAR expression COMMA expression COMMA SINGLE_QUOTED RPAR>, <atom :  * LPAR expression RPAR>, <spatial_predicate :  * _binary_spatial_predicate_func LPAR expression COMMA expression RPAR>, <condition_1 :  * predicate>, <literal :  * envelope>, <predicate :  * expression IS NULL>, <sum :  * sum MINUS product>, <predicate :  * expression IN LPAR expression __predicate_star_4 RPAR>, <attribute :  * NAME>, <_binary_spatial_predicate_func :  * CROSSES>, <predicate :  * attribute __ANON_3>, <temporal_predicate :  * expression BEFORE DATETIME>, <wkt__geometry :  * wkt__point>, <_binary_spatial_predicate_func :  * OVERLAPS>, <wkt__point :  * POINT LPAR wkt__coordinate RPAR>, <wkt__polygon :  * POLYGON LPAR wkt__coordinate_lists RPAR>, <product :  * product STAR atom>, <wkt__multilinestring :  * MULTILINESTRING LPAR wkt__coordinate_lists RPAR>, <temporal_predicate :  * expression DURING OR AFTER period>, <condition_1 :  * NOT predicate>, <number :  * INT>, <attribute :  * DOUBLE_QUOTED>, <predicate :  * temporal_predicate>, <predicate :  * INCLUDE>, <temporal_predicate :  * expression BEFORE OR DURING period>, <predicate :  * EXCLUDE>, <predicate :  * spatial_predicate>, <spatial_predicate :  * BBOX LPAR expression COMMA full_number COMMA full_number COMMA full_number COMMA full_number COMMA SINGLE_QUOTED RPAR>, <start :  * condition>, <predicate :  * expression NOT IN LPAR expression RPAR>, <attribute :  * NAME COLON NAME>, <literal :  * BOOLEAN>, <literal :  * SINGLE_QUOTED>, <atom :  * attribute>, <predicate :  * expression LESSTHAN expression>, <predicate :  * expression EQUAL expression>, <temporal_predicate :  * expression DURING period>, <predicate :  * expression __ANON_1 expression>, <wkt__geometry :  * wkt__polygon>, <_distance_spatial_predicate_func :  * BEYOND>, <$root_start :  * start>, <wkt__linestring :  * LINESTRING LPAR wkt__coordinate_list RPAR>, <predicate :  * expression IN LPAR expression RPAR>, <spatial_predicate :  * BBOX LPAR expression COMMA full_number COMMA full_number COMMA full_number COMMA full_number RPAR>, <wkt__geometry :  * wkt__geometrycollection>, <spatial_predicate :  * _distance_spatial_predicate_func LPAR expression COMMA expression COMMA number COMMA distance_units RPAR>, <wkt__geometry :  * wkt__linestring>, <atom :  * literal>, <ewkt_geometry :  * SRID EQUAL wkt__INT SEMICOLON wkt__geometry>, <wkt__multipoint :  * MULTIPOINT LPAR LPAR wkt__coordinate RPAR __wkt__multipoint_star_1 RPAR>, <condition :  * condition_1>, <wkt__geometrycollection :  * GEOMETRYCOLLECTION LPAR wkt__geometry RPAR>, <predicate :  * expression MORETHAN expression>, <literal :  * ewkt_geometry>, <atom :  * MINUS atom>, <_binary_spatial_predicate_func :  * INTERSECTS>, <wkt__geometrycollection :  * GEOMETRYCOLLECTION LPAR wkt__geometry __wkt__geometrycollection_star_0 RPAR>, <predicate :  * expression NOT ILIKE SINGLE_QUOTED>, <predicate :  * expression IS NOT NULL>, <sum :  * sum PLUS product>, <sum :  * product>, <predicate :  * expression __ANON_0 expression>, <predicate :  * expression BETWEEN expression AND expression>, <_binary_spatial_predicate_func :  * DISJOINT>, <predicate :  * attribute EXISTS>, <product :  * atom>, <predicate :  * expression __ANON_2 expression>, <number :  * FLOAT>, <_binary_spatial_predicate_func :  * WITHIN>, <predicate :  * expression NOT IN LPAR expression __predicate_star_4 RPAR>, <_binary_spatial_predicate_func :  * CONTAINS>}

E.....................................................
======================================================================
ERROR: test_ogc_request_with_get_records_request (tests.models.tests_ogc_request.OGCRequestTest.test_ogc_request_with_get_records_request)
Test that OGCRequest helper class works correctly for a given GetMap get request
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/jonas/git/django-ows-lib/tests/models/tests_ogc_request.py", line 97, in test_ogc_request_with_get_records_request
    f = ogc_request.filter_constraint()
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/ows_lib/models/ogc_request.py", line 97, in filter_constraint
    ast = parse_ecql(constraint)
          ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/pygeofilter/parsers/ecql/parser.py", line 210, in parse
    return parser.parse(cql_text)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/lark.py", line 581, in parse
    return self.parser.parse(text, start=start, on_error=on_error)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/parser_frontends.py", line 106, in parse
    return self.parser.parse(stream, chosen_start, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/parsers/lalr_parser.py", line 41, in parse
    return self.parser.parse(lexer, start)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/parsers/lalr_parser.py", line 171, in parse
    return self.parse_from_state(parser_state)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/parsers/lalr_parser.py", line 179, in parse_from_state
    state.feed_token(token)
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/parsers/lalr_parser.py", line 150, in feed_token
    value = callbacks[rule](s)
            ^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/parse_tree_builder.py", line 148, in __call__
    return self.node_builder(filtered)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/parse_tree_builder.py", line 326, in f
    return wrapper(func, name, children, None)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/visitors.py", line 443, in _vargs_inline
    return f(*children)
           ^^^^^^^^^^^^
  File "/home/jonas/git/django-ows-lib/.venv/lib/python3.11/site-packages/lark/visitors.py", line 427, in f
    return _f(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: ECQLTransformer.attribute() takes 2 positional arguments but 3 were given

```


Fix patch:
diff --git a/pygeofilter/parsers/ecql/grammar.lark b/pygeofilter/parsers/ecql/grammar.lark
index 0a9e7b9..16bc156 100644
--- a/pygeofilter/parsers/ecql/grammar.lark
+++ b/pygeofilter/parsers/ecql/grammar.lark
@@ -90,6 +90,7 @@
 
 attribute: NAME
          | DOUBLE_QUOTED
+         | QUALIFIED_NAME
 
 ?literal: number
         | BOOLEAN
@@ -113,6 +114,8 @@ BOOLEAN: ( "TRUE" | "FALSE" )
 DOUBLE_QUOTED: "\"" /.*?/ "\""
 SINGLE_QUOTED: "'" /.*?/ "'"
 
+QUALIFIED_NAME: NAME ":" NAME
+
 %import .wkt.ewkt_geometry
 %import .iso8601.DATETIME
 %import .iso8601.DURATION


Test patch:
diff --git a/tests/parsers/ecql/test_parser.py b/tests/parsers/ecql/test_parser.py
index 59b41a4..c6dbbc6 100644
--- a/tests/parsers/ecql/test_parser.py
+++ b/tests/parsers/ecql/test_parser.py
@@ -34,6 +34,14 @@
 from pygeofilter.parsers.ecql import parse
 
 
+def test_namespace_attribute_eq_literal():
+    result = parse("ns:attr = 'A'")
+    assert result == ast.Equal(
+        ast.Attribute("ns:attr"),
+        "A",
+    )
+
+
 def test_attribute_eq_literal():
     result = parse("attr = 'A'")
     assert result == ast.Equal(