Skip to content

Commit b579ca4

Browse files
committed
normalize CQL text support to CQL2 (#2015)
1 parent 2433cea commit b579ca4

9 files changed

Lines changed: 41 additions & 100 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ jobs:
130130
pip3 install -r requirements-pubsub.txt
131131
pip3 install .
132132
pip3 install GDAL==`gdal-config --version`
133+
pip3 install --force-reinstall https://github.com/geopython/pygeofilter/archive/main.zip
133134
- name: setup test data ⚙️
134135
run: |
135136
pybabel compile -d locale -l es
Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
.. _cql:
1+
.. _cql2:
22

3-
CQL support
4-
===========
3+
CQL2 support
4+
============
55

6-
OGC Common Query Language (`CQL2`_) is a generic language designed to provide enhanced query and subset/filtering to (primarily) feature and record data.
6+
`OGC Common Query Language`_ (CQL2) is a generic language designed to provide enhanced query and subset/filtering to (primarily) feature and record data.
77

88
Providers
99
---------
@@ -14,7 +14,7 @@ for current provider support.
1414
Limitations
1515
-----------
1616

17-
Support of CQL is limited to `Basic CQL2 <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-core>`_ and thus it allows to query with the
17+
Support is limited to `Basic CQL2 <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-core>`_ and thus it allows to query with the
1818
following predicates:
1919

2020
* comparison predicates
@@ -24,9 +24,9 @@ following predicates:
2424
Formats
2525
-------
2626

27-
Supported providers leverage the CQL2 dialect with the JSON encoding `CQL-JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_.
27+
Supported providers leverage the CQL2 dialect with the JSON encoding `CQL JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_.
2828

29-
PostgreSQL supports both `CQL2 JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_ and `CQL text <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-text>`_ dialects.
29+
PostgreSQL supports both `CQL JSON <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-json>`_ and `CQL Text <https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-text>`_ dialects.
3030

3131
Queries
3232
^^^^^^^
@@ -83,7 +83,7 @@ Or
8383
]
8484
}'
8585
86-
The same ``BETWEEN`` query using HTTP GET request formatted as CQL text and URL encoded as below:
86+
The same ``BETWEEN`` query using HTTP GET request formatted as CQL2 text and URL encoded as below:
8787

8888
.. code-block:: bash
8989
@@ -103,25 +103,10 @@ An ``EQUALS`` example for a specific property:
103103
]
104104
}'
105105
106-
A ``CROSSES`` example via an HTTP GET request. The CQL text is passed via the ``filter`` parameter.
106+
A ``S_CROSSES`` example via an HTTP GET request. The CQL2 text is passed via the ``filter`` parameter.
107107

108108
.. code-block:: bash
109109
110-
curl "http://localhost:5000/collections/hot_osm_waterways/items?f=json&filter=CROSSES(foo_geom,%20LINESTRING(28%20-2,%2030%20-4))"
110+
curl "http://localhost:5000/collections/hot_osm_waterways/items?f=json&filter=S_CROSSES(foo_geom,%20LINESTRING(28%20-2,%2030%20-4))"
111111
112-
A ``DWITHIN`` example via HTTP GET and using a custom CRS for the filter geometry:
113-
114-
.. code-block:: bash
115-
116-
curl "http://localhost:5000/collections/beni/items?filter=DWITHIN(geometry,POINT(1392921%205145517),100,meters)&filter-crs=http://www.opengis.net/def/crs/EPSG/0/3857"
117-
118-
119-
The same example, but this time providing a geometry in EWKT format:
120-
121-
.. code-block:: bash
122-
123-
curl "http://localhost:5000/collections/beni/items?filter=DWITHIN(geometry,SRID=3857;POINT(1392921%205145517),100,meters)"
124-
125-
Note that the CQL text has been URL encoded. This is required in curl commands but when entering in a browser, plain text can be used e.g. ``CROSSES(foo_geom, LINESTRING(28 -2, 30 -4))``.
126-
127-
.. _`CQL2`: https://docs.ogc.org/is/21-065r2/21-065r2.html
112+
.. _`OGC Common Query Language`: https://docs.ogc.org/is/21-065r2/21-065r2.html

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ reference documentation on all aspects of the project.
4545
plugins
4646
html-templating
4747
crs
48-
cql
48+
cql2
4949
language
5050
development
5151
ogc-compliance

docs/source/publishing/ogcapi-features.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ To publish an Elasticsearch index, the following are required in your index:
115115
The ES provider also has the support for the CQL queries as indicated in the table above.
116116

117117
.. seealso::
118-
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
118+
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
119119

120120
.. _ERDDAP Tabledap Service:
121121

@@ -292,7 +292,7 @@ These are optional and if not specified, the default from the engine will be use
292292
This provider has support for the CQL queries as indicated in the Provider table above.
293293

294294
.. seealso::
295-
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
295+
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
296296

297297

298298
OGR
@@ -432,7 +432,7 @@ To publish an OpenSearch index, the following are required in your index:
432432
The OpenSearch provider also has the support for the CQL queries as indicated in the table above.
433433

434434
.. seealso::
435-
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
435+
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
436436

437437
.. _Oracle:
438438

@@ -730,7 +730,7 @@ block contains the necessary socket connection information.
730730
This provider has support for the CQL queries as indicated in the Provider table above.
731731

732732
.. seealso::
733-
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
733+
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
734734

735735
SQLiteGPKG
736736
^^^^^^^^^^

docs/source/publishing/ogcapi-records.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ To publish an Elasticsearch index, the following are required in your index:
5454
The ES provider also has the support for the CQL queries as indicated in the table above.
5555

5656
.. seealso::
57-
:ref:`cql` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
57+
:ref:`cql2` for more details on how to use Common Query Language (CQL) to filter the collection with specific queries.
5858

5959
TinyDBCatalogue
6060
^^^^^^^^^^^^^^^

pygeoapi/api/itemtypes.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from typing import Any, Tuple, Union
4444
import urllib.parse
4545

46-
from pygeofilter.parsers.ecql import parse as parse_ecql_text
46+
from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
4747
from pygeofilter.parsers.cql2_json import parse as parse_cql2_json
4848
from pyproj.exceptions import CRSError
4949

@@ -84,7 +84,10 @@
8484
'http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables-query-parameters', # noqa
8585
'http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/create-replace-delete', # noqa
8686
'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/schemas',
87-
'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/core-roles-features' # noqa
87+
'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/core-roles-features', # noqa
88+
'http://www.opengis.net/spec/cql2/1.0/conf/cql2-text',
89+
'http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2'
90+
8891
]
8992

9093
CONFORMANCE_CLASSES_RECORDS = [
@@ -488,7 +491,7 @@ def get_collection_items(
488491

489492
if cql_text is not None:
490493
try:
491-
filter_ = parse_ecql_text(cql_text)
494+
filter_ = parse_cql2_text(cql_text)
492495
filter_ = modify_pygeofilter(
493496
filter_,
494497
filter_crs_uri=filter_crs_uri,
@@ -522,8 +525,8 @@ def get_collection_items(
522525

523526
LOGGER.debug('Processing filter-lang parameter')
524527
filter_lang = request.params.get('filter-lang')
525-
# Currently only cql-text is handled, but it is optional
526-
if filter_lang not in [None, 'cql-json', 'cql-text']:
528+
filter_langs = [None, 'cql-json', 'cql-text', 'cql2-text', 'cql2-json']
529+
if filter_lang not in filter_langs:
527530
msg = 'Invalid filter language'
528531
return api.get_exception(
529532
HTTPStatus.BAD_REQUEST, headers, request.format,

tests/api/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ def test_conformance(config, api_):
589589

590590
assert isinstance(root, dict)
591591
assert 'conformsTo' in root
592-
assert len(root['conformsTo']) == 42
592+
assert len(root['conformsTo']) == 44
593593
assert 'http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs' \
594594
in root['conformsTo']
595595

tests/other/test_crs.py

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# Authors: Tom Kralidis <tomkralidis@gmail.com>
44
#
5-
# Copyright (c) 2025 Tom Kralidis
5+
# Copyright (c) 2026 Tom Kralidis
66
#
77
# Permission is hereby granted, free of charge, to any person
88
# obtaining a copy of this software and associated documentation
@@ -33,7 +33,7 @@
3333
import pytest
3434
from pyproj.exceptions import CRSError
3535
import pygeofilter.ast
36-
from pygeofilter.parsers.ecql import parse
36+
from pygeofilter.parsers.cql2_text import parse
3737
from pygeofilter.values import Geometry
3838
from shapely.geometry import Point
3939

@@ -201,7 +201,7 @@ def test_transform_bbox():
201201

202202
@pytest.mark.parametrize('original_filter, filter_crs, storage_crs, geometry_colum_name, expected', [ # noqa
203203
pytest.param(
204-
'INTERSECTS(geometry, POINT(1 1))',
204+
'S_INTERSECTS(geometry, POINT(1 1))',
205205
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
206206
None,
207207
None,
@@ -212,7 +212,7 @@ def test_transform_bbox():
212212
id='passthrough'
213213
),
214214
pytest.param(
215-
'INTERSECTS(geometry, POINT(1 1))',
215+
'S_INTERSECTS(geometry, POINT(1 1))',
216216
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
217217
None,
218218
'custom_geom_name',
@@ -223,7 +223,7 @@ def test_transform_bbox():
223223
id='unnested-geometry-name'
224224
),
225225
pytest.param(
226-
'some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))',
226+
'some_attribute = 10 AND S_INTERSECTS(geometry, POINT(1 1))',
227227
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
228228
None,
229229
'custom_geom_name',
@@ -238,31 +238,7 @@ def test_transform_bbox():
238238
id='nested-geometry-name'
239239
),
240240
pytest.param(
241-
'(some_attribute = 10 AND INTERSECTS(geometry, POINT(1 1))) OR '
242-
'DWITHIN(geometry, POINT(2 2), 10, meters)',
243-
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
244-
None,
245-
'custom_geom_name',
246-
pygeofilter.ast.Or(
247-
pygeofilter.ast.And(
248-
pygeofilter.ast.Equal(
249-
pygeofilter.ast.Attribute(name='some_attribute'), 10),
250-
pygeofilter.ast.GeometryIntersects(
251-
pygeofilter.ast.Attribute(name='custom_geom_name'),
252-
Geometry({'type': 'Point', 'coordinates': (1, 1)})
253-
),
254-
),
255-
pygeofilter.ast.DistanceWithin(
256-
pygeofilter.ast.Attribute(name='custom_geom_name'),
257-
Geometry({'type': 'Point', 'coordinates': (2, 2)}),
258-
distance=10,
259-
units='meters',
260-
)
261-
),
262-
id='complex-filter-name'
263-
),
264-
pytest.param(
265-
'INTERSECTS(geometry, POINT(12.512829 41.896698))',
241+
'S_INTERSECTS(geometry, POINT(12.512829 41.896698))',
266242
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
267243
'http://www.opengis.net/def/crs/EPSG/0/3004',
268244
None,
@@ -273,7 +249,7 @@ def test_transform_bbox():
273249
id='unnested-geometry-transformed-coords'
274250
),
275251
pytest.param(
276-
'some_attribute = 10 AND INTERSECTS(geometry, POINT(12.512829 41.896698))', # noqa
252+
'some_attribute = 10 AND S_INTERSECTS(geometry, POINT(12.512829 41.896698))', # noqa
277253
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
278254
'http://www.opengis.net/def/crs/EPSG/0/3004',
279255
None,
@@ -288,31 +264,7 @@ def test_transform_bbox():
288264
id='nested-geometry-transformed-coords'
289265
),
290266
pytest.param(
291-
'(some_attribute = 10 AND INTERSECTS(geometry, POINT(12.512829 41.896698))) OR ' # noqa
292-
'DWITHIN(geometry, POINT(12 41), 10, meters)',
293-
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
294-
'http://www.opengis.net/def/crs/EPSG/0/3004',
295-
None,
296-
pygeofilter.ast.Or(
297-
pygeofilter.ast.And(
298-
pygeofilter.ast.Equal(
299-
pygeofilter.ast.Attribute(name='some_attribute'), 10),
300-
pygeofilter.ast.GeometryIntersects(
301-
pygeofilter.ast.Attribute(name='geometry'),
302-
Geometry({'type': 'Point', 'coordinates': (2313682.387730346, 4641308.550187246)}) # noqa
303-
),
304-
),
305-
pygeofilter.ast.DistanceWithin(
306-
pygeofilter.ast.Attribute(name='geometry'),
307-
Geometry({'type': 'Point', 'coordinates': (2267681.8892602, 4543101.513292163)}), # noqa
308-
distance=10,
309-
units='meters',
310-
)
311-
),
312-
id='complex-filter-transformed-coords'
313-
),
314-
pytest.param(
315-
'INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
267+
'S_INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
316268
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
317269
'http://www.opengis.net/def/crs/EPSG/0/3004',
318270
None,
@@ -323,7 +275,7 @@ def test_transform_bbox():
323275
id='unnested-geometry-transformed-coords-explicit-input-crs-ewkt'
324276
),
325277
pytest.param(
326-
'INTERSECTS(geometry, POINT(1392921 5145517))',
278+
'S_INTERSECTS(geometry, POINT(1392921 5145517))',
327279
'http://www.opengis.net/def/crs/EPSG/0/3857',
328280
'http://www.opengis.net/def/crs/EPSG/0/3004',
329281
None,
@@ -334,7 +286,7 @@ def test_transform_bbox():
334286
id='unnested-geometry-transformed-coords-explicit-input-crs-filter-crs'
335287
),
336288
pytest.param(
337-
'INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
289+
'S_INTERSECTS(geometry, SRID=3857;POINT(1392921 5145517))',
338290
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
339291
'http://www.opengis.net/def/crs/EPSG/0/3004',
340292
None,
@@ -345,7 +297,7 @@ def test_transform_bbox():
345297
id='unnested-geometry-transformed-coords-ewkt-crs-overrides-filter-crs'
346298
),
347299
pytest.param(
348-
'INTERSECTS(geometry, POINT(12.512829 41.896698))',
300+
'S_INTERSECTS(geometry, POINT(12.512829 41.896698))',
349301
'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
350302
'http://www.opengis.net/def/crs/EPSG/0/3004',
351303
'custom_geom_name',

tests/provider/test_postgresql_provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
import pyproj
5858
from shapely.geometry import shape as geojson_to_geom
5959

60-
from pygeofilter.parsers.ecql import parse
60+
from pygeofilter.parsers.cql2_text import parse
6161

6262
from pygeoapi.api import API
6363
from pygeoapi.api.itemtypes import (
@@ -410,7 +410,7 @@ def test_get_not_existing_item_raise_exception(config):
410410
80835475, 80835478, 80835483, 80835486]),
411411
("osm_id BETWEEN 80800000 AND 80900000 AND waterway = 'stream'",
412412
[80835470]),
413-
("osm_id BETWEEN 80800000 AND 80900000 AND waterway ILIKE 'sTrEam'",
413+
("osm_id BETWEEN 80800000 AND 80900000 AND CASEI(waterway) LIKE CASEI('sTrEam')", # noqa
414414
[80835470]),
415415
("osm_id BETWEEN 80800000 AND 80900000 AND waterway LIKE 's%'",
416416
[80835470]),
@@ -421,7 +421,7 @@ def test_get_not_existing_item_raise_exception(config):
421421
("osm_id BETWEEN 80800000 AND 80900000 AND BBOX(foo_geom, 29, -2.8, 29.2, -2.9)", # noqa
422422
[80827793, 80835470, 80835472, 80835483, 80835489]),
423423
("osm_id BETWEEN 80800000 AND 80900000 AND "
424-
"CROSSES(foo_geom, LINESTRING(29.091 -2.731, 29.253 -2.845))",
424+
"S_CROSSES(foo_geom, LINESTRING(29.091 -2.731, 29.253 -2.845))",
425425
[80835470, 80835472, 80835489])
426426
])
427427
def test_query_cql(config, cql, expected_ids):

0 commit comments

Comments
 (0)