ransacklib 1.1.0.dev5__tar.gz → 1.1.0.dev7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ransacklib-1.1.0.dev5/ransacklib.egg-info → ransacklib-1.1.0.dev7}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/pyproject.toml +1 -1
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/operator.py +128 -50
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/parser.py +10 -2
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/transformer.py +67 -38
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7/ransacklib.egg-info}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/tests/test_transformer.py +193 -3
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/LICENSE +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/README.rst +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/__init__.py +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/exceptions.py +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/function.py +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/py.typed +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/SOURCES.txt +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/dependency_links.txt +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/requires.txt +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/top_level.txt +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/setup.cfg +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/tests/test_operator.py +0 -0
- {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/tests/test_parser.py +0 -0
|
@@ -500,63 +500,104 @@ def binary_operation(operator: str, left: Any, right: Any) -> Any:
|
|
|
500
500
|
raise OperatorNotFoundError(operator, (t1, t2), (left, right)) from None
|
|
501
501
|
|
|
502
502
|
|
|
503
|
-
def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any]:
|
|
504
|
-
|
|
503
|
+
def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any, bool]:
|
|
504
|
+
"""
|
|
505
|
+
Maps an expression into its SQL representation.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
op: Operator string (e.g., "=", "+", "and", "contains").
|
|
509
|
+
l_sql: SQL string representing the left operand.
|
|
510
|
+
r_sql: SQL string representing the right operand.
|
|
511
|
+
t1: Resolved type of the left operand.
|
|
512
|
+
t2: Resolved type of the right operand.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
A tuple of:
|
|
516
|
+
- SQL expression string,
|
|
517
|
+
- result type,
|
|
518
|
+
- switch flag (True if operands were reversed semantically).
|
|
519
|
+
|
|
520
|
+
Raises:
|
|
521
|
+
KeyError:
|
|
522
|
+
If no mapping exists for the given operator and operand types.
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
def _get_comp_dict_sql(
|
|
526
|
+
op: str, tuple_repr_f
|
|
527
|
+
) -> dict[tuple[Operand, Operand], tuple[str, bool]]:
|
|
528
|
+
"""
|
|
529
|
+
Generate comparison operator mappings for multiple type combinations.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
op: SQL comparison operator (e.g., ">", "<=").
|
|
533
|
+
tuple_repr_f: SQL function name used for tuple bounds (e.g. "lower").
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Mapping from (type1, type2) -> (SQL string, switch flag).
|
|
537
|
+
"""
|
|
505
538
|
return {
|
|
506
|
-
(Number, Number): f"{l_sql} {op} {r_sql}",
|
|
507
|
-
(Number, list): f"{l_sql} {op} ANY({r_sql})",
|
|
508
|
-
(Number, tuple): f"{l_sql} {op} {tuple_repr_f}({r_sql})",
|
|
509
|
-
(datetime, datetime): f"{l_sql} {op} {r_sql}",
|
|
510
|
-
(datetime, list): f"{l_sql} {op} ANY({r_sql})",
|
|
511
|
-
(datetime, tuple): f"{r_sql} {op} {tuple_repr_f}({l_sql})",
|
|
512
|
-
(timedelta, timedelta): f"{l_sql} {op} {r_sql}",
|
|
513
|
-
(timedelta, list): f"{l_sql} {op} ANY({r_sql})",
|
|
514
|
-
("ip", "ip"): f"{l_sql} {op} {r_sql}",
|
|
515
|
-
("ip", list): f"{l_sql} {op} ANY({r_sql})",
|
|
516
|
-
# (list, list): f"{l_sql} && {r_sql}",
|
|
517
|
-
# (tuple, tuple): f"upper({l_sql}) > lower({r_sql})",
|
|
539
|
+
(Number, Number): (f"{l_sql} {op} {r_sql}", False),
|
|
540
|
+
(Number, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
541
|
+
(Number, tuple): (f"{l_sql} {op} {tuple_repr_f}({r_sql})", False),
|
|
542
|
+
(datetime, datetime): (f"{l_sql} {op} {r_sql}", False),
|
|
543
|
+
(datetime, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
544
|
+
(datetime, tuple): (f"{r_sql} {op} {tuple_repr_f}({l_sql})", True),
|
|
545
|
+
(timedelta, timedelta): (f"{l_sql} {op} {r_sql}", False),
|
|
546
|
+
(timedelta, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
547
|
+
("ip", "ip"): (f"{l_sql} {op} {r_sql}", False),
|
|
548
|
+
("ip", list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
518
549
|
}
|
|
519
550
|
|
|
551
|
+
# Comparison and logical operators (result is always boolean).
|
|
552
|
+
# The boolean flag indicates whether operand order was reversed.
|
|
520
553
|
d: dict[str, dict] = {
|
|
521
554
|
"=": {
|
|
522
|
-
(Number, Number): f"{l_sql} = {r_sql}",
|
|
523
|
-
(Number, list): f"{l_sql} = ANY({r_sql})",
|
|
524
|
-
(Number, tuple): f"{r_sql} @> {l_sql}",
|
|
525
|
-
(datetime, datetime): f"{l_sql} = {r_sql}",
|
|
526
|
-
(datetime, list): f"{l_sql} = ANY({r_sql})",
|
|
527
|
-
(datetime, tuple): f"{r_sql} @> {l_sql}",
|
|
528
|
-
(timedelta, timedelta): f"{l_sql} = {r_sql}",
|
|
529
|
-
(timedelta, list): f"{l_sql} = ANY({r_sql})",
|
|
530
|
-
(str, str): f"{l_sql} = {r_sql}",
|
|
531
|
-
(str, list): f"{l_sql} = ANY({r_sql})",
|
|
532
|
-
("ip", "ip"): f"{l_sql} && {r_sql}",
|
|
533
|
-
("ip", list): f"{l_sql} && ANY({r_sql})",
|
|
534
|
-
(list, list): f"{l_sql} && {r_sql}",
|
|
535
|
-
(tuple, tuple): f"{l_sql} && {r_sql}",
|
|
555
|
+
(Number, Number): (f"{l_sql} = {r_sql}", False),
|
|
556
|
+
(Number, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
557
|
+
(Number, tuple): (f"{r_sql} @> {l_sql}", True),
|
|
558
|
+
(datetime, datetime): (f"{l_sql} = {r_sql}", False),
|
|
559
|
+
(datetime, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
560
|
+
(datetime, tuple): (f"{r_sql} @> {l_sql}", True),
|
|
561
|
+
(timedelta, timedelta): (f"{l_sql} = {r_sql}", False),
|
|
562
|
+
(timedelta, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
563
|
+
(str, str): (f"{l_sql} = {r_sql}", False),
|
|
564
|
+
(str, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
565
|
+
("ip", "ip"): (f"{l_sql} && {r_sql}", False),
|
|
566
|
+
("ip", list): (f"{l_sql} && ANY({r_sql})", False),
|
|
567
|
+
(list, list): (f"{l_sql} && {r_sql}", False),
|
|
568
|
+
(tuple, tuple): (f"{l_sql} && {r_sql}", False),
|
|
536
569
|
},
|
|
537
570
|
">": _get_comp_dict_sql(">", "lower")
|
|
538
|
-
| {(tuple, tuple): f"upper({l_sql}) > lower({r_sql})"},
|
|
571
|
+
| {(tuple, tuple): (f"upper({l_sql}) > lower({r_sql})", False)},
|
|
539
572
|
">=": _get_comp_dict_sql(">=", "lower")
|
|
540
|
-
| {(tuple, tuple): f"upper({l_sql}) >= lower({r_sql})"},
|
|
573
|
+
| {(tuple, tuple): (f"upper({l_sql}) >= lower({r_sql})", False)},
|
|
541
574
|
"<": _get_comp_dict_sql("<", "upper")
|
|
542
|
-
| {(tuple, tuple): f"lower({l_sql}) < upper({r_sql})"},
|
|
575
|
+
| {(tuple, tuple): (f"lower({l_sql}) < upper({r_sql})", False)},
|
|
543
576
|
"<=": _get_comp_dict_sql("<=", "upper")
|
|
544
|
-
| {(tuple, tuple): f"lower({l_sql}) <= upper({r_sql})"},
|
|
545
|
-
"and": {(bool, bool): f"{l_sql} AND {r_sql}"},
|
|
546
|
-
"or": {(bool, bool): f"{l_sql} OR {r_sql}"},
|
|
547
|
-
"contains": {
|
|
577
|
+
| {(tuple, tuple): (f"lower({l_sql}) <= upper({r_sql})", False)},
|
|
578
|
+
"and": {(bool, bool): (f"{l_sql} AND {r_sql}", False)},
|
|
579
|
+
"or": {(bool, bool): (f"({l_sql} OR {r_sql})", False)},
|
|
580
|
+
"contains": {
|
|
581
|
+
(str, str): (f"position({r_sql} in {l_sql})>0", True),
|
|
582
|
+
(list, str): (
|
|
583
|
+
f"position({r_sql} in array_to_string({l_sql}, ','))>0",
|
|
584
|
+
True,
|
|
585
|
+
),
|
|
586
|
+
},
|
|
548
587
|
}
|
|
588
|
+
|
|
589
|
+
# Arithmetic operators (result type varies, operands are not swapped).
|
|
549
590
|
d2: dict[str, dict] = {
|
|
550
591
|
"+": {
|
|
551
|
-
(Number, Number): (f"{l_sql} + {r_sql}", Number),
|
|
552
|
-
(datetime, timedelta): (f"{l_sql} + {r_sql}", datetime),
|
|
553
|
-
(timedelta, timedelta): (f"{l_sql} + {r_sql}", timedelta),
|
|
592
|
+
(Number, Number): (f"({l_sql} + {r_sql})", Number),
|
|
593
|
+
(datetime, timedelta): (f"({l_sql} + {r_sql})", datetime),
|
|
594
|
+
(timedelta, timedelta): (f"({l_sql} + {r_sql})", timedelta),
|
|
554
595
|
},
|
|
555
596
|
"-": {
|
|
556
|
-
(Number, Number): (f"{l_sql} - {r_sql}", Number),
|
|
557
|
-
(datetime, timedelta): (f"{l_sql} - {r_sql}", datetime),
|
|
558
|
-
(timedelta, timedelta): (f"{l_sql} - {r_sql}", timedelta),
|
|
559
|
-
(datetime, datetime): (f"{l_sql} - {r_sql}", timedelta),
|
|
597
|
+
(Number, Number): (f"({l_sql} - {r_sql})", Number),
|
|
598
|
+
(datetime, timedelta): (f"({l_sql} - {r_sql})", datetime),
|
|
599
|
+
(timedelta, timedelta): (f"({l_sql} - {r_sql})", timedelta),
|
|
600
|
+
(datetime, datetime): (f"({l_sql} - {r_sql})", timedelta),
|
|
560
601
|
},
|
|
561
602
|
"*": {
|
|
562
603
|
(timedelta, Number): (f"{l_sql} * {r_sql}", timedelta),
|
|
@@ -567,21 +608,55 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any]:
|
|
|
567
608
|
(Number, Number): (f"{l_sql} / {r_sql}", Number),
|
|
568
609
|
},
|
|
569
610
|
"%": {
|
|
570
|
-
(Number, Number): (f"{l_sql} % {r_sql}", Number),
|
|
611
|
+
(Number, Number): (f"({l_sql} % {r_sql})", Number),
|
|
571
612
|
},
|
|
572
613
|
}
|
|
573
|
-
|
|
574
|
-
|
|
614
|
+
|
|
615
|
+
if op == "==" and t1 == t2:
|
|
616
|
+
return (f"{l_sql} = {r_sql}", bool, False)
|
|
617
|
+
|
|
618
|
+
# 'in' operator behaves the same as '=' operator.
|
|
575
619
|
if op == "in":
|
|
576
|
-
|
|
620
|
+
sql, switch = d["="][(t1, t2)]
|
|
621
|
+
return sql, bool, switch
|
|
622
|
+
|
|
623
|
+
# Arithmetic operator handling.
|
|
577
624
|
if op in d2:
|
|
578
|
-
|
|
579
|
-
|
|
625
|
+
sql, type_ = d2[op][(t1, t2)]
|
|
626
|
+
return sql, type_, False
|
|
627
|
+
|
|
628
|
+
# Default: comparison/logical operator.
|
|
629
|
+
sql, switch = d[op][(t1, t2)]
|
|
630
|
+
return sql, bool, switch
|
|
580
631
|
|
|
581
632
|
|
|
582
633
|
def binary_operation_sql(
|
|
583
634
|
operator: str, left: str, right: str, l_type: Any, r_type: Any
|
|
584
|
-
) -> tuple[str, Any]:
|
|
635
|
+
) -> tuple[str, bool, Any]:
|
|
636
|
+
"""
|
|
637
|
+
Converts a binary operation into its SQL equivalent with type handling.
|
|
638
|
+
|
|
639
|
+
This function normalizes operand types (e.g., numeric types, IP types),
|
|
640
|
+
delegates SQL generation to `_operator_map_sql`, and handles operand
|
|
641
|
+
swapping if needed.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
operator: Operator string (e.g., "+", "=", "in").
|
|
645
|
+
left: SQL string for the left operand.
|
|
646
|
+
right: SQL string for the right operand.
|
|
647
|
+
l_type: Type of the left operand.
|
|
648
|
+
r_type: Type of the right operand.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
A tuple of:
|
|
652
|
+
- SQL expression string,
|
|
653
|
+
- result type,
|
|
654
|
+
- switch flag (True if operands were reversed).
|
|
655
|
+
|
|
656
|
+
Raises:
|
|
657
|
+
OperatorNotFoundError:
|
|
658
|
+
If no valid SQL mapping exists for the operator/type combination.
|
|
659
|
+
"""
|
|
585
660
|
# Normalize numeric types
|
|
586
661
|
l_type = Number if l_type in (int, float) else l_type
|
|
587
662
|
r_type = Number if r_type in (int, float) else r_type
|
|
@@ -595,7 +670,10 @@ def binary_operation_sql(
|
|
|
595
670
|
return _operator_map_sql(operator, left, right, l_type, r_type)
|
|
596
671
|
except KeyError:
|
|
597
672
|
try:
|
|
598
|
-
|
|
673
|
+
sql, type_, switch = _operator_map_sql(
|
|
674
|
+
operator, right, left, r_type, l_type
|
|
675
|
+
)
|
|
676
|
+
return sql, type_, not switch
|
|
599
677
|
except KeyError:
|
|
600
678
|
pass
|
|
601
679
|
raise OperatorNotFoundError(operator, (l_type, r_type), (left, right)) from None
|
|
@@ -12,6 +12,7 @@ Classes:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import os
|
|
15
|
+
from datetime import tzinfo
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any, no_type_check
|
|
17
18
|
|
|
@@ -120,7 +121,11 @@ class Parser:
|
|
|
120
121
|
%ignore WS
|
|
121
122
|
""" # noqa
|
|
122
123
|
|
|
123
|
-
def __init__(
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
context: dict[str, Any] | None = None,
|
|
127
|
+
default_timezone: tzinfo | None = None,
|
|
128
|
+
) -> None:
|
|
124
129
|
"""
|
|
125
130
|
Initialize the Parser.
|
|
126
131
|
|
|
@@ -128,6 +133,9 @@ class Parser:
|
|
|
128
133
|
context (dict[str, Any] | None): Optional dictionary of variables that can
|
|
129
134
|
be referenced in queries. Context variables override data variables unless
|
|
130
135
|
the data variable is explicitly accessed with a leading dot.
|
|
136
|
+
default_timezone (tzinfo | None): Time zone to apply to parsed datetime
|
|
137
|
+
values that do not include time zone information. If None, naive datetimes
|
|
138
|
+
remain naive.
|
|
131
139
|
"""
|
|
132
140
|
# Determine the cache file of the grammar. By default, it's in the home
|
|
133
141
|
# directory. But when the environment variable is set, use the value
|
|
@@ -142,7 +150,7 @@ class Parser:
|
|
|
142
150
|
propagate_positions=True,
|
|
143
151
|
cache=cache_path,
|
|
144
152
|
)
|
|
145
|
-
self.shaper = ExpressionTransformer(context)
|
|
153
|
+
self.shaper = ExpressionTransformer(context, default_timezone)
|
|
146
154
|
|
|
147
155
|
@no_type_check
|
|
148
156
|
def parse(self, data: str) -> Tree[Token]:
|
|
@@ -20,7 +20,7 @@ Classes:
|
|
|
20
20
|
|
|
21
21
|
import re
|
|
22
22
|
from collections.abc import Mapping, MutableSequence
|
|
23
|
-
from datetime import datetime, timedelta
|
|
23
|
+
from datetime import datetime, timedelta, tzinfo
|
|
24
24
|
from typing import Any, cast
|
|
25
25
|
|
|
26
26
|
from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
|
|
@@ -194,7 +194,11 @@ class ExpressionTransformer(Transformer):
|
|
|
194
194
|
into appropriate objects (ipranges, datetime, list, ...).
|
|
195
195
|
"""
|
|
196
196
|
|
|
197
|
-
def __init__(
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
context: dict[str, Any] | None = None,
|
|
200
|
+
default_timezone: tzinfo | None = None,
|
|
201
|
+
) -> None:
|
|
198
202
|
"""
|
|
199
203
|
Initializes the ExpressionTransformer with a context dictionary
|
|
200
204
|
for constants.
|
|
@@ -203,8 +207,12 @@ class ExpressionTransformer(Transformer):
|
|
|
203
207
|
context: A dictionary containing constant values that can be
|
|
204
208
|
accessed by variable names within expressions.
|
|
205
209
|
Defaults to an empty dictionary if none is provided.
|
|
210
|
+
default_timezone: Time zone to apply to parsed datetime values
|
|
211
|
+
that do not include time zone information.
|
|
212
|
+
If None, naive datetimes remain naive.
|
|
206
213
|
"""
|
|
207
214
|
self.context = context if context is not None else {}
|
|
215
|
+
self.default_timezone = default_timezone
|
|
208
216
|
|
|
209
217
|
def number(self, data: Token) -> Tree[TokenWrapper]:
|
|
210
218
|
"""
|
|
@@ -322,6 +330,11 @@ class ExpressionTransformer(Transformer):
|
|
|
322
330
|
|
|
323
331
|
# Convert to datetime object
|
|
324
332
|
dtime = datetime.fromisoformat(datetime_str)
|
|
333
|
+
|
|
334
|
+
# If no time zone was specified, then set to default time zone
|
|
335
|
+
if dtime.tzinfo is None:
|
|
336
|
+
dtime = dtime.replace(tzinfo=self.default_timezone)
|
|
337
|
+
|
|
325
338
|
tw = TokenWrapper(_add_tokens(date, time), dtime)
|
|
326
339
|
|
|
327
340
|
return Tree("datetime", [tw], _create_meta(tw))
|
|
@@ -337,7 +350,14 @@ class ExpressionTransformer(Transformer):
|
|
|
337
350
|
"""
|
|
338
351
|
return Tree(
|
|
339
352
|
"datetime",
|
|
340
|
-
[
|
|
353
|
+
[
|
|
354
|
+
TokenWrapper(
|
|
355
|
+
date,
|
|
356
|
+
datetime.fromisoformat(date + "T00:00:00").replace(
|
|
357
|
+
tzinfo=self.default_timezone
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
],
|
|
341
361
|
_create_meta(date),
|
|
342
362
|
)
|
|
343
363
|
|
|
@@ -819,7 +839,8 @@ class SQLInterpreter(Interpreter):
|
|
|
819
839
|
data: Optional dictionary containing the definition of queried columns.
|
|
820
840
|
|
|
821
841
|
Returns:
|
|
822
|
-
A part of the SQL query
|
|
842
|
+
A part of the SQL query (the part after 'SELECT' or 'WHERE') and its
|
|
843
|
+
parameters.
|
|
823
844
|
"""
|
|
824
845
|
self.data = data if data is not None else {}
|
|
825
846
|
|
|
@@ -852,13 +873,13 @@ class SQLInterpreter(Interpreter):
|
|
|
852
873
|
cast = "iprange"
|
|
853
874
|
return (f"%s::{cast}", [token.real_value], type(token.real_value))
|
|
854
875
|
|
|
855
|
-
def neg(self, tree: Tree) -> tuple[str, list
|
|
876
|
+
def neg(self, tree: Tree) -> tuple[str, list, Any]:
|
|
856
877
|
sql, params, type_ = self.visit(tree)
|
|
857
|
-
return (f"-{sql}", params, type_)
|
|
878
|
+
return (f"(-{sql})", params, type_)
|
|
858
879
|
|
|
859
880
|
def range_op(
|
|
860
881
|
self, l_tree: Tree, r_tree: Tree
|
|
861
|
-
) -> tuple[str, list
|
|
882
|
+
) -> tuple[str, list, type[tuple] | type[IP]]:
|
|
862
883
|
l_sql, l_params, l_type = self.visit(l_tree)
|
|
863
884
|
r_sql, r_params, r_type = self.visit(r_tree)
|
|
864
885
|
|
|
@@ -867,7 +888,7 @@ class SQLInterpreter(Interpreter):
|
|
|
867
888
|
elif issubclass(l_type, float) and issubclass(r_type, float):
|
|
868
889
|
range_type = "numrange"
|
|
869
890
|
elif issubclass(l_type, datetime) and issubclass(r_type, datetime):
|
|
870
|
-
if "WITH TIME ZONE" in l_sql
|
|
891
|
+
if "WITH TIME ZONE" in l_sql and "WITH TIME ZONE" in r_sql:
|
|
871
892
|
range_type = "tstzrange"
|
|
872
893
|
else:
|
|
873
894
|
range_type = "tsrange"
|
|
@@ -879,7 +900,7 @@ class SQLInterpreter(Interpreter):
|
|
|
879
900
|
)
|
|
880
901
|
else:
|
|
881
902
|
raise EvaluationError(
|
|
882
|
-
f"Range operator is not supported for types '{l_type}' and '{r_type}'",
|
|
903
|
+
f"Range operator is not supported for types '{l_type}' and '{r_type}'.",
|
|
883
904
|
line=l_tree.meta.line,
|
|
884
905
|
column=l_tree.meta.column,
|
|
885
906
|
start_pos=l_tree.meta.start_pos,
|
|
@@ -898,12 +919,12 @@ class SQLInterpreter(Interpreter):
|
|
|
898
919
|
tuple,
|
|
899
920
|
)
|
|
900
921
|
|
|
901
|
-
def var_from_data(self, var: TokenWrapper) -> tuple[str, list
|
|
922
|
+
def var_from_data(self, var: TokenWrapper) -> tuple[str, list, Any]:
|
|
902
923
|
if var.real_value in self.data:
|
|
903
924
|
column, type_ = self.data[var.real_value]
|
|
904
925
|
return (f'"{column}"', [], type_)
|
|
905
926
|
raise EvaluationError(
|
|
906
|
-
f"The type of column '{var.real_value}' was not defined",
|
|
927
|
+
f"The type of column '{var.real_value}' was not defined.",
|
|
907
928
|
line=var.line,
|
|
908
929
|
column=var.column,
|
|
909
930
|
start_pos=var.start_pos,
|
|
@@ -912,68 +933,69 @@ class SQLInterpreter(Interpreter):
|
|
|
912
933
|
end_pos=var.end_pos,
|
|
913
934
|
)
|
|
914
935
|
|
|
915
|
-
def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, list
|
|
936
|
+
def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, list, Any]:
|
|
916
937
|
l_sql, l_params, l_type = self.visit(l_tree)
|
|
917
938
|
r_sql, r_params, r_type = self.visit(r_tree)
|
|
918
939
|
|
|
919
|
-
sql, type_ = binary_operation_sql(
|
|
940
|
+
sql, type_, switch = binary_operation_sql(
|
|
941
|
+
operator, l_sql, r_sql, l_type, r_type
|
|
942
|
+
)
|
|
943
|
+
params = l_params + r_params if not switch else r_params + l_params
|
|
920
944
|
|
|
921
|
-
return sql,
|
|
945
|
+
return sql, params, type_
|
|
922
946
|
|
|
923
|
-
def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
947
|
+
def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
924
948
|
return self._binary_operation("+", l_tree, r_tree)
|
|
925
949
|
|
|
926
|
-
def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
950
|
+
def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
927
951
|
return self._binary_operation("-", l_tree, r_tree)
|
|
928
952
|
|
|
929
|
-
def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
953
|
+
def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
930
954
|
return self._binary_operation("*", l_tree, r_tree)
|
|
931
955
|
|
|
932
|
-
def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
956
|
+
def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
933
957
|
return self._binary_operation("/", l_tree, r_tree)
|
|
934
958
|
|
|
935
|
-
def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
959
|
+
def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
936
960
|
return self._binary_operation("%", l_tree, r_tree)
|
|
937
961
|
|
|
938
|
-
def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
962
|
+
def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
939
963
|
return self._binary_operation("==", l_tree, r_tree)
|
|
940
964
|
|
|
941
|
-
def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
965
|
+
def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
942
966
|
return self._binary_operation("in", l_tree, r_tree)
|
|
943
967
|
|
|
944
|
-
def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
968
|
+
def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
945
969
|
return self._binary_operation("=", l_tree, r_tree)
|
|
946
970
|
|
|
947
|
-
def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
971
|
+
def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
948
972
|
return self._binary_operation(">", l_tree, r_tree)
|
|
949
973
|
|
|
950
|
-
def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
974
|
+
def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
951
975
|
return self._binary_operation(">=", l_tree, r_tree)
|
|
952
976
|
|
|
953
|
-
def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
977
|
+
def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
954
978
|
return self._binary_operation("<", l_tree, r_tree)
|
|
955
979
|
|
|
956
|
-
def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
980
|
+
def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
957
981
|
return self._binary_operation("<=", l_tree, r_tree)
|
|
958
982
|
|
|
959
|
-
def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
983
|
+
def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
960
984
|
return self._binary_operation("or", l_tree, r_tree)
|
|
961
985
|
|
|
962
|
-
def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
986
|
+
def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
963
987
|
return self._binary_operation("and", l_tree, r_tree)
|
|
964
988
|
|
|
965
|
-
def not_op(self, tree: Tree) -> tuple[str, list
|
|
989
|
+
def not_op(self, tree: Tree) -> tuple[str, list, type[bool]]:
|
|
966
990
|
sql, params, type_ = self.visit(tree)
|
|
967
991
|
return f"NOT {sql}", params, type_
|
|
968
992
|
|
|
969
|
-
def contains_op(
|
|
970
|
-
self, l_tree: Tree, r_tree: Tree
|
|
971
|
-
) -> tuple[str, list[Any], type[bool]]:
|
|
993
|
+
def contains_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
972
994
|
return self._binary_operation("contains", l_tree, r_tree)
|
|
973
995
|
|
|
974
|
-
def exists_op(self, path: Token) -> tuple[str, list
|
|
996
|
+
def exists_op(self, path: Token) -> tuple[str, list, type[bool]]:
|
|
975
997
|
var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
|
|
976
|
-
return f"{var_sql}
|
|
998
|
+
return f"{var_sql} IS NOT NULL", [], bool
|
|
977
999
|
|
|
978
1000
|
def exists_with_default(self, path: Token, default: Any) -> Any:
|
|
979
1001
|
var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
|
|
@@ -987,14 +1009,21 @@ class SQLInterpreter(Interpreter):
|
|
|
987
1009
|
|
|
988
1010
|
args_sql = []
|
|
989
1011
|
params = []
|
|
1012
|
+
types = []
|
|
990
1013
|
|
|
991
1014
|
for arg in args.children if args else []:
|
|
992
|
-
sql, p,
|
|
1015
|
+
sql, p, type_ = self.visit(arg)
|
|
993
1016
|
args_sql.append(sql)
|
|
994
1017
|
params.extend(p)
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1018
|
+
types.append(type_)
|
|
1019
|
+
|
|
1020
|
+
if function_name == "now" and len(args_sql) == 0:
|
|
1021
|
+
return "localtimestamp", params, datetime
|
|
1022
|
+
if function_name in ("len", "length") and len(args_sql) == 1:
|
|
1023
|
+
if types[0] is list:
|
|
1024
|
+
return f"array_length({args_sql[0]}, 1)", params, int
|
|
1025
|
+
if types[0] is str:
|
|
1026
|
+
return f"length({args_sql[0]})", params, int
|
|
998
1027
|
raise EvaluationError(
|
|
999
1028
|
f"Function '{name.real_value}' was not found.",
|
|
1000
1029
|
line=name.line,
|
|
@@ -213,6 +213,68 @@ class TestExpressionTransformer:
|
|
|
213
213
|
assert result_data.children[0].real_value == "my_var"
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
class TestDefaultTimezone:
|
|
217
|
+
def test_naive_datetime_gets_default_timezone(self):
|
|
218
|
+
parser = Parser(default_timezone=timezone.utc)
|
|
219
|
+
|
|
220
|
+
result = parser.parse("2024-01-01T12:00:00").children[0].real_value
|
|
221
|
+
|
|
222
|
+
assert result.tzinfo == timezone.utc
|
|
223
|
+
assert str(result) == "2024-01-01 12:00:00+00:00"
|
|
224
|
+
|
|
225
|
+
result = parser.parse("2024-01-01").children[0].real_value
|
|
226
|
+
|
|
227
|
+
assert result.tzinfo == timezone.utc
|
|
228
|
+
assert str(result) == "2024-01-01 00:00:00+00:00"
|
|
229
|
+
|
|
230
|
+
def test_naive_datetime_with_custom_timezone(self):
|
|
231
|
+
custom_tz = timezone(timedelta(hours=2))
|
|
232
|
+
parser = Parser(default_timezone=custom_tz)
|
|
233
|
+
|
|
234
|
+
result = parser.parse("2024-01-01T12:00:00").children[0].real_value
|
|
235
|
+
|
|
236
|
+
assert result.tzinfo == custom_tz
|
|
237
|
+
assert str(result) == "2024-01-01 12:00:00+02:00"
|
|
238
|
+
|
|
239
|
+
result = parser.parse("2024-01-01").children[0].real_value
|
|
240
|
+
|
|
241
|
+
assert result.tzinfo == custom_tz
|
|
242
|
+
assert str(result) == "2024-01-01 00:00:00+02:00"
|
|
243
|
+
|
|
244
|
+
def test_datetime_with_timezone_not_overridden(self):
|
|
245
|
+
parser = Parser(default_timezone=timezone.utc)
|
|
246
|
+
|
|
247
|
+
result = parser.parse("2024-01-01T12:00:00+02:00").children[0].real_value
|
|
248
|
+
|
|
249
|
+
# Should keep original timezone
|
|
250
|
+
assert result.tzinfo.utcoffset(result) == timedelta(hours=2)
|
|
251
|
+
assert str(result) == "2024-01-01 12:00:00+02:00"
|
|
252
|
+
|
|
253
|
+
def test_no_default_timezone_keeps_naive_datetime(self):
|
|
254
|
+
parser = Parser(default_timezone=None)
|
|
255
|
+
|
|
256
|
+
result = parser.parse("2024-01-01T12:00:00").children[0].real_value
|
|
257
|
+
|
|
258
|
+
assert result.tzinfo is None
|
|
259
|
+
assert str(result) == "2024-01-01 12:00:00"
|
|
260
|
+
|
|
261
|
+
result = parser.parse("2024-01-01").children[0].real_value
|
|
262
|
+
|
|
263
|
+
assert result.tzinfo is None
|
|
264
|
+
assert str(result) == "2024-01-01 00:00:00"
|
|
265
|
+
|
|
266
|
+
def test_expression_transformer_default_timezone_direct(self, parser):
|
|
267
|
+
transformer = ExpressionTransformer(default_timezone=timezone.utc)
|
|
268
|
+
|
|
269
|
+
result = (
|
|
270
|
+
transformer.transform(parser.parse_only("2024-01-01T12:00:00"))
|
|
271
|
+
.children[0]
|
|
272
|
+
.real_value
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
assert result.tzinfo == timezone.utc
|
|
276
|
+
|
|
277
|
+
|
|
216
278
|
class TestFilter:
|
|
217
279
|
def parse_filter(self, parser, filter_, expression):
|
|
218
280
|
"""Helper to parse, evaluate atoms to Python object, then evaluate."""
|
|
@@ -522,6 +584,7 @@ class TestSQLInterpreter:
|
|
|
522
584
|
"c_int": ("c", int),
|
|
523
585
|
"d_int": ("d", int),
|
|
524
586
|
"Description": ("description", str),
|
|
587
|
+
"Category": ("category", list),
|
|
525
588
|
}
|
|
526
589
|
return sql.to_sql(tree, data)
|
|
527
590
|
|
|
@@ -530,9 +593,9 @@ class TestSQLInterpreter:
|
|
|
530
593
|
[
|
|
531
594
|
# Numbers.
|
|
532
595
|
("5", "%s", [5]),
|
|
533
|
-
("-12", "-%s", [12]),
|
|
596
|
+
("-12", "(-%s)", [12]),
|
|
534
597
|
("23.22", "%s", [23.22]),
|
|
535
|
-
("-0.224", "-%s", [0.224]),
|
|
598
|
+
("-0.224", "(-%s)", [0.224]),
|
|
536
599
|
("13e19", "%s", [1.3e20]),
|
|
537
600
|
# Datetime.
|
|
538
601
|
("2025-01-01", "%s::TIMESTAMP", [datetime(2025, 1, 1, 0, 0)]),
|
|
@@ -554,7 +617,7 @@ class TestSQLInterpreter:
|
|
|
554
617
|
),
|
|
555
618
|
# Timedelta.
|
|
556
619
|
("01:23:34", "%s", [timedelta(seconds=5014)]),
|
|
557
|
-
("-10D23:59:59", "-%s", [timedelta(days=10, seconds=86399)]),
|
|
620
|
+
("-10D23:59:59", "(-%s)", [timedelta(days=10, seconds=86399)]),
|
|
558
621
|
("99:59:59", "%s", [timedelta(days=4, seconds=14399)]),
|
|
559
622
|
# Strings.
|
|
560
623
|
("''", "%s", [""]),
|
|
@@ -659,6 +722,133 @@ class TestSQLInterpreter:
|
|
|
659
722
|
),
|
|
660
723
|
[],
|
|
661
724
|
),
|
|
725
|
+
# Test arithmetic operations.
|
|
726
|
+
(
|
|
727
|
+
"(10 + 12 - 7) * (15 / (8 % 3)) - (-5)",
|
|
728
|
+
"(((%s + %s) - %s) * %s / (%s % %s) - (-%s))",
|
|
729
|
+
[10, 12, 7, 15, 8, 3, 5],
|
|
730
|
+
),
|
|
731
|
+
# Test comparisons.
|
|
732
|
+
(
|
|
733
|
+
"10 > 12 and 12 < 20 and (17 >= 12 or 12 <= 1) and not 53 == 13",
|
|
734
|
+
"%s > %s AND %s < %s AND (%s >= %s OR %s <= %s) AND NOT %s = %s",
|
|
735
|
+
[10, 12, 12, 20, 17, 12, 12, 1, 53, 13],
|
|
736
|
+
),
|
|
737
|
+
# Test exists operators.
|
|
738
|
+
("a_int??", '"a" IS NOT NULL', []),
|
|
739
|
+
("a_int??14", 'COALESCE("a", %s)', [14]),
|
|
740
|
+
# Test contains operator.
|
|
741
|
+
("'abcdfg' contains 'bcd'", "position(%s in %s)>0", ["bcd", "abcdfg"]),
|
|
742
|
+
(
|
|
743
|
+
"Category contains 'Attempt'",
|
|
744
|
+
"position(%s in array_to_string(\"category\", ','))>0",
|
|
745
|
+
["Attempt"],
|
|
746
|
+
),
|
|
747
|
+
(
|
|
748
|
+
"['abcdfg', 'qwerty', 'azerty'] contains 'bcd'",
|
|
749
|
+
"position(%s in array_to_string(ARRAY[%s, %s, %s], ','))>0",
|
|
750
|
+
["bcd", "abcdfg", "qwerty", "azerty"],
|
|
751
|
+
),
|
|
752
|
+
# Test = operator.
|
|
753
|
+
("1 = 1", "%s = %s", [1, 1]),
|
|
754
|
+
("1 = [1, 2, 3]", "%s = ANY(ARRAY[%s, %s, %s])", [1, 1, 2, 3]),
|
|
755
|
+
("[1, 2, 3] = 1", "%s = ANY(ARRAY[%s, %s, %s])", [1, 1, 2, 3]),
|
|
756
|
+
("[1, 2] = [3, 4]", "ARRAY[%s, %s] && ARRAY[%s, %s]", [1, 2, 3, 4]),
|
|
757
|
+
(
|
|
758
|
+
"2 = 1..10",
|
|
759
|
+
(
|
|
760
|
+
"CASE WHEN %s < %s "
|
|
761
|
+
"THEN int4range(%s, %s, '[]') "
|
|
762
|
+
"ELSE int4range(%s, %s, '[]') END @> %s"
|
|
763
|
+
),
|
|
764
|
+
[1, 10, 1, 10, 10, 1, 2],
|
|
765
|
+
),
|
|
766
|
+
(
|
|
767
|
+
"1..10 = 2",
|
|
768
|
+
(
|
|
769
|
+
"CASE WHEN %s < %s "
|
|
770
|
+
"THEN int4range(%s, %s, '[]') "
|
|
771
|
+
"ELSE int4range(%s, %s, '[]') END @> %s"
|
|
772
|
+
),
|
|
773
|
+
[1, 10, 1, 10, 10, 1, 2],
|
|
774
|
+
),
|
|
775
|
+
(
|
|
776
|
+
"a_int..b_int = c_int..d_int",
|
|
777
|
+
(
|
|
778
|
+
'CASE WHEN "a" < "b" '
|
|
779
|
+
'THEN int4range("a", "b", \'[]\') '
|
|
780
|
+
'ELSE int4range("b", "a", \'[]\') END '
|
|
781
|
+
'&& CASE WHEN "c" < "d" '
|
|
782
|
+
'THEN int4range("c", "d", \'[]\') '
|
|
783
|
+
'ELSE int4range("d", "c", \'[]\') END'
|
|
784
|
+
),
|
|
785
|
+
[],
|
|
786
|
+
),
|
|
787
|
+
(
|
|
788
|
+
"10.10.10.10 = 10.10.10.10",
|
|
789
|
+
"%s::ipaddress && %s::ipaddress",
|
|
790
|
+
[from_str("10.10.10.10"), from_str("10.10.10.10")],
|
|
791
|
+
),
|
|
792
|
+
(
|
|
793
|
+
"10.10.10.10 = 10.10.10.0 .. 10.10.10.20",
|
|
794
|
+
"%s::ipaddress && iprange(%s::ipaddress, %s::ipaddress)",
|
|
795
|
+
[
|
|
796
|
+
from_str("10.10.10.10"),
|
|
797
|
+
from_str("10.10.10.0"),
|
|
798
|
+
from_str("10.10.10.20"),
|
|
799
|
+
],
|
|
800
|
+
),
|
|
801
|
+
(
|
|
802
|
+
"192.168.0.5 = 192.168.0.0-192.168.0.255",
|
|
803
|
+
"%s::ipaddress && %s::iprange",
|
|
804
|
+
[from_str("192.168.0.5"), from_str("192.168.0.0-192.168.0.255")],
|
|
805
|
+
),
|
|
806
|
+
(
|
|
807
|
+
"192.168.0.12 = 192.168.1.0/24",
|
|
808
|
+
"%s::ipaddress && %s::iprange",
|
|
809
|
+
[from_str("192.168.0.12"), from_str("192.168.1.0/24")],
|
|
810
|
+
),
|
|
811
|
+
(
|
|
812
|
+
"10.10.10.10-10.10.10.20 = 10.10.10.15-10.10.10.25",
|
|
813
|
+
"%s::iprange && %s::iprange",
|
|
814
|
+
[
|
|
815
|
+
from_str("10.10.10.10-10.10.10.20"),
|
|
816
|
+
from_str("10.10.10.15-10.10.10.25"),
|
|
817
|
+
],
|
|
818
|
+
),
|
|
819
|
+
(
|
|
820
|
+
"10.10.10.10-10.10.10.20 = 192.168.0.0/16",
|
|
821
|
+
"%s::iprange && %s::iprange",
|
|
822
|
+
[from_str("10.10.10.10-10.10.10.20"), from_str("192.168.0.0/16")],
|
|
823
|
+
),
|
|
824
|
+
(
|
|
825
|
+
"192.168.1.0/24 = 192.168.0.0/16",
|
|
826
|
+
"%s::iprange && %s::iprange",
|
|
827
|
+
[from_str("192.168.1.0/24"), from_str("192.168.0.0/16")],
|
|
828
|
+
),
|
|
829
|
+
(
|
|
830
|
+
"192.168.1.10 = [192.168.0.0/24, 192.168.0.0/16]",
|
|
831
|
+
("%s::ipaddress && ANY(ARRAY[%s::iprange, %s::iprange])"),
|
|
832
|
+
[
|
|
833
|
+
from_str("192.168.1.10"),
|
|
834
|
+
from_str("192.168.0.0/24"),
|
|
835
|
+
from_str("192.168.0.0/16"),
|
|
836
|
+
],
|
|
837
|
+
),
|
|
838
|
+
# Test functions.
|
|
839
|
+
(
|
|
840
|
+
"now() > 2025-12-12T12:34:56Z",
|
|
841
|
+
"localtimestamp > %s::TIMESTAMP WITH TIME ZONE",
|
|
842
|
+
[datetime(2025, 12, 12, 12, 34, 56, tzinfo=timezone.utc)],
|
|
843
|
+
),
|
|
844
|
+
("length('')", "length(%s)", [""]),
|
|
845
|
+
("length('abcde')", "length(%s)", ["abcde"]),
|
|
846
|
+
(
|
|
847
|
+
"length([1, 2, 3]) == 3",
|
|
848
|
+
"array_length(ARRAY[%s, %s, %s], 1) = %s",
|
|
849
|
+
[1, 2, 3, 3],
|
|
850
|
+
),
|
|
851
|
+
("len(Category) > 1", 'array_length("category", 1) > %s', [1]),
|
|
662
852
|
],
|
|
663
853
|
)
|
|
664
854
|
def test_parse_sql(self, parser, sql, expr, expected_sql, expected_params):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|