Coverage for gws-app/gws/lib/cql/__init__.py: 100%

3 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""CQL2 support. 

2 

3Parses CQL2-Text filter expressions and turns them into database expressions. 

4 

5Reference: 

6 - https://docs.ogc.org/is/21-065r2/21-065r2.html 

7 - https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-bnf 

8 

9Submodules: 

10 

11- ``parser``: the CQL2-Text parser (``parse``, ``ParseError``) and the 

12 constants for node types (``Node``) and keyword sets (``C``). 

13- ``builder``: the generic tree walker ``Builder``, the SQLAlchemy 

14 generator ``SqlBuilder`` and ``BuildError``. 

15 

16The parser does not know about databases, and the builders do not parse 

17text: the parse tree described below is the interface between them. 

18 

19Usage:: 

20 

21 cond = cql.SqlBuilder(table).build(cql.parse("a_int > 10 AND S_INTERSECTS(a_geom, POINT(1 1))")) 

22 sel = sa.select(table).where(cond) 

23 

24Parse trees 

25----------- 

26 

27``parse`` returns a tree of plain lists, where the first element is the node type 

28and the rest are arguments:: 

29 

30 a_int > 10 ['>', ['NAME', 'a_int'], ['INT', 10]] 

31 a_int IS NULL ['IS_NULL', ['NAME', 'a_int']] 

32 a IN (1, 2) ['IN', ['NAME', 'a'], ['INT', 1], ['INT', 2]] 

33 

34Node types are listed in ``Node``, operators and other keyword sets in ``C``. 

35Literal nodes carry a python value: ``['INT', 10]``, ``['DATE', datetime.date(...)]``. 

36A ``NAME`` node carries the dot-separated parts of a property name: ``a.b`` is 

37``['NAME', 'a', 'b']``. The operators ``<>`` and ``!=`` are both emitted as ``<>``. 

38 

39Function calls come in two flavours. Names the standard knows about (``C.FUNCTIONS``) 

40are checked for arity and emitted lowercased as ``FUNCTION``, everything else is 

41emitted verbatim as ``USER_FUNCTION``:: 

42 

43 S_Intersects(g, h) ['FUNCTION', 's_intersects', ['NAME', 'g'], ['NAME', 'h']] 

44 myschema.fn(1) ['USER_FUNCTION', 'myschema.fn', ['INT', 1]] 

45 

46Builders 

47-------- 

48 

49``Builder`` walks a tree and dispatches on the node type to a ``build_<type>`` method, 

50and on the function name to a ``func_<name>`` method. Operators without a ``build_<type>`` 

51method go to ``build_operator``. Missing methods raise ``BuildError``, 

52so a subclass supports exactly what it implements. 

53 

54``SqlBuilder`` generates SQLAlchemy expressions for a postgis table and implements all 

55standard functions. Subclasses customize single node types, e.g. a model that stores 

56geometries in a projected crs only overrides the geometry literals:: 

57 

58 class MyBuilder(cql.SqlBuilder): 

59 def build_wkt(self, args): 

60 return sa.func.ST_Transform(super().build_wkt(args), 3857) 

61 

62Backend specific functions are handled by ``build_user_function``, which receives the 

63name as written, followed by the argument nodes. 

64 

65Notes 

66----- 

67 

68- ``SqlBuilder`` requires postgis, and the ``unaccent`` extension for the ``ACCENTI`` 

69 function (``CREATE EXTENSION unaccent``). 

70- ``SqlBuilder`` resolves a ``NAME`` by its first part only, as a column of the table. 

71- Geometry literals and ``BBOX`` are in WGS84 (EPSG:4326). 

72- Temporal predicates compare ``tstzrange`` values, an instant being a degenerate 

73 range. Bounds are inclusive, so intervals that only touch do intersect. 

74 Timestamps without a zone are read as UTC. 

75- Array literals are accepted both in the standard form ``('a', 'b')`` and as 

76 ``['a', 'b']``. Arrays are compared as sets, in particular ``A_EQUALS`` ignores 

77 order and duplicates. 

78- ``BBOX`` is limited to the 2d form with four arguments. 

79""" 

80 

81from .parser import parse, ParseError, Node, C 

82from .builder import Builder, SqlBuilder, BuildError 

83 

84__all__ = [ 

85 'parse', 

86 'ParseError', 

87 'Node', 

88 'C', 

89 'Builder', 

90 'SqlBuilder', 

91 'BuildError', 

92]