1# ------------------------------------------------------------------------------
2#
3# Project: pycql <https://github.com/geopython/pycql>
4# Authors: Fabian Schindler <fabian.schindler@eox.at>
5#
6# ------------------------------------------------------------------------------
7# Copyright (C) 2019 EOX IT Services GmbH
8#
9# Permission is hereby granted, free of charge, to any person obtaining a copy
10# of this software and associated documentation files (the "Software"), to deal
11# in the Software without restriction, including without limitation the rights
12# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13# copies of the Software, and to permit persons to whom the Software is
14# furnished to do so, subject to the following conditions:
15#
16# The above copyright notice and this permission notice shall be included in all
17# copies of this Software or works derived from this Software.
18#
19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25# THE SOFTWARE.
26# ------------------------------------------------------------------------------
27
28"""
29"""
30
31
32class Node:
33    """ The base class for all other nodes to display the AST of CQL.
34    """
35    inline = False
36
37    def get_sub_nodes(self):
38        """ Get a list of sub-node of this node.
39
40            :return: a list of all sub-nodes
41            :rtype: list[Node]
42        """
43        raise NotImplementedError
44
45    def get_template(self):
46        """ Get a template string (using the ``%`` operator)
47            to represent the current node and sub-nodes. The template string
48            must provide a template replacement for each sub-node reported by
49            :func:`~pycql.ast.Node.get_sub_nodes`.
50
51            :return: the template to render
52        """
53        raise NotImplementedError
54
55    def __eq__(self, other):
56        if type(self) != type(other):
57            return False
58
59        return self.__dict__ == other.__dict__
60
61
62class ConditionNode(Node):
63    """ The base class for all nodes representing a condition
64    """
65    pass
66
67
68class NotConditionNode(ConditionNode):
69    """
70    Node class to represent a negation condition.
71
72    :ivar sub_node: the condition node to be negated
73    :type sub_node: Node
74    """
75
76    def __init__(self, sub_node):
77        self.sub_node = sub_node
78
79    def get_sub_nodes(self):
80        """ Returns the sub-node for the negated condition. """
81        return [self.sub_node]
82
83    def get_template(self):
84        return "NOT %s"
85
86
87class CombinationConditionNode(ConditionNode):
88    """ Node class to represent a condition to combine two other conditions
89        using either AND or OR.
90
91        :ivar lhs: the left hand side node of this combination
92        :type lhs: Node
93        :ivar rhs: the right hand side node of this combination
94        :type rhs: Node
95        :ivar op: the combination type. Either ``"AND"`` or ``"OR"``
96        :type op: str
97    """
98    def __init__(self, lhs, rhs, op):
99        self.lhs = lhs
100        self.rhs = rhs
101        self.op = op
102
103    def get_sub_nodes(self):
104        return [self.lhs, self.rhs]
105
106    def get_template(self):
107        return "%%s %s %%s" % self.op
108
109
110class PredicateNode(Node):
111    """ The base class for all nodes representing a predicate
112    """
113    pass
114
115
116class ComparisonPredicateNode(PredicateNode):
117    """ Node class to represent a comparison predicate: to compare two
118        expressions using a comparison operation.
119
120        :ivar lhs: the left hand side node of this comparison
121        :type lhs: Node
122        :ivar rhs: the right hand side node of this comparison
123        :type rhs: Node
124        :ivar op: the comparison type. One of ``"="``, ``"<>"``, ``"<"``,
125                  ``">"``, ``"<="``, ``">="``
126        :type op: str
127    """
128    def __init__(self, lhs, rhs, op):
129        self.lhs = lhs
130        self.rhs = rhs
131        self.op = op
132
133    def get_sub_nodes(self):
134        return [self.lhs, self.rhs]
135
136    def get_template(self):
137        return "%%s %s %%s" % self.op
138
139
140class BetweenPredicateNode(PredicateNode):
141    """ Node class to represent a BETWEEN predicate: to check whether an
142        expression value within a range.
143
144        :ivar lhs: the left hand side node of this comparison
145        :type lhs: Node
146        :ivar low: the lower bound of the clause
147        :type low: Node
148        :ivar high: the upper bound of the clause
149        :type high: Node
150        :ivar not_: whether the predicate shall be negated
151        :type not_: bool
152    """
153    def __init__(self, lhs, low, high, not_):
154        self.lhs = lhs
155        self.low = low
156        self.high = high
157        self.not_ = not_
158
159    def get_sub_nodes(self):
160        return [self.lhs, self.low, self.high]
161
162    def get_template(self):
163        return "%%s %sBETWEEN %%s AND %%s" % ("NOT " if self.not_ else "")
164
165
166class LikePredicateNode(PredicateNode):
167    """ Node class to represent a wildcard sting matching predicate.
168
169        :ivar lhs: the left hand side node of this predicate
170        :type lhs: Node
171        :ivar rhs: the right hand side node of this predicate
172        :type rhs: Node
173        :ivar case: whether the comparison shall be case sensitive
174        :type case: bool
175        :ivar not_: whether the predicate shall be negated
176        :type not_: bool
177    """
178    def __init__(self, lhs, rhs, case, not_):
179        self.lhs = lhs
180        self.rhs = rhs
181        self.case = case
182        self.not_ = not_
183
184    def get_sub_nodes(self):
185        return [self.lhs, self.rhs]
186
187    def get_template(self):
188        return "%%s %s%sLIKE %%s" % (
189            "NOT " if self.not_ else "",
190            "I" if self.case else ""
191        )
192
193
194class InPredicateNode(PredicateNode):
195    """ Node class to represent list checking predicate.
196
197        :ivar lhs: the left hand side node of this predicate
198        :type lhs: Node
199        :ivar sub_nodes: the list of sub nodes to check the inclusion
200                         against
201        :type sub_nodes: list[Node]
202        :ivar not_: whether the predicate shall be negated
203        :type not_: bool
204    """
205    def __init__(self, lhs, sub_nodes, not_):
206        self.lhs = lhs
207        self.sub_nodes = sub_nodes
208        self.not_ = not_
209
210    def get_sub_nodes(self):
211        return [self.lhs] + list(self.sub_nodes)
212
213    def get_template(self):
214        return "%%s %sIN (%s)" % (
215            "NOT " if self.not_ else "",
216            ", ".join(["%s"] * len(self.sub_nodes))
217        )
218
219
220class NullPredicateNode(PredicateNode):
221    """ Node class to represent null check predicate.
222
223        :ivar lhs: the left hand side node of this predicate
224        :type lhs: Node
225        :ivar not_: whether the predicate shall be negated
226        :type not_: bool
227    """
228    def __init__(self, lhs, not_):
229        self.lhs = lhs
230        self.not_ = not_
231
232    def get_sub_nodes(self):
233        return [self.lhs]
234
235    def get_template(self):
236        return "%%s IS %sNULL" % ("NOT " if self.not_ else "")
237
238
239# class ExistsPredicateNode(PredicateNode):
240#     pass
241
242
243class TemporalPredicateNode(PredicateNode):
244    """ Node class to represent temporal predicate.
245
246        :ivar lhs: the left hand side node of this comparison
247        :type lhs: Node
248        :ivar rhs: the right hand side node of this comparison
249        :type rhs: Node
250        :ivar op: the comparison type. One of ``"BEFORE"``,
251                  ``"BEFORE OR DURING"``, ``"DURING"``,
252                  ``"DURING OR AFTER"``, ``"AFTER"``
253        :type op: str
254    """
255    def __init__(self, lhs, rhs, op):
256        self.lhs = lhs
257        self.rhs = rhs
258        self.op = op
259
260    def get_sub_nodes(self):
261        return [self.lhs, self.rhs]
262
263    def get_template(self):
264        return "%%s %s %%s" % self.op
265
266
267class SpatialPredicateNode(PredicateNode):
268    """ Node class to represent spatial relation predicate.
269
270        :ivar lhs: the left hand side node of this comparison
271        :type lhs: Node
272        :ivar rhs: the right hand side node of this comparison
273        :type rhs: Node
274        :ivar op: the comparison type. One of ``"INTERSECTS"``,
275                  ``"DISJOINT"``, ``"CONTAINS"``, ``"WITHIN"``,
276                  ``"TOUCHES"``, ``"CROSSES"``, ``"OVERLAPS"``,
277                  ``"EQUALS"``, ``"RELATE"``, ``"DWITHIN"``, ``"BEYOND"``
278        :type op: str
279        :ivar pattern: the relationship patter for the ``"RELATE"`` operation
280        :type pattern: str or None
281        :ivar distance: the distance for distance related operations
282        :type distance: Node or None
283        :ivar units: the units for distance related operations
284        :type units: str or None
285    """
286    def __init__(self, lhs, rhs, op, pattern=None, distance=None, units=None):
287        self.lhs = lhs
288        self.rhs = rhs
289        self.op = op
290        self.pattern = pattern
291        self.distance = distance
292        self.units = units
293
294    def get_sub_nodes(self):
295        return [self.lhs, self.rhs]
296
297    def get_template(self):
298        if self.pattern:
299            return "%s(%%s, %%s, %r)" % (self.op, self.pattern)
300        elif self.distance or self.units:
301            return "%s(%%s, %%s, %r, %r)" % (self.op, self.distance, self.units)
302        else:
303            return "%s(%%s, %%s)" % (self.op)
304
305
306class BBoxPredicateNode(PredicateNode):
307    """ Node class to represent a bounding box predicate.
308
309        :ivar lhs: the left hand side node of this predicate
310        :type lhs: Node
311        :ivar minx: the minimum X value of the bounding box
312        :type minx: float
313        :ivar miny: the minimum Y value of the bounding box
314        :type miny: float
315        :ivar maxx: the maximum X value of the bounding box
316        :type maxx: float
317        :ivar maxx: the maximum Y value of the bounding box
318        :type maxx: float
319        :ivar crs: the coordinate reference system identifier
320                   for the CRS the BBox is expressed in
321        :type crs: str
322    """
323    def __init__(self, lhs, minx, miny, maxx, maxy, crs=None):
324        self.lhs = lhs
325        self.minx = minx
326        self.miny = miny
327        self.maxx = maxx
328        self.maxy = maxy
329        self.crs = crs
330
331    def get_sub_nodes(self):
332        return [self.lhs]
333
334    def get_template(self):
335        return "BBOX(%%s, %r, %r, %r, %r, %r)" % (
336            self.minx, self.miny, self.maxx, self.maxy, self.crs
337        )
338
339
340class ExpressionNode(Node):
341    """ The base class for all nodes representing expressions
342    """
343    pass
344
345
346class AttributeExpression(ExpressionNode):
347    """ Node class to represent attribute lookup expressions
348
349        :ivar name: the name of the attribute to be accessed
350        :type name: str
351    """
352    inline = True
353
354    def __init__(self, name):
355        self.name = name
356
357    def __repr__(self):
358        return "ATTRIBUTE %s" % self.name
359
360
361class LiteralExpression(ExpressionNode):
362    """ Node class to represent literal value expressions
363
364        :ivar value: the value of the literal
365        :type value: str, float, int, datetime, timedelta
366    """
367    inline = True
368
369    def __init__(self, value):
370        self.value = value
371
372    def __repr__(self):
373        return "LITERAL %r" % self.value
374
375
376class ArithmeticExpressionNode(ExpressionNode):
377    """ Node class to represent arithmetic operation expressions with two
378        sub-expressions and an operator.
379
380        :ivar lhs: the left hand side node of this arithmetic expression
381        :type lhs: Node
382        :ivar rhs: the right hand side node of this arithmetic expression
383        :type rhs: Node
384        :ivar op: the comparison type. One of ``"+"``, ``"-"``, ``"*"``, ``"/"``
385        :type op: str
386    """
387    def __init__(self, lhs, rhs, op):
388        self.lhs = lhs
389        self.rhs = rhs
390        self.op = op
391
392    def get_sub_nodes(self):
393        return [self.lhs, self.rhs]
394
395    def get_template(self):
396        return "%%s %s %%s" % self.op
397
398
399def indent(text, amount, ch=' '):
400    padding = amount * ch
401    return ''.join(padding+line for line in text.splitlines(True))
402
403
404def get_repr(node, indent_amount=0, indent_incr=4):
405    """ Get a debug representation of the given AST node. ``indent_amount``
406        and ``indent_incr`` are for the recursive call and don't need to be
407        passed.
408
409        :param Node node: the node to get the representation for
410        :param int indent_amount: the current indentation level
411        :param int indent_incr: the indentation incrementation per level
412        :return: the represenation of the node
413        :rtype: str
414    """
415    sub_nodes = node.get_sub_nodes()
416    template = node.get_template()
417
418    args = []
419    for sub_node in sub_nodes:
420        if isinstance(sub_node, Node) and not sub_node.inline:
421            args.append("(\n%s\n)" %
422                indent(
423                    get_repr(sub_node, indent_amount + indent_incr, indent_incr),
424                    indent_amount + indent_incr
425                )
426            )
427        else:
428            args.append(repr(sub_node))
429
430    return template % tuple(args)
431