diff --git a/pygeofilter/parsers/cql/__init__.py b/pygeofilter/parsers/cql/__init__.py new file mode 100644 index 0000000..210bb9e --- /dev/null +++ b/pygeofilter/parsers/cql/__init__.py @@ -0,0 +1 @@ +from .parser import parse \ No newline at end of file diff --git a/pygeofilter/parsers/cql/grammar.lark b/pygeofilter/parsers/cql/grammar.lark new file mode 100644 index 0000000..de4ca7f --- /dev/null +++ b/pygeofilter/parsers/cql/grammar.lark @@ -0,0 +1,229 @@ +// ------------------------------------------------------------------------------ +// +// Project: pygeofilter +// Authors: Fabian Schindler +// +// ------------------------------------------------------------------------------ +// Copyright (C) 2021 EOX IT Services GmbH +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies of this Software or works derived from this Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ------------------------------------------------------------------------------ + +?start: boolean_value_expression + + +?boolean_value_expression: boolean_value_expression_1 + | boolean_value_expression "AND" boolean_value_expression_1 -> and_ + | boolean_value_expression "OR" boolean_value_expression_1 -> or_ + +?boolean_value_expression_1: boolean_value_expression_2 + | "NOT" boolean_value_expression_2 -> not_ + | "(" boolean_value_expression ")" + +?boolean_value_expression_2: comparison_predicate + | spatial_predicate + // | temporal_predicate + // | array_predicate + +?comparison_predicate: binary_comparison_predicate + | is_like_predicate + | is_between_predicate + | is_in_list_predicate + | is_null_predicate + +?binary_comparison_predicate: scalar_expression "=" scalar_expression -> eq + | scalar_expression "<>" scalar_expression -> ne + | scalar_expression "<" scalar_expression -> lt + | scalar_expression "<=" scalar_expression -> lte + | scalar_expression ">" scalar_expression -> gt + | scalar_expression ">=" scalar_expression -> gte + +!is_like_predicate: scalar_expression [ "NOT" ] "LIKE" character_literal [ "WILDCARD" SINGLE_QUOTED_ALPHA ] [ "SINGLECHAR" SINGLE_QUOTED_ALPHA ] [ "ESCAPECHAR" SINGLE_QUOTED_ALPHA ] [ "NOCASE" boolean ] -> like +!is_between_predicate: numeric_expression [ "NOT" ] "BETWEEN" numeric_expression "AND" numeric_expression + +!is_in_list_predicate: in_list_operand [ "NOT" ] "IN" "(" in_list_operand { "," in_list_operand } ")" +?in_list_operand: scalar_expression | temporal_literal | spatial_literal + +!is_null_predicate: scalar_expression "IS" [ "NOT" ] "NULL" + +?spatial_predicate: spatial_operator "(" geom_expression "," geom_expression ")" + +!spatial_operator = "INTERSECTS" + | "EQUALS" + | "DISJOINT" + | "TOUCHES" + | "WITHIN" + | "OVERLAPS" + | "CROSSES" + | "CONTAINS" + +?geom_expression: spatial_literal + | property + | function + +?temporal_predicate: temporal_expression temporal_operator temporal_expression + +?temporal_expression: temporal_literal + | property + | function + +!temporal_operator: "ANYINTERACTS" + | "BEFORE" + | "AFTER" + | "MEETS" + | "METBY" + | "TOVERLAPS" + | "OVERLAPPEDBY" + | "BEGINS" + | "BEGUNBY" + | "DURING" + | "TCONTAINS" + | "ENDS" + | "ENDEDBY" + | "TEQUALS" + +?array_predicate: array_expression array_operator array_expression + +?array_expression: property + | function + | array_literal + +array_literal = "[" "]" + | "[" array_element { "," array_element } "]" + +?array_element: character_literal + | numeric_literal + | boolean_literal + | spatial_literal + | temporal_literal + | property + | function + | arithmetic_expression + | array_literal + +!array_operator: "AEQUALS" + | "ACONTAINS" + | "CONTAINED BY" + | "AOVERLAPS" + + +?scalar_expression: character_literal + | numeric_literal + | boolean + | property + | function + | arithmetic_expression + + +?arithmetic_expression: sum +// | NAME "(" [ expression ("," expression)* ] ")" -> function + +?sum: product + | sum "+" product -> add + | sum "-" product -> sub + +?product: atom + | product "*" atom -> mul + | product "/" atom -> div + +?atom: attribute + | numeric_literal + | "-" atom -> neg + | function + +?character_literal: SINGLE_QUOTED + | "B" SINGLE_QUOTED -> binary_string + | "X" SINGLE_QUOTED -> hex_string +?numeric_literal: FLOAT | INT +?boolean: "TRUE" | "true" | "FALSE" | "false" | "T"i | "F"i | "1" | "0" + +// ?condition: condition_1 +// | condition "AND" condition_1 -> and_ +// | condition "OR" condition_1 -> or_ + +// ?condition_1: predicate +// | "NOT" predicate -> not_ +// | "(" condition ")" + +// ?predicate: expression "=" expression -> eq +// | expression "<>" expression -> ne +// | expression "<" expression -> lt +// | expression "<=" expression -> lte +// | expression ">" expression -> gt +// | expression ">=" expression -> gte +// | expression "BETWEEN" expression "AND" expression -> between +// | expression "NOT" "BETWEEN" expression "AND" expression -> not_between +// | expression "LIKE" SINGLE_QUOTED -> like +// | expression "NOT" "LIKE" SINGLE_QUOTED -> not_like +// | expression "ILIKE" SINGLE_QUOTED -> ilike +// | expression "NOT" "ILIKE" SINGLE_QUOTED -> not_ilike +// | expression "IN" "(" expression ( "," expression )* ")" -> in_ +// | expression "NOT" "IN" "(" expression ( "," expression )* ")" -> not_in +// | expression "IS" "NULL" -> null +// | expression "IS" "NOT" "NULL" -> not_null +// | attribute "EXISTS" -> exists +// | attribute "DOES-NOT-EXIST" -> does_not_exist +// | "INCLUDE" -> include +// | "EXCLUDE" -> exclude +// | temporal_predicate +// | spatial_predicate + +// ?temporal_predicate: expression "BEFORE" DATETIME -> before +// | expression "BEFORE" "OR" "DURING" period -> before_or_during +// | expression "DURING" period -> during +// | expression "DURING" "OR" "AFTER" period -> during_or_after +// | expression "AFTER" DATETIME -> after + +// ?spatial_predicate: _binary_spatial_predicate_func "(" expression "," expression ")" -> binary_spatial_predicate +// | "RELATE" "(" expression "," expression "," SINGLE_QUOTED ")" -> relate_spatial_predicate +// | _distance_spatial_predicate_func "(" expression "," expression "," number "," distance_units ")" -> distance_spatial_predicate +// | "BBOX" "(" expression "," number "," number "," number "," number [ "," SINGLE_QUOTED] ")" -> bbox_spatial_predicate +// !_binary_spatial_predicate_func: "INTERSECTS" | "DISJOINT" | "CONTAINS" | "WITHIN" | "TOUCHES" | "CROSSES" | "OVERLAPS" | "EQUALS" +// !_distance_spatial_predicate_func: "DWITHIN" | "BEYOND" +// !distance_units: "feet" | "meters" | "statute miles" | "nautical miles" | "kilometers" -> distance_units + +// attribute: NAME +// | DOUBLE_QUOTED + +// ?literal: number +// | BOOLEAN +// | SINGLE_QUOTED +// | ewkt_geometry -> geometry +// | envelope + +// ?number: FLOAT | INT + +// period: DATETIME "/" DATETIME +// | DURATION "/" DATETIME +// | DATETIME "/" DURATION + +// envelope: "ENVELOPE" "(" number number number number ")" + +DOUBLE_QUOTED: "\"" /.*?/ "\"" +SINGLE_QUOTED: "'" /.*?/ "'" +SINGLE_QUOTED_ALPHA: "'" /[a-z]/ "'" + +%import .wkt.ewkt_geometry +%import .iso8601.DATETIME +%import .iso8601.DURATION +%import common.CNAME -> NAME +%import common.INT +%import common.FLOAT +%import common.WS_INLINE +%ignore WS_INLINE diff --git a/pygeofilter/parsers/cql/parser.py b/pygeofilter/parsers/cql/parser.py new file mode 100644 index 0000000..3cb6765 --- /dev/null +++ b/pygeofilter/parsers/cql/parser.py @@ -0,0 +1,219 @@ +# ------------------------------------------------------------------------------ +# +# Project: pygeofilter +# Authors: Fabian Schindler +# +# ------------------------------------------------------------------------------ +# Copyright (C) 2021 EOX IT Services GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies of this Software or works derived from this Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os.path +import logging + +from lark import Lark, logger, v_args + +from ... import ast +from ... import values +from ..wkt import WKTTransformer +from ..iso8601 import ISO8601Transformer + + +logger.setLevel(logging.DEBUG) + + +SPATIAL_PREDICATES_MAP = { + "INTERSECTS": ast.GeometryIntersects, + "DISJOINT": ast.GeometryDisjoint, + "CONTAINS": ast.GeometryContains, + "WITHIN": ast.GeometryWithin, + "TOUCHES": ast.GeometryTouches, + "CROSSES": ast.GeometryCrosses, + "OVERLAPS": ast.GeometryOverlaps, + "EQUALS": ast.GeometryEquals, +} + + +@v_args(inline=True) +class CQLTransformer(WKTTransformer, ISO8601Transformer): + def and_(self, lhs, rhs): + return ast.And(lhs, rhs) + + def or_(self, lhs, rhs): + return ast.Or(lhs, rhs) + + def not_(self, node): + return ast.Not(node) + + def eq(self, lhs, rhs): + return ast.Equal(lhs, rhs) + + def ne(self, lhs, rhs): + return ast.NotEqual(lhs, rhs) + + def lt(self, lhs, rhs): + return ast.LessThan(lhs, rhs) + + def lte(self, lhs, rhs): + return ast.LessEqual(lhs, rhs) + + def gt(self, lhs, rhs): + return ast.GreaterThan(lhs, rhs) + + def gte(self, lhs, rhs): + return ast.GreaterEqual(lhs, rhs) + + def between(self, lhs, low, high): + return ast.Between(lhs, low, high, False) + + def not_between(self, lhs, low, high): + return ast.Between(lhs, low, high, True) + + def like(self, node, pattern): + return ast.Like(node, pattern, False, '%', '.', '\\', False) + + def not_like(self, node, pattern): + return ast.Like(node, pattern, False, '%', '.', '\\', True) + + def ilike(self, node, pattern): + return ast.Like(node, pattern, True, '%', '.', '\\', False) + + def not_ilike(self, node, pattern): + return ast.Like(node, pattern, True, '%', '.', '\\', True) + + def in_(self, node, *options): + return ast.In(node, list(options), False) + + def not_in(self, node, *options): + return ast.In(node, list(options), True) + + def null(self, node): + return ast.IsNull(node, False) + + def not_null(self, node): + return ast.IsNull(node, True) + + def exists(self, attribute): + return ast.Exists(attribute, False) + + def does_not_exist(self, attribute): + return ast.Exists(attribute, True) + + def include(self): + return ast.Include(False) + + def exclude(self): + return ast.Include(True) + + def before(self, node, dt): + return ast.TimeBefore(node, dt) + + def before_or_during(self, node, period): + return ast.TimeBeforeOrDuring(node, period) + + def during(self, node, period): + return ast.TimeDuring(node, period) + + def during_or_after(self, node, period): + return ast.TimeDuringOrAfter(node, period) + + def after(self, node, dt): + return ast.TimeAfter(node, dt) + + def binary_spatial_predicate(self, op, lhs, rhs): + return SPATIAL_PREDICATES_MAP[op](lhs, rhs) + + def relate_spatial_predicate(self, lhs, rhs, pattern): + return ast.Relate(lhs, rhs, pattern) + + def distance_spatial_predicate(self, op, lhs, rhs, distance, units): + cls = ast.DistanceWithin if op == "DWITHIN" else ast.DistanceBeyond + return cls(lhs, rhs, distance, units) + + def distance_units(self, value): + return value + + def bbox_spatial_predicate(self, lhs, minx, miny, maxx, maxy, crs=None): + return ast.BBox(lhs, minx, miny, maxx, maxy, crs) + + def function(self, func_name, *expressions): + return ast.Function( + func_name, list(expressions) + ) + + def add(self, lhs, rhs): + return ast.Add(lhs, rhs) + + def sub(self, lhs, rhs): + return ast.Sub(lhs, rhs) + + def mul(self, lhs, rhs): + return ast.Mul(lhs, rhs) + + def div(self, lhs, rhs): + return ast.Div(lhs, rhs) + + def neg(self, value): + return -value + + def attribute(self, name): + return ast.Attribute(str(name)) + + def period(self, start, end): + return [start, end] + + def INT(self, value): + return int(value) + + def FLOAT(self, value): + return float(value) + + def boolean(self, value): + return value in ("TRUE", "true", "T", "t", "1") + + def DOUBLE_QUOTED(self, token): + return token[1:-1] + + def SINGLE_QUOTED(self, token): + return token[1:-1] + + def geometry(self, value): + return values.Geometry(value) + + def envelope(self, x1, x2, y1, y2): + return values.Envelope(x1, x2, y1, y2) + + +parser = Lark.open( + 'grammar.lark', + rel_to=__file__, + parser='lalr', + debug=True, + transformer=CQLTransformer(), + import_paths=[os.path.dirname(os.path.dirname(__file__))] +) + + +def parse(cql_text): + return parser.parse(cql_text) + + + +if __name__ == "__main__": + print(parse("'abc' < 'bce'")) \ No newline at end of file