Coverage for gws-app/gws/lib/cql/parser.py: 97%
391 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""CQL2-Text parser. See the package documentation for the parse tree format.
3Reference:
4 - https://docs.ogc.org/is/21-065r2/21-065r2.html#cql2-bnf
5"""
7import re
8import datetime
11def parse(s: str):
12 """Parse a CQL2-Text expression.
14 Args:
15 s: CQL2-Text expression.
17 Returns:
18 A parse tree.
20 Raises:
21 `ParseError` if the expression is invalid.
22 """
24 parser = _Parser()
25 return parser.parse(s)
28class ParseError(Exception):
29 pass
32class Node:
33 AND = 'AND'
34 ARRAY = 'ARRAY'
35 BETWEEN = 'BETWEEN'
36 BOOL = 'BOOL'
37 DATE = 'DATE'
38 FLOAT = 'FLOAT'
39 FUNCTION = 'FUNCTION'
40 IN = 'IN'
41 INT = 'INT'
42 IS_NULL = 'IS_NULL'
43 LIKE = 'LIKE'
44 NAME = 'NAME'
45 NOT = 'NOT'
46 NOT_BETWEEN = 'NOT_BETWEEN'
47 NOT_IN = 'NOT_IN'
48 NOT_LIKE = 'NOT_LIKE'
49 NOT_NULL = 'NOT_NULL'
50 OR = 'OR'
51 STRING = 'STRING'
52 TIMESTAMP = 'TIMESTAMP'
53 USER_FUNCTION = 'USER_FUNCTION'
54 WKT = 'WKT'
57class C:
58 LITERALS = {
59 Node.ARRAY,
60 Node.BOOL,
61 Node.DATE,
62 Node.FLOAT,
63 Node.INT,
64 Node.STRING,
65 Node.TIMESTAMP,
66 Node.WKT,
67 }
69 COMPARISON_OPERATORS = {'=', '<>', '!=', '<', '<=', '>', '>='}
70 NOT_EQUAL_OPERATORS = {'<>', '!='}
71 ADDITIVE_OPERATORS = {'+', '-'}
72 MULTIPLICATIVE_OPERATORS = {'*', '/', '%'}
73 POWER_OPERATORS = {'^'}
75 OPERATORS = COMPARISON_OPERATORS | ADDITIVE_OPERATORS | MULTIPLICATIVE_OPERATORS | POWER_OPERATORS
77 KEYWORDS = {
78 'and',
79 'or',
80 'not',
81 'is',
82 'like',
83 'between',
84 'in',
85 }
87 PREDICATE_KEYWORDS = {
88 'not',
89 'is',
90 'like',
91 'between',
92 'in',
93 }
95 WKT_KEYWORDS = {
96 'point',
97 'linestring',
98 'polygon',
99 'multipoint',
100 'multilinestring',
101 'multipolygon',
102 'geometrycollection',
103 }
105 FUNCTIONS = {
106 's_intersects': 2,
107 's_contains': 2,
108 's_crosses': 2,
109 's_disjoint': 2,
110 's_equals': 2,
111 's_overlaps': 2,
112 's_touches': 2,
113 's_within': 2,
114 't_after': 2,
115 't_before': 2,
116 't_contains': 2,
117 't_disjoint': 2,
118 't_during': 2,
119 't_equals': 2,
120 't_finishedby': 2,
121 't_finishes': 2,
122 't_intersects': 2,
123 't_meets': 2,
124 't_metby': 2,
125 't_overlappedby': 2,
126 't_overlaps': 2,
127 't_startedby': 2,
128 't_starts': 2,
129 'a_contains': 2,
130 'a_containedby': 2,
131 'a_equals': 2,
132 'a_overlaps': 2,
133 'bbox': 4,
134 'date': 1,
135 'timestamp': 1,
136 'interval': 2,
137 'casei': 1,
138 'accenti': 1,
139 }
141 ARRAY_FUNCTIONS = {
142 'a_contains',
143 'a_containedby',
144 'a_equals',
145 'a_overlaps',
146 }
148 PATTERN_FUNCTIONS = {
149 'casei',
150 'accenti',
151 }
153##
156class _Token:
157 def __init__(self, type: str, value, pos: int):
158 self.type = type
159 self.value = value
160 self.index = pos
162 def __repr__(self):
163 return f'_Token({self.type}, {self.value!r}, {self.index})'
166_TOKENS = [
167 ('WHITESPACE', r'\s+'),
168 ('TIMESTAMP', r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?'),
169 ('DATE', r'\d{4}-\d{2}-\d{2}'),
170 ('NUMBER', r'-?\d+\.?\d*(?:[eE][+-]?\d+)?'),
171 ('STRING', r"'(?:[^'\\]|\\.|'')*'"),
172 ('QUOTED', r'"(?:[^"\\]|\\.|"")*"'),
173 ('IDENT', r'[a-zA-Z_][a-zA-Z0-9_]*'),
174 ('', r'<=|>=|<>|!=|[()[\],.+\-*/%^<>=]'),
175]
178class _Parser:
179 def __init__(self):
180 self.tokens = []
181 self.index = 0
183 def parse(self, s: str):
184 self.tokens = list(self.tokenize(s))
185 self.tokens.append(_Token('EOF', None, len(s)))
186 self.index = 0
188 e = self.parse_boolean_expression()
189 if self.tok().type != 'EOF':
190 raise self.error(f'unexpected token')
191 return e
193 def error(self, message: str, pos=None):
194 if pos is None:
195 pos = self.tok().index
196 return ParseError(f'Parse error: {message} ({pos})')
198 def tokenize(self, s):
199 pos = 0
200 while pos < len(s):
201 tok = None
202 for typ, pattern in _TOKENS:
203 r = re.compile(pattern)
204 m = r.match(s, pos=pos)
205 if m:
206 v = m.group(0)
207 tok = _Token(typ or v, v, pos)
208 pos = m.end()
209 break
210 if tok is None:
211 raise self.error(f'unexpected character', pos)
212 if tok.type != 'WHITESPACE':
213 yield tok
215 def node(self, type: str, *args):
216 return {'type': type, 'args': list(args)}
218 ##
220 def tok(self) -> _Token:
221 if self.index < len(self.tokens):
222 return self.tokens[self.index]
223 return self.tokens[-1]
225 def peek(self, n=1) -> _Token:
226 if self.index + n < len(self.tokens):
227 return self.tokens[self.index + n]
228 return self.tokens[-1]
230 def pop(self) -> _Token:
231 tok = self.tok()
232 self.index += 1
233 return tok
235 def expect(self, token_type: str) -> _Token:
236 tok = self.tok()
237 if tok.type != token_type:
238 raise self.error(f'expected {token_type}, got {tok.type}')
239 return self.pop()
241 def is_a(self, token_type: str) -> bool:
242 tok = self.tok()
243 return tok.type == token_type
245 def is_ident(self, value: str) -> bool:
246 tok = self.tok()
247 return tok.type == 'IDENT' and tok.value.upper() == value.upper()
249 def expect_ident(self, value: str) -> _Token:
250 tok = self.tok()
251 if tok.type != 'IDENT' or tok.value.upper() != value.upper():
252 raise self.error(f'expected {value}, got {tok}')
253 return self.pop()
255 ##
257 def parse_boolean_expression(self):
258 return self.parse_or_expression()
260 def parse_or_expression(self):
261 args = [self.parse_and_expression()]
262 while self.is_ident('OR'):
263 self.pop()
264 args.append(self.parse_and_expression())
265 return [Node.OR, *args] if len(args) > 1 else args[0]
267 def parse_and_expression(self):
268 args = [self.parse_not_expression()]
269 while self.is_ident('AND'):
270 self.pop()
271 args.append(self.parse_not_expression())
272 return [Node.AND, *args] if len(args) > 1 else args[0]
274 def parse_not_expression(self):
275 if self.is_ident('NOT'):
276 self.pop()
277 e = self.parse_not_expression()
278 return [Node.NOT, e]
279 return self.parse_primary_expression()
281 def parse_primary_expression(self):
282 if self.is_a('('):
283 # a parenthesized group is a boolean expression, unless it turns out
284 # to be an operand, like in "(a + b) * c = 1"
285 index = self.index
286 self.pop()
287 e = self.parse_boolean_expression()
288 self.expect(')')
289 if not self.is_operand_follower():
290 return e
291 self.index = index
292 return self.parse_predicate()
294 def is_operand_follower(self):
295 tok = self.tok()
296 if tok.type in C.OPERATORS:
297 return True
298 return tok.type == 'IDENT' and tok.value.lower() in C.PREDICATE_KEYWORDS
300 def parse_predicate(self):
301 tok = self.tok()
302 if tok.type == 'EOF':
303 raise self.error('unexpected end of expression')
305 e = self.parse_expression()
307 if self.is_ident('NOT'):
308 self.pop()
309 if self.is_ident('LIKE'):
310 return self.parse_like_predicate(e, True)
311 if self.is_ident('BETWEEN'):
312 return self.parse_between_predicate(e, True)
313 if self.is_ident('IN'):
314 return self.parse_in_predicate(e, True)
315 raise self.error(f'unexpected {self.tok().type!r}')
317 if self.is_ident('IS'):
318 return self.parse_is_null_predicate(e)
319 if self.is_ident('LIKE'):
320 return self.parse_like_predicate(e, False)
321 if self.is_ident('BETWEEN'):
322 return self.parse_between_predicate(e, False)
323 if self.is_ident('IN'):
324 return self.parse_in_predicate(e, False)
326 return self.parse_comparison_predicate(e)
328 def parse_comparison_predicate(self, e):
329 tok = self.tok()
330 if tok.type in C.COMPARISON_OPERATORS:
331 self.pop()
332 b = self.parse_expression()
333 op = '<>' if tok.type in C.NOT_EQUAL_OPERATORS else tok.type
334 return [op, e, b]
335 return e
337 def parse_like_predicate(self, e, is_not):
338 self.expect_ident('LIKE')
339 pattern = self.parse_pattern_expression()
340 return [Node.NOT_LIKE if is_not else Node.LIKE, e, pattern]
342 def parse_pattern_expression(self):
343 if self.is_a('IDENT') and self.tok().value.lower() in C.PATTERN_FUNCTIONS and self.peek().type == '(':
344 return self.parse_postfix_expression()
345 return self.parse_string_literal()
347 def parse_between_predicate(self, e, is_not):
348 self.expect_ident('BETWEEN')
349 a = self.parse_expression()
350 self.expect_ident('AND')
351 b = self.parse_expression()
352 return [Node.NOT_BETWEEN if is_not else Node.BETWEEN, e, a, b]
354 def parse_in_predicate(self, e, is_not):
355 self.expect_ident('IN')
356 if self.is_a('('):
357 self.pop()
358 a = self.parse_list(')')
359 elif self.is_a('['):
360 self.pop()
361 a = self.parse_list(']')
362 else:
363 raise self.error('expected ( or [ after IN')
364 return [Node.NOT_IN if is_not else Node.IN, e, *a]
366 def parse_is_null_predicate(self, e):
367 self.expect_ident('IS')
368 is_not = False
369 if self.is_ident('NOT'):
370 self.pop()
371 is_not = True
372 self.expect_ident('NULL')
373 return [Node.NOT_NULL if is_not else Node.IS_NULL, e]
375 def parse_expression(self):
376 return self.parse_additive_expression()
378 def parse_additive_expression(self):
379 a = self.parse_multiplicative_expression()
380 while self.tok().type in C.ADDITIVE_OPERATORS:
381 op = self.pop().value
382 b = self.parse_multiplicative_expression()
383 a = [op, a, b]
384 return a
386 def parse_multiplicative_expression(self):
387 a = self.parse_power_expression()
388 while self.tok().type in C.MULTIPLICATIVE_OPERATORS:
389 op = self.pop().value
390 b = self.parse_power_expression()
391 a = [op, a, b]
392 return a
394 def parse_power_expression(self):
395 a = self.parse_unary_expression()
396 while self.tok().type in C.POWER_OPERATORS:
397 op = self.pop().value
398 b = self.parse_unary_expression()
399 a = [op, a, b]
400 return a
402 def parse_unary_expression(self):
403 if self.tok().type in C.ADDITIVE_OPERATORS:
404 op = self.pop().value
405 e = self.parse_unary_expression()
406 if op == '-':
407 return ['-', e]
408 return e
409 return self.parse_postfix_expression()
411 def parse_postfix_expression(self):
412 pos = self.tok().index
413 e = self.parse_atom()
414 if self.is_a('('):
415 self.pop()
416 return self.parse_call(e, pos)
417 return e
419 def parse_call(self, head, pos):
420 if head[0] != Node.NAME:
421 raise self.error('invalid function name', pos)
423 name = '.'.join(head[1:])
424 key = name.lower()
426 if key in C.ARRAY_FUNCTIONS:
427 args = self.parse_array_argument_list()
428 else:
429 args = self.parse_list(')')
431 if key not in C.FUNCTIONS:
432 return [Node.USER_FUNCTION, name, *args]
434 if len(args) != C.FUNCTIONS[key]:
435 raise self.error(f'invalid number of arguments for {name!r}', pos)
436 return [Node.FUNCTION, key, *args]
438 def parse_atom(self):
439 tok = self.tok()
441 if tok.type == 'EOF':
442 raise self.error('unexpected EOF')
444 if self.is_a('('):
445 self.pop()
446 expr = self.parse_boolean_expression()
447 self.expect(')')
448 return expr
450 if self.is_a('['):
451 self.pop()
452 return [Node.ARRAY, *self.parse_list(']')]
453 if self.is_a('NUMBER'):
454 return self.parse_number_literal()
455 if self.is_a('STRING'):
456 return self.parse_string_literal()
457 if self.is_a('TIMESTAMP'):
458 return self.parse_timestamp_literal()
459 if self.is_a('DATE'):
460 return self.parse_date_literal()
461 if self.is_ident('TRUE') or self.is_ident('FALSE'):
462 return [Node.BOOL, self.pop().value.upper() == 'TRUE']
463 if self.is_a('IDENT') and self.tok().value.lower() in C.WKT_KEYWORDS:
464 p = self.peek()
465 if p.type == '(' or (p.type == 'IDENT' and p.value.upper() == 'Z'):
466 return self.parse_geometry_literal()
467 if self.is_a('IDENT') or self.is_a('QUOTED'):
468 return self.parse_name()
470 raise self.error(f'unexpected token: {tok.type}')
472 def parse_name(self):
473 parts = []
475 while True:
476 tok = self.tok()
477 if tok.type == 'QUOTED':
478 parts.append(self.unquote(self.pop().value))
479 elif tok.type == 'IDENT':
480 parts.append(self.pop().value)
481 else:
482 raise self.error(f'expected identifier')
483 if self.is_a('.'):
484 self.pop()
485 continue
486 break
488 return [Node.NAME, *parts]
490 def parse_number(self):
491 val = self.expect('NUMBER').value
492 if '.' in val or 'e' in val.lower():
493 return float(val)
494 return int(val)
496 def parse_number_literal(self):
497 val = self.parse_number()
498 return [Node.FLOAT if isinstance(val, float) else Node.INT, val]
500 def parse_string_literal(self):
501 val = self.expect('STRING').value
502 return [Node.STRING, self.unquote(val)]
504 def parse_timestamp_literal(self):
505 val = self.pop().value
506 if val.endswith('Z'):
507 val = val[:-1] + '+00:00'
508 try:
509 return [Node.TIMESTAMP, datetime.datetime.fromisoformat(val)]
510 except ValueError:
511 raise self.error(f'invalid timestamp')
513 def parse_date_literal(self):
514 val = self.pop().value
515 try:
516 year, month, day = val.split('-')
517 return [Node.DATE, datetime.date(int(year), int(month), int(day))]
518 except ValueError:
519 raise self.error(f'invalid date')
521 def parse_geometry_literal(self):
522 parts = []
523 parens = 0
524 has_word = False
526 while True:
527 if self.is_a('IDENT'):
528 if has_word:
529 parts.append(' ')
530 parts.append(self.pop().value.upper())
531 has_word = True
532 elif self.is_a('NUMBER'):
533 if has_word:
534 parts.append(' ')
535 parts.append(str(self.parse_number()))
536 has_word = True
537 elif self.is_a(','):
538 self.pop()
539 parts.append(', ')
540 has_word = False
541 elif self.is_a('('):
542 self.pop()
543 parts.append('(')
544 has_word = False
545 parens += 1
546 elif self.is_a(')'):
547 self.pop()
548 parts.append(')')
549 has_word = False
550 parens -= 1
551 if parens == 0:
552 break
553 else:
554 break
556 return [Node.WKT, ''.join(parts)]
558 def parse_array_argument_list(self):
559 elements = []
560 if not self.is_a(')'):
561 elements.append(self.parse_array_argument())
562 while self.is_a(','):
563 self.pop()
564 elements.append(self.parse_array_argument())
565 self.expect(')')
566 return elements
568 def parse_array_argument(self):
569 if self.is_a('('):
570 self.pop()
571 return [Node.ARRAY, *self.parse_list(')')]
572 return self.parse_expression()
574 def parse_list(self, end):
575 elements = []
576 if not self.is_a(end):
577 elements.append(self.parse_expression())
578 while self.is_a(','):
579 self.pop()
580 elements.append(self.parse_expression())
581 self.expect(end)
582 return elements
584 def unquote(self, s: str):
585 quote = s[0]
586 return s[1:-1].replace('\\' + quote, quote).replace(quote + quote, quote)