ransacklib 1.1.0.dev4__tar.gz → 1.1.0.dev6__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.dev4/ransacklib.egg-info → ransacklib-1.1.0.dev6}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/pyproject.toml +2 -1
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransack/operator.py +55 -47
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransack/transformer.py +44 -38
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6/ransacklib.egg-info}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/tests/test_transformer.py +285 -2
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/LICENSE +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/README.rst +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransack/__init__.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransack/exceptions.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransack/function.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransack/parser.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransack/py.typed +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransacklib.egg-info/SOURCES.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransacklib.egg-info/dependency_links.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransacklib.egg-info/requires.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/ransacklib.egg-info/top_level.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/setup.cfg +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/tests/test_operator.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev6}/tests/test_parser.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ransacklib"
|
|
7
|
-
version = "1.1.0.
|
|
7
|
+
version = "1.1.0.dev6"
|
|
8
8
|
description = "A modern, extensible language for manipulation with structured data"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
readme = "README.rst"
|
|
@@ -83,4 +83,5 @@ select = [
|
|
|
83
83
|
|
|
84
84
|
ignore = [
|
|
85
85
|
"PLR0913", # too-many-arguments
|
|
86
|
+
"DTZ001", # call-datetime-without-tzinfo
|
|
86
87
|
]
|
|
@@ -500,63 +500,65 @@ 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
|
-
def _get_comp_dict_sql(
|
|
503
|
+
def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any, bool]:
|
|
504
|
+
def _get_comp_dict_sql(
|
|
505
|
+
op: str, tuple_repr_f
|
|
506
|
+
) -> dict[tuple[Operand, Operand], tuple[str, bool]]:
|
|
505
507
|
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})",
|
|
508
|
+
(Number, Number): (f"{l_sql} {op} {r_sql}", False),
|
|
509
|
+
(Number, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
510
|
+
(Number, tuple): (f"{l_sql} {op} {tuple_repr_f}({r_sql})", False),
|
|
511
|
+
(datetime, datetime): (f"{l_sql} {op} {r_sql}", False),
|
|
512
|
+
(datetime, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
513
|
+
(datetime, tuple): (f"{r_sql} {op} {tuple_repr_f}({l_sql})", True),
|
|
514
|
+
(timedelta, timedelta): (f"{l_sql} {op} {r_sql}", False),
|
|
515
|
+
(timedelta, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
516
|
+
("ip", "ip"): (f"{l_sql} {op} {r_sql}", False),
|
|
517
|
+
("ip", list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
516
518
|
# (list, list): f"{l_sql} && {r_sql}",
|
|
517
519
|
# (tuple, tuple): f"upper({l_sql}) > lower({r_sql})",
|
|
518
520
|
}
|
|
519
521
|
|
|
520
522
|
d: dict[str, dict] = {
|
|
521
523
|
"=": {
|
|
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}",
|
|
524
|
+
(Number, Number): (f"{l_sql} = {r_sql}", False),
|
|
525
|
+
(Number, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
526
|
+
(Number, tuple): (f"{r_sql} @> {l_sql}", True),
|
|
527
|
+
(datetime, datetime): (f"{l_sql} = {r_sql}", False),
|
|
528
|
+
(datetime, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
529
|
+
(datetime, tuple): (f"{r_sql} @> {l_sql}", True),
|
|
530
|
+
(timedelta, timedelta): (f"{l_sql} = {r_sql}", False),
|
|
531
|
+
(timedelta, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
532
|
+
(str, str): (f"{l_sql} = {r_sql}", False),
|
|
533
|
+
(str, list): (f"{l_sql} = ANY({r_sql})", False),
|
|
534
|
+
("ip", "ip"): (f"{l_sql} && {r_sql}", False),
|
|
535
|
+
("ip", list): (f"{l_sql} && ANY({r_sql})", False),
|
|
536
|
+
(list, list): (f"{l_sql} && {r_sql}", False),
|
|
537
|
+
(tuple, tuple): (f"{l_sql} && {r_sql}", False),
|
|
536
538
|
},
|
|
537
539
|
">": _get_comp_dict_sql(">", "lower")
|
|
538
|
-
| {(tuple, tuple): f"upper({l_sql}) > lower({r_sql})"},
|
|
540
|
+
| {(tuple, tuple): (f"upper({l_sql}) > lower({r_sql})", False)},
|
|
539
541
|
">=": _get_comp_dict_sql(">=", "lower")
|
|
540
|
-
| {(tuple, tuple): f"upper({l_sql}) >= lower({r_sql})"},
|
|
542
|
+
| {(tuple, tuple): (f"upper({l_sql}) >= lower({r_sql})", False)},
|
|
541
543
|
"<": _get_comp_dict_sql("<", "upper")
|
|
542
|
-
| {(tuple, tuple): f"lower({l_sql}) < upper({r_sql})"},
|
|
544
|
+
| {(tuple, tuple): (f"lower({l_sql}) < upper({r_sql})", False)},
|
|
543
545
|
"<=": _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": {(str, str): f"position({r_sql} in {l_sql})>0"},
|
|
546
|
+
| {(tuple, tuple): (f"lower({l_sql}) <= upper({r_sql})", False)},
|
|
547
|
+
"and": {(bool, bool): (f"{l_sql} AND {r_sql}", False)},
|
|
548
|
+
"or": {(bool, bool): (f"({l_sql} OR {r_sql})", False)},
|
|
549
|
+
"contains": {(str, str): (f"position({r_sql} in {l_sql})>0", True)},
|
|
548
550
|
}
|
|
549
551
|
d2: dict[str, dict] = {
|
|
550
552
|
"+": {
|
|
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),
|
|
553
|
+
(Number, Number): (f"({l_sql} + {r_sql})", Number),
|
|
554
|
+
(datetime, timedelta): (f"({l_sql} + {r_sql})", datetime),
|
|
555
|
+
(timedelta, timedelta): (f"({l_sql} + {r_sql})", timedelta),
|
|
554
556
|
},
|
|
555
557
|
"-": {
|
|
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),
|
|
558
|
+
(Number, Number): (f"({l_sql} - {r_sql})", Number),
|
|
559
|
+
(datetime, timedelta): (f"({l_sql} - {r_sql})", datetime),
|
|
560
|
+
(timedelta, timedelta): (f"({l_sql} - {r_sql})", timedelta),
|
|
561
|
+
(datetime, datetime): (f"({l_sql} - {r_sql})", timedelta),
|
|
560
562
|
},
|
|
561
563
|
"*": {
|
|
562
564
|
(timedelta, Number): (f"{l_sql} * {r_sql}", timedelta),
|
|
@@ -567,21 +569,24 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any]:
|
|
|
567
569
|
(Number, Number): (f"{l_sql} / {r_sql}", Number),
|
|
568
570
|
},
|
|
569
571
|
"%": {
|
|
570
|
-
(Number, Number): (f"{l_sql} % {r_sql}", Number),
|
|
572
|
+
(Number, Number): (f"({l_sql} % {r_sql})", Number),
|
|
571
573
|
},
|
|
572
574
|
}
|
|
573
575
|
if op == "==":
|
|
574
|
-
return (f"{l_sql} = {r_sql}", bool)
|
|
576
|
+
return (f"{l_sql} = {r_sql}", bool, False)
|
|
575
577
|
if op == "in":
|
|
576
|
-
|
|
578
|
+
sql, switch = d["="][(t1, t2)]
|
|
579
|
+
return sql, bool, switch
|
|
577
580
|
if op in d2:
|
|
578
|
-
|
|
579
|
-
|
|
581
|
+
sql, type_ = d2[op][(t1, t2)]
|
|
582
|
+
return sql, type_, False
|
|
583
|
+
sql, switch = d[op][(t1, t2)]
|
|
584
|
+
return sql, bool, switch
|
|
580
585
|
|
|
581
586
|
|
|
582
587
|
def binary_operation_sql(
|
|
583
588
|
operator: str, left: str, right: str, l_type: Any, r_type: Any
|
|
584
|
-
) -> tuple[str, Any]:
|
|
589
|
+
) -> tuple[str, bool, Any]:
|
|
585
590
|
# Normalize numeric types
|
|
586
591
|
l_type = Number if l_type in (int, float) else l_type
|
|
587
592
|
r_type = Number if r_type in (int, float) else r_type
|
|
@@ -595,7 +600,10 @@ def binary_operation_sql(
|
|
|
595
600
|
return _operator_map_sql(operator, left, right, l_type, r_type)
|
|
596
601
|
except KeyError:
|
|
597
602
|
try:
|
|
598
|
-
|
|
603
|
+
sql, type_, switch = _operator_map_sql(
|
|
604
|
+
operator, right, left, r_type, l_type
|
|
605
|
+
)
|
|
606
|
+
return sql, type_, not switch
|
|
599
607
|
except KeyError:
|
|
600
608
|
pass
|
|
601
609
|
raise OperatorNotFoundError(operator, (l_type, r_type), (left, right)) from None
|
|
@@ -828,19 +828,23 @@ class SQLInterpreter(Interpreter):
|
|
|
828
828
|
self.data = {}
|
|
829
829
|
return result, params
|
|
830
830
|
|
|
831
|
-
def number(self, token: TokenWrapper) -> tuple[str, list
|
|
831
|
+
def number(self, token: TokenWrapper) -> tuple[str, list, type[int | float]]:
|
|
832
832
|
return ("%s", [token.real_value], type(token.real_value))
|
|
833
833
|
|
|
834
|
-
def datetime(self, token: TokenWrapper) -> tuple[str, list
|
|
835
|
-
|
|
834
|
+
def datetime(self, token: TokenWrapper) -> tuple[str, list, type[datetime]]:
|
|
835
|
+
if token.real_value.tzinfo is not None:
|
|
836
|
+
cast = "TIMESTAMP WITH TIME ZONE"
|
|
837
|
+
else:
|
|
838
|
+
cast = "TIMESTAMP"
|
|
839
|
+
return (f"%s::{cast}", [token.real_value], type(token.real_value))
|
|
836
840
|
|
|
837
|
-
def timedelta_(self, token: TokenWrapper) -> tuple[str, list
|
|
841
|
+
def timedelta_(self, token: TokenWrapper) -> tuple[str, list, type[timedelta]]:
|
|
838
842
|
return ("%s", [token.real_value], type(token.real_value))
|
|
839
843
|
|
|
840
|
-
def string_(self, token: TokenWrapper) -> tuple[str, list
|
|
844
|
+
def string_(self, token: TokenWrapper) -> tuple[str, list, type[str]]:
|
|
841
845
|
return ("%s", [token.real_value], type(token.real_value))
|
|
842
846
|
|
|
843
|
-
def ip(self, token: TokenWrapper) -> tuple[str, list
|
|
847
|
+
def ip(self, token: TokenWrapper) -> tuple[str, list, type[IP]]:
|
|
844
848
|
match token.real_value:
|
|
845
849
|
case IP4() | IP6():
|
|
846
850
|
cast = "ipaddress"
|
|
@@ -848,18 +852,16 @@ class SQLInterpreter(Interpreter):
|
|
|
848
852
|
cast = "iprange"
|
|
849
853
|
return (f"%s::{cast}", [token.real_value], type(token.real_value))
|
|
850
854
|
|
|
851
|
-
def neg(self, tree: Tree) -> tuple[str, list
|
|
855
|
+
def neg(self, tree: Tree) -> tuple[str, list, Any]:
|
|
852
856
|
sql, params, type_ = self.visit(tree)
|
|
853
|
-
return (f"-{sql}", params, type_)
|
|
857
|
+
return (f"(-{sql})", params, type_)
|
|
854
858
|
|
|
855
859
|
def range_op(
|
|
856
860
|
self, l_tree: Tree, r_tree: Tree
|
|
857
|
-
) -> tuple[str, list
|
|
861
|
+
) -> tuple[str, list, type[tuple] | type[IP]]:
|
|
858
862
|
l_sql, l_params, l_type = self.visit(l_tree)
|
|
859
863
|
r_sql, r_params, r_type = self.visit(r_tree)
|
|
860
864
|
|
|
861
|
-
params = l_params + r_params
|
|
862
|
-
|
|
863
865
|
if issubclass(l_type, int) and issubclass(r_type, int):
|
|
864
866
|
range_type = "int4range"
|
|
865
867
|
elif issubclass(l_type, float) and issubclass(r_type, float):
|
|
@@ -872,7 +874,7 @@ class SQLInterpreter(Interpreter):
|
|
|
872
874
|
elif issubclass(l_type, (IP4, IP6)) and issubclass(r_type, (IP4, IP6)):
|
|
873
875
|
return (
|
|
874
876
|
f"iprange({l_sql}, {r_sql})",
|
|
875
|
-
|
|
877
|
+
l_params + r_params,
|
|
876
878
|
IP4Range if issubclass(l_type, IP4) else IP6Range,
|
|
877
879
|
)
|
|
878
880
|
else:
|
|
@@ -892,11 +894,11 @@ class SQLInterpreter(Interpreter):
|
|
|
892
894
|
f"THEN {range_type}({l_sql}, {r_sql}, '[]') "
|
|
893
895
|
f"ELSE {range_type}({r_sql}, {l_sql}, '[]') END"
|
|
894
896
|
),
|
|
895
|
-
|
|
897
|
+
l_params + r_params + l_params + r_params + r_params + l_params,
|
|
896
898
|
tuple,
|
|
897
899
|
)
|
|
898
900
|
|
|
899
|
-
def var_from_data(self, var: TokenWrapper) -> tuple[str, list
|
|
901
|
+
def var_from_data(self, var: TokenWrapper) -> tuple[str, list, Any]:
|
|
900
902
|
if var.real_value in self.data:
|
|
901
903
|
column, type_ = self.data[var.real_value]
|
|
902
904
|
return (f'"{column}"', [], type_)
|
|
@@ -910,68 +912,69 @@ class SQLInterpreter(Interpreter):
|
|
|
910
912
|
end_pos=var.end_pos,
|
|
911
913
|
)
|
|
912
914
|
|
|
913
|
-
def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, list
|
|
915
|
+
def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, list, Any]:
|
|
914
916
|
l_sql, l_params, l_type = self.visit(l_tree)
|
|
915
917
|
r_sql, r_params, r_type = self.visit(r_tree)
|
|
916
918
|
|
|
917
|
-
sql, type_ = binary_operation_sql(
|
|
919
|
+
sql, type_, switch = binary_operation_sql(
|
|
920
|
+
operator, l_sql, r_sql, l_type, r_type
|
|
921
|
+
)
|
|
922
|
+
params = l_params + r_params if not switch else r_params + l_params
|
|
918
923
|
|
|
919
|
-
return sql,
|
|
924
|
+
return sql, params, type_
|
|
920
925
|
|
|
921
|
-
def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
926
|
+
def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
922
927
|
return self._binary_operation("+", l_tree, r_tree)
|
|
923
928
|
|
|
924
|
-
def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
929
|
+
def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
925
930
|
return self._binary_operation("-", l_tree, r_tree)
|
|
926
931
|
|
|
927
|
-
def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
932
|
+
def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
928
933
|
return self._binary_operation("*", l_tree, r_tree)
|
|
929
934
|
|
|
930
|
-
def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
935
|
+
def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
931
936
|
return self._binary_operation("/", l_tree, r_tree)
|
|
932
937
|
|
|
933
|
-
def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
938
|
+
def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
|
|
934
939
|
return self._binary_operation("%", l_tree, r_tree)
|
|
935
940
|
|
|
936
|
-
def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
941
|
+
def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
937
942
|
return self._binary_operation("==", l_tree, r_tree)
|
|
938
943
|
|
|
939
|
-
def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
944
|
+
def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
940
945
|
return self._binary_operation("in", l_tree, r_tree)
|
|
941
946
|
|
|
942
|
-
def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
947
|
+
def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
943
948
|
return self._binary_operation("=", l_tree, r_tree)
|
|
944
949
|
|
|
945
|
-
def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
950
|
+
def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
946
951
|
return self._binary_operation(">", l_tree, r_tree)
|
|
947
952
|
|
|
948
|
-
def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
953
|
+
def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
949
954
|
return self._binary_operation(">=", l_tree, r_tree)
|
|
950
955
|
|
|
951
|
-
def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
956
|
+
def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
952
957
|
return self._binary_operation("<", l_tree, r_tree)
|
|
953
958
|
|
|
954
|
-
def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
959
|
+
def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
955
960
|
return self._binary_operation("<=", l_tree, r_tree)
|
|
956
961
|
|
|
957
|
-
def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
962
|
+
def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
958
963
|
return self._binary_operation("or", l_tree, r_tree)
|
|
959
964
|
|
|
960
|
-
def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list
|
|
965
|
+
def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
961
966
|
return self._binary_operation("and", l_tree, r_tree)
|
|
962
967
|
|
|
963
|
-
def not_op(self, tree: Tree) -> tuple[str, list
|
|
968
|
+
def not_op(self, tree: Tree) -> tuple[str, list, type[bool]]:
|
|
964
969
|
sql, params, type_ = self.visit(tree)
|
|
965
970
|
return f"NOT {sql}", params, type_
|
|
966
971
|
|
|
967
|
-
def contains_op(
|
|
968
|
-
self, l_tree: Tree, r_tree: Tree
|
|
969
|
-
) -> tuple[str, list[Any], type[bool]]:
|
|
972
|
+
def contains_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
|
|
970
973
|
return self._binary_operation("contains", l_tree, r_tree)
|
|
971
974
|
|
|
972
|
-
def exists_op(self, path: Token) -> tuple[str, list
|
|
975
|
+
def exists_op(self, path: Token) -> tuple[str, list, type[bool]]:
|
|
973
976
|
var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
|
|
974
|
-
return f"{var_sql}
|
|
977
|
+
return f"{var_sql} IS NOT NULL", [], bool
|
|
975
978
|
|
|
976
979
|
def exists_with_default(self, path: Token, default: Any) -> Any:
|
|
977
980
|
var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
|
|
@@ -1004,7 +1007,7 @@ class SQLInterpreter(Interpreter):
|
|
|
1004
1007
|
)
|
|
1005
1008
|
|
|
1006
1009
|
@v_args(tree=True)
|
|
1007
|
-
def list(self, data):
|
|
1010
|
+
def list(self, data: Tree) -> tuple[str, list, Any]:
|
|
1008
1011
|
parts = []
|
|
1009
1012
|
params = []
|
|
1010
1013
|
|
|
@@ -1015,4 +1018,7 @@ class SQLInterpreter(Interpreter):
|
|
|
1015
1018
|
parts.append(sql)
|
|
1016
1019
|
params.extend(p)
|
|
1017
1020
|
|
|
1021
|
+
if not parts:
|
|
1022
|
+
return "'{}'", [], list
|
|
1023
|
+
|
|
1018
1024
|
return f"ARRAY[{', '.join(parts)}]", params, list
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
-
from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
|
|
4
|
+
from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range, from_str
|
|
5
5
|
from lark import Token
|
|
6
6
|
|
|
7
7
|
from ransack.exceptions import RansackError, ShapeError
|
|
@@ -9,6 +9,7 @@ from ransack.parser import Parser
|
|
|
9
9
|
from ransack.transformer import (
|
|
10
10
|
ExpressionTransformer,
|
|
11
11
|
Filter,
|
|
12
|
+
SQLInterpreter,
|
|
12
13
|
TokenWrapper,
|
|
13
14
|
get_values,
|
|
14
15
|
)
|
|
@@ -29,6 +30,11 @@ def expr_transformer():
|
|
|
29
30
|
return ExpressionTransformer()
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def sql():
|
|
35
|
+
return SQLInterpreter()
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
sample_data = {
|
|
33
39
|
"Category": ["Intrusion.UserCompromise"],
|
|
34
40
|
"ConnCount": 1,
|
|
@@ -505,3 +511,280 @@ class TestFilter:
|
|
|
505
511
|
exp_tree = expr_transformer.transform(parser.parse_only("my_var == 14"))
|
|
506
512
|
res = filter_.transform(exp_tree)
|
|
507
513
|
assert res
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class TestSQLInterpreter:
|
|
517
|
+
def parse(self, parser, sql, expression):
|
|
518
|
+
tree = parser.parse(expression)
|
|
519
|
+
data = {
|
|
520
|
+
"a_int": ("a", int),
|
|
521
|
+
"b_int": ("b", int),
|
|
522
|
+
"c_int": ("c", int),
|
|
523
|
+
"d_int": ("d", int),
|
|
524
|
+
"Description": ("description", str),
|
|
525
|
+
}
|
|
526
|
+
return sql.to_sql(tree, data)
|
|
527
|
+
|
|
528
|
+
@pytest.mark.parametrize(
|
|
529
|
+
("expr", "expected_sql", "expected_params"),
|
|
530
|
+
[
|
|
531
|
+
# Numbers.
|
|
532
|
+
("5", "%s", [5]),
|
|
533
|
+
("-12", "(-%s)", [12]),
|
|
534
|
+
("23.22", "%s", [23.22]),
|
|
535
|
+
("-0.224", "(-%s)", [0.224]),
|
|
536
|
+
("13e19", "%s", [1.3e20]),
|
|
537
|
+
# Datetime.
|
|
538
|
+
("2025-01-01", "%s::TIMESTAMP", [datetime(2025, 1, 1, 0, 0)]),
|
|
539
|
+
(
|
|
540
|
+
"2025-01-01T22:14:24.142+02:00",
|
|
541
|
+
"%s::TIMESTAMP WITH TIME ZONE",
|
|
542
|
+
[
|
|
543
|
+
datetime(
|
|
544
|
+
2025,
|
|
545
|
+
1,
|
|
546
|
+
1,
|
|
547
|
+
22,
|
|
548
|
+
14,
|
|
549
|
+
24,
|
|
550
|
+
142000,
|
|
551
|
+
tzinfo=timezone(timedelta(seconds=7200)),
|
|
552
|
+
)
|
|
553
|
+
],
|
|
554
|
+
),
|
|
555
|
+
# Timedelta.
|
|
556
|
+
("01:23:34", "%s", [timedelta(seconds=5014)]),
|
|
557
|
+
("-10D23:59:59", "(-%s)", [timedelta(days=10, seconds=86399)]),
|
|
558
|
+
("99:59:59", "%s", [timedelta(days=4, seconds=14399)]),
|
|
559
|
+
# Strings.
|
|
560
|
+
("''", "%s", [""]),
|
|
561
|
+
('"string"', "%s", ["string"]),
|
|
562
|
+
('"\';DROP TABLE USERS"', "%s", ["';DROP TABLE USERS"]),
|
|
563
|
+
# IPs.
|
|
564
|
+
("192.168.0.0", "%s::ipaddress", [from_str("192.168.0.0")]),
|
|
565
|
+
("192.168.0.0/16", "%s::iprange", [from_str("192.168.0.0/16")]),
|
|
566
|
+
(
|
|
567
|
+
"192.168.0.10-192.168.0.50",
|
|
568
|
+
"%s::iprange",
|
|
569
|
+
[from_str("192.168.0.10-192.168.0.50")],
|
|
570
|
+
),
|
|
571
|
+
("fd00::1", "%s::ipaddress", [from_str("fd00::1")]),
|
|
572
|
+
("fd00::/8", "%s::iprange", [from_str("fd00::/8")]),
|
|
573
|
+
("fd00::1-fd00::10", "%s::iprange", [from_str("fd00::1-fd00::10")]),
|
|
574
|
+
# List.
|
|
575
|
+
("[1, 2, 3]", "ARRAY[%s, %s, %s]", [1, 2, 3]),
|
|
576
|
+
("[01:23:34]", "ARRAY[%s]", [timedelta(seconds=5014)]),
|
|
577
|
+
("[]", "'{}'", []),
|
|
578
|
+
# Ranges.
|
|
579
|
+
(
|
|
580
|
+
"100..1",
|
|
581
|
+
(
|
|
582
|
+
"CASE WHEN %s < %s "
|
|
583
|
+
"THEN int4range(%s, %s, '[]') "
|
|
584
|
+
"ELSE int4range(%s, %s, '[]') END"
|
|
585
|
+
),
|
|
586
|
+
[100, 1, 100, 1, 1, 100],
|
|
587
|
+
),
|
|
588
|
+
(
|
|
589
|
+
"100.12..1.14",
|
|
590
|
+
(
|
|
591
|
+
"CASE WHEN %s < %s "
|
|
592
|
+
"THEN numrange(%s, %s, '[]') "
|
|
593
|
+
"ELSE numrange(%s, %s, '[]') END"
|
|
594
|
+
),
|
|
595
|
+
[100.12, 1.14, 100.12, 1.14, 1.14, 100.12],
|
|
596
|
+
),
|
|
597
|
+
(
|
|
598
|
+
"2024-01-01T00:00:00+00:00..2024-12-31T00:00:00Z",
|
|
599
|
+
(
|
|
600
|
+
"CASE WHEN %s::TIMESTAMP WITH TIME ZONE "
|
|
601
|
+
"< %s::TIMESTAMP WITH TIME ZONE THEN "
|
|
602
|
+
"tstzrange(%s::TIMESTAMP WITH TIME ZONE, "
|
|
603
|
+
"%s::TIMESTAMP WITH TIME ZONE, '[]') ELSE "
|
|
604
|
+
"tstzrange(%s::TIMESTAMP WITH TIME ZONE, "
|
|
605
|
+
"%s::TIMESTAMP WITH TIME ZONE, '[]') END"
|
|
606
|
+
),
|
|
607
|
+
[
|
|
608
|
+
datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc),
|
|
609
|
+
datetime(2024, 12, 31, 0, 0, tzinfo=timezone.utc),
|
|
610
|
+
datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc),
|
|
611
|
+
datetime(2024, 12, 31, 0, 0, tzinfo=timezone.utc),
|
|
612
|
+
datetime(2024, 12, 31, 0, 0, tzinfo=timezone.utc),
|
|
613
|
+
datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc),
|
|
614
|
+
],
|
|
615
|
+
),
|
|
616
|
+
(
|
|
617
|
+
"2024-01-01T00:00:00..2024-12-31T00:00:00",
|
|
618
|
+
(
|
|
619
|
+
"CASE WHEN %s::TIMESTAMP "
|
|
620
|
+
"< %s::TIMESTAMP THEN "
|
|
621
|
+
"tsrange(%s::TIMESTAMP, "
|
|
622
|
+
"%s::TIMESTAMP, '[]') ELSE "
|
|
623
|
+
"tsrange(%s::TIMESTAMP, "
|
|
624
|
+
"%s::TIMESTAMP, '[]') END"
|
|
625
|
+
),
|
|
626
|
+
[
|
|
627
|
+
datetime(
|
|
628
|
+
2024,
|
|
629
|
+
1,
|
|
630
|
+
1,
|
|
631
|
+
0,
|
|
632
|
+
0,
|
|
633
|
+
),
|
|
634
|
+
datetime(2024, 12, 31, 0, 0),
|
|
635
|
+
datetime(2024, 1, 1, 0, 0),
|
|
636
|
+
datetime(2024, 12, 31, 0, 0),
|
|
637
|
+
datetime(2024, 12, 31, 0, 0),
|
|
638
|
+
datetime(2024, 1, 1, 0, 0),
|
|
639
|
+
],
|
|
640
|
+
),
|
|
641
|
+
(
|
|
642
|
+
"192.168.0.10..192.168.0.50",
|
|
643
|
+
"iprange(%s::ipaddress, %s::ipaddress)",
|
|
644
|
+
[from_str("192.168.0.10"), from_str("192.168.0.50")],
|
|
645
|
+
),
|
|
646
|
+
(
|
|
647
|
+
"fd00::1..fd00::10",
|
|
648
|
+
"iprange(%s::ipaddress, %s::ipaddress)",
|
|
649
|
+
[from_str("fd00::1"), from_str("fd00::10")],
|
|
650
|
+
),
|
|
651
|
+
# Test variable access.
|
|
652
|
+
("a_int", '"a"', []),
|
|
653
|
+
(
|
|
654
|
+
"a_int .. b_int",
|
|
655
|
+
(
|
|
656
|
+
'CASE WHEN "a" < "b" '
|
|
657
|
+
'THEN int4range("a", "b", \'[]\') '
|
|
658
|
+
'ELSE int4range("b", "a", \'[]\') END'
|
|
659
|
+
),
|
|
660
|
+
[],
|
|
661
|
+
),
|
|
662
|
+
# Test arithmetic operations.
|
|
663
|
+
(
|
|
664
|
+
"(10 + 12 - 7) * (15 / (8 % 3)) - (-5)",
|
|
665
|
+
"(((%s + %s) - %s) * %s / (%s % %s) - (-%s))",
|
|
666
|
+
[10, 12, 7, 15, 8, 3, 5],
|
|
667
|
+
),
|
|
668
|
+
# Test comparisons.
|
|
669
|
+
(
|
|
670
|
+
"10 > 12 and 12 < 20 and (17 >= 12 or 12 <= 1) and not 53 == 13",
|
|
671
|
+
"%s > %s AND %s < %s AND (%s >= %s OR %s <= %s) AND NOT %s = %s",
|
|
672
|
+
[10, 12, 12, 20, 17, 12, 12, 1, 53, 13],
|
|
673
|
+
),
|
|
674
|
+
# Test exists operators.
|
|
675
|
+
("a_int??", '"a" IS NOT NULL', []),
|
|
676
|
+
("a_int??14", 'COALESCE("a", %s)', [14]),
|
|
677
|
+
# Test contains operator.
|
|
678
|
+
("'abcdfg' contains 'bcd'", "position(%s in %s)>0", ["bcd", "abcdfg"]),
|
|
679
|
+
# Test = operator.
|
|
680
|
+
("1 = 1", "%s = %s", [1, 1]),
|
|
681
|
+
("1 = [1, 2, 3]", "%s = ANY(ARRAY[%s, %s, %s])", [1, 1, 2, 3]),
|
|
682
|
+
("[1, 2, 3] = 1", "%s = ANY(ARRAY[%s, %s, %s])", [1, 1, 2, 3]),
|
|
683
|
+
("[1, 2] = [3, 4]", "ARRAY[%s, %s] && ARRAY[%s, %s]", [1, 2, 3, 4]),
|
|
684
|
+
(
|
|
685
|
+
"2 = 1..10",
|
|
686
|
+
(
|
|
687
|
+
"CASE WHEN %s < %s "
|
|
688
|
+
"THEN int4range(%s, %s, '[]') "
|
|
689
|
+
"ELSE int4range(%s, %s, '[]') END @> %s"
|
|
690
|
+
),
|
|
691
|
+
[1, 10, 1, 10, 10, 1, 2],
|
|
692
|
+
),
|
|
693
|
+
(
|
|
694
|
+
"1..10 = 2",
|
|
695
|
+
(
|
|
696
|
+
"CASE WHEN %s < %s "
|
|
697
|
+
"THEN int4range(%s, %s, '[]') "
|
|
698
|
+
"ELSE int4range(%s, %s, '[]') END @> %s"
|
|
699
|
+
),
|
|
700
|
+
[1, 10, 1, 10, 10, 1, 2],
|
|
701
|
+
),
|
|
702
|
+
(
|
|
703
|
+
"a_int..b_int = c_int..d_int",
|
|
704
|
+
(
|
|
705
|
+
'CASE WHEN "a" < "b" '
|
|
706
|
+
'THEN int4range("a", "b", \'[]\') '
|
|
707
|
+
'ELSE int4range("b", "a", \'[]\') END '
|
|
708
|
+
'&& CASE WHEN "c" < "d" '
|
|
709
|
+
'THEN int4range("c", "d", \'[]\') '
|
|
710
|
+
'ELSE int4range("d", "c", \'[]\') END'
|
|
711
|
+
),
|
|
712
|
+
[],
|
|
713
|
+
),
|
|
714
|
+
(
|
|
715
|
+
"10.10.10.10 = 10.10.10.10",
|
|
716
|
+
"%s::ipaddress && %s::ipaddress",
|
|
717
|
+
[from_str("10.10.10.10"), from_str("10.10.10.10")],
|
|
718
|
+
),
|
|
719
|
+
(
|
|
720
|
+
"10.10.10.10 = 10.10.10.0 .. 10.10.10.20",
|
|
721
|
+
"%s::ipaddress && iprange(%s::ipaddress, %s::ipaddress)",
|
|
722
|
+
[
|
|
723
|
+
from_str("10.10.10.10"),
|
|
724
|
+
from_str("10.10.10.0"),
|
|
725
|
+
from_str("10.10.10.20"),
|
|
726
|
+
],
|
|
727
|
+
),
|
|
728
|
+
(
|
|
729
|
+
"192.168.0.5 = 192.168.0.0-192.168.0.255",
|
|
730
|
+
"%s::ipaddress && %s::iprange",
|
|
731
|
+
[from_str("192.168.0.5"), from_str("192.168.0.0-192.168.0.255")],
|
|
732
|
+
),
|
|
733
|
+
(
|
|
734
|
+
"192.168.0.12 = 192.168.1.0/24",
|
|
735
|
+
"%s::ipaddress && %s::iprange",
|
|
736
|
+
[from_str("192.168.0.12"), from_str("192.168.1.0/24")],
|
|
737
|
+
),
|
|
738
|
+
(
|
|
739
|
+
"10.10.10.10-10.10.10.20 = 10.10.10.15-10.10.10.25",
|
|
740
|
+
"%s::iprange && %s::iprange",
|
|
741
|
+
[
|
|
742
|
+
from_str("10.10.10.10-10.10.10.20"),
|
|
743
|
+
from_str("10.10.10.15-10.10.10.25"),
|
|
744
|
+
],
|
|
745
|
+
),
|
|
746
|
+
(
|
|
747
|
+
"10.10.10.10-10.10.10.20 = 192.168.0.0/16",
|
|
748
|
+
"%s::iprange && %s::iprange",
|
|
749
|
+
[from_str("10.10.10.10-10.10.10.20"), from_str("192.168.0.0/16")],
|
|
750
|
+
),
|
|
751
|
+
(
|
|
752
|
+
"192.168.1.0/24 = 192.168.0.0/16",
|
|
753
|
+
"%s::iprange && %s::iprange",
|
|
754
|
+
[from_str("192.168.1.0/24"), from_str("192.168.0.0/16")],
|
|
755
|
+
),
|
|
756
|
+
(
|
|
757
|
+
"192.168.1.10 = [192.168.0.0/24, 192.168.0.0/16]",
|
|
758
|
+
("%s::ipaddress && ANY(ARRAY[%s::iprange, %s::iprange])"),
|
|
759
|
+
[
|
|
760
|
+
from_str("192.168.1.10"),
|
|
761
|
+
from_str("192.168.0.0/24"),
|
|
762
|
+
from_str("192.168.0.0/16"),
|
|
763
|
+
],
|
|
764
|
+
),
|
|
765
|
+
],
|
|
766
|
+
)
|
|
767
|
+
def test_parse_sql(self, parser, sql, expr, expected_sql, expected_params):
|
|
768
|
+
sql_str, params = self.parse(parser, sql, expr)
|
|
769
|
+
|
|
770
|
+
assert sql_str == expected_sql
|
|
771
|
+
assert params == expected_params
|
|
772
|
+
|
|
773
|
+
def to_sql_and_expect_error(self, parser, sql, expression):
|
|
774
|
+
tree = parser.parse(expression)
|
|
775
|
+
with pytest.raises(RansackError):
|
|
776
|
+
sql.to_sql(tree)
|
|
777
|
+
|
|
778
|
+
@pytest.mark.parametrize(
|
|
779
|
+
"expression",
|
|
780
|
+
[
|
|
781
|
+
"2000-12-24..192.168.0.50", # Unsupported type of range.
|
|
782
|
+
"not_there", # Unknown column.
|
|
783
|
+
],
|
|
784
|
+
)
|
|
785
|
+
def test_invalid_cases(self, parser, sql, expression):
|
|
786
|
+
self.to_sql_and_expect_error(
|
|
787
|
+
parser,
|
|
788
|
+
sql,
|
|
789
|
+
expression,
|
|
790
|
+
)
|
|
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
|
|
File without changes
|