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

3 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +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 ``<>``. 

38Unary minus is emitted as ``NEG``: ``-a`` is ``['NEG', ['NAME', 'a']]``. 

39 

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

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

42emitted verbatim as ``USER_FUNCTION``:: 

43 

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

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

46 

47Builders 

48-------- 

49 

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

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

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

53so a subclass supports exactly what it implements. 

54 

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

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

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

58 

59 class MyBuilder(cql.SqlBuilder): 

60 def build_wkt(self, args): 

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

62 

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

64name as written, followed by the argument nodes. 

65 

66Notes 

67----- 

68 

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

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

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

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

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

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

75 Timestamps without a zone are read as UTC. 

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

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

78 order and duplicates. 

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

80""" 

81 

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

83from .builder import Builder, SqlBuilder, BuildError 

84 

85__all__ = [ 

86 'parse', 

87 'ParseError', 

88 'Node', 

89 'C', 

90 'Builder', 

91 'SqlBuilder', 

92 'BuildError', 

93]