ransacklib 1.1.0.dev3__tar.gz → 1.1.0.dev5__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.dev3/ransacklib.egg-info → ransacklib-1.1.0.dev5}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/pyproject.toml +2 -1
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/operator.py +1 -1
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/transformer.py +89 -64
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5/ransacklib.egg-info}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/tests/test_transformer.py +113 -141
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/LICENSE +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/README.rst +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/__init__.py +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/exceptions.py +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/function.py +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/parser.py +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/py.typed +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/SOURCES.txt +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/dependency_links.txt +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/requires.txt +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/top_level.txt +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/setup.cfg +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/tests/test_operator.py +0 -0
- {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/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.dev5"
|
|
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
|
]
|
|
@@ -567,7 +567,7 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any]:
|
|
|
567
567
|
(Number, Number): (f"{l_sql} / {r_sql}", Number),
|
|
568
568
|
},
|
|
569
569
|
"%": {
|
|
570
|
-
(Number, Number): (f"{l_sql} % {r_sql}",
|
|
570
|
+
(Number, Number): (f"{l_sql} % {r_sql}", Number),
|
|
571
571
|
},
|
|
572
572
|
}
|
|
573
573
|
if op == "==":
|
|
@@ -810,7 +810,7 @@ class SQLInterpreter(Interpreter):
|
|
|
810
810
|
A class providing a method for translating the query from 'ransack' language to SQL.
|
|
811
811
|
"""
|
|
812
812
|
|
|
813
|
-
def to_sql(self, tree: Tree, data: dict | None = None) -> str:
|
|
813
|
+
def to_sql(self, tree: Tree, data: dict | None = None) -> tuple[str, list[Any]]:
|
|
814
814
|
"""
|
|
815
815
|
Translates the given tree to PostgreSQL query using the provided data.
|
|
816
816
|
|
|
@@ -823,52 +823,44 @@ class SQLInterpreter(Interpreter):
|
|
|
823
823
|
"""
|
|
824
824
|
self.data = data if data is not None else {}
|
|
825
825
|
|
|
826
|
-
result, _ = self.visit(tree)
|
|
826
|
+
result, params, _ = self.visit(tree)
|
|
827
827
|
|
|
828
828
|
self.data = {}
|
|
829
|
-
return result
|
|
829
|
+
return result, params
|
|
830
830
|
|
|
831
|
-
def number(self, token: TokenWrapper) -> tuple[str, type[int | float]]:
|
|
832
|
-
return (
|
|
831
|
+
def number(self, token: TokenWrapper) -> tuple[str, list, type[int | float]]:
|
|
832
|
+
return ("%s", [token.real_value], type(token.real_value))
|
|
833
833
|
|
|
834
|
-
def datetime(self, token: TokenWrapper) -> tuple[str, type[datetime]]:
|
|
834
|
+
def datetime(self, token: TokenWrapper) -> tuple[str, list, type[datetime]]:
|
|
835
835
|
if token.real_value.tzinfo is not None:
|
|
836
|
-
|
|
836
|
+
cast = "TIMESTAMP WITH TIME ZONE"
|
|
837
837
|
else:
|
|
838
|
-
|
|
839
|
-
return (
|
|
840
|
-
f"{type_} '{token.real_value.isoformat()}'",
|
|
841
|
-
type(token.real_value),
|
|
842
|
-
)
|
|
838
|
+
cast = "TIMESTAMP"
|
|
839
|
+
return (f"%s::{cast}", [token.real_value], type(token.real_value))
|
|
843
840
|
|
|
844
|
-
def timedelta_(self, token: TokenWrapper) -> tuple[str, type[timedelta]]:
|
|
845
|
-
return (
|
|
841
|
+
def timedelta_(self, token: TokenWrapper) -> tuple[str, list, type[timedelta]]:
|
|
842
|
+
return ("%s", [token.real_value], type(token.real_value))
|
|
846
843
|
|
|
847
|
-
def string_(self, token: TokenWrapper) -> tuple[str, type[str]]:
|
|
848
|
-
return (
|
|
844
|
+
def string_(self, token: TokenWrapper) -> tuple[str, list, type[str]]:
|
|
845
|
+
return ("%s", [token.real_value], type(token.real_value))
|
|
849
846
|
|
|
850
|
-
def ip(self, token: TokenWrapper) -> tuple[str, type[IP]]:
|
|
847
|
+
def ip(self, token: TokenWrapper) -> tuple[str, list, type[IP]]:
|
|
851
848
|
match token.real_value:
|
|
852
849
|
case IP4() | IP6():
|
|
853
850
|
cast = "ipaddress"
|
|
854
851
|
case IP4Range() | IP6Range() | IP4Net() | IP6Net():
|
|
855
852
|
cast = "iprange"
|
|
856
|
-
return (f"{cast}
|
|
853
|
+
return (f"%s::{cast}", [token.real_value], type(token.real_value))
|
|
857
854
|
|
|
858
|
-
def neg(self, tree: Tree) -> tuple[str, Any]:
|
|
859
|
-
|
|
860
|
-
return (f"-{
|
|
861
|
-
|
|
862
|
-
@v_args(tree=True)
|
|
863
|
-
def list(self, data: Tree) -> tuple[str, type[list]]:
|
|
864
|
-
elements = ", ".join(self.visit(x)[0] for x in data.children if x is not None)
|
|
865
|
-
return (f"ARRAY[{elements}]", list)
|
|
855
|
+
def neg(self, tree: Tree) -> tuple[str, list[Any], Any]:
|
|
856
|
+
sql, params, type_ = self.visit(tree)
|
|
857
|
+
return (f"-{sql}", params, type_)
|
|
866
858
|
|
|
867
859
|
def range_op(
|
|
868
860
|
self, l_tree: Tree, r_tree: Tree
|
|
869
|
-
) -> tuple[str, type[tuple] | type[IP]]:
|
|
870
|
-
l_sql, l_type = self.visit(l_tree)
|
|
871
|
-
r_sql, r_type = self.visit(r_tree)
|
|
861
|
+
) -> tuple[str, list[Any], type[tuple] | type[IP]]:
|
|
862
|
+
l_sql, l_params, l_type = self.visit(l_tree)
|
|
863
|
+
r_sql, r_params, r_type = self.visit(r_tree)
|
|
872
864
|
|
|
873
865
|
if issubclass(l_type, int) and issubclass(r_type, int):
|
|
874
866
|
range_type = "int4range"
|
|
@@ -882,6 +874,7 @@ class SQLInterpreter(Interpreter):
|
|
|
882
874
|
elif issubclass(l_type, (IP4, IP6)) and issubclass(r_type, (IP4, IP6)):
|
|
883
875
|
return (
|
|
884
876
|
f"iprange({l_sql}, {r_sql})",
|
|
877
|
+
l_params + r_params,
|
|
885
878
|
IP4Range if issubclass(l_type, IP4) else IP6Range,
|
|
886
879
|
)
|
|
887
880
|
else:
|
|
@@ -901,13 +894,14 @@ class SQLInterpreter(Interpreter):
|
|
|
901
894
|
f"THEN {range_type}({l_sql}, {r_sql}, '[]') "
|
|
902
895
|
f"ELSE {range_type}({r_sql}, {l_sql}, '[]') END"
|
|
903
896
|
),
|
|
897
|
+
l_params + r_params + l_params + r_params + r_params + l_params,
|
|
904
898
|
tuple,
|
|
905
899
|
)
|
|
906
900
|
|
|
907
|
-
def var_from_data(self, var: TokenWrapper) -> tuple[str, Any]:
|
|
901
|
+
def var_from_data(self, var: TokenWrapper) -> tuple[str, list[Any], Any]:
|
|
908
902
|
if var.real_value in self.data:
|
|
909
903
|
column, type_ = self.data[var.real_value]
|
|
910
|
-
return (f'"{column}"', type_)
|
|
904
|
+
return (f'"{column}"', [], type_)
|
|
911
905
|
raise EvaluationError(
|
|
912
906
|
f"The type of column '{var.real_value}' was not defined",
|
|
913
907
|
line=var.line,
|
|
@@ -918,75 +912,89 @@ class SQLInterpreter(Interpreter):
|
|
|
918
912
|
end_pos=var.end_pos,
|
|
919
913
|
)
|
|
920
914
|
|
|
921
|
-
def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, Any]:
|
|
922
|
-
l_sql, l_type = self.visit(l_tree)
|
|
923
|
-
r_sql, r_type = self.visit(r_tree)
|
|
915
|
+
def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, list[Any], Any]:
|
|
916
|
+
l_sql, l_params, l_type = self.visit(l_tree)
|
|
917
|
+
r_sql, r_params, r_type = self.visit(r_tree)
|
|
918
|
+
|
|
919
|
+
sql, type_ = binary_operation_sql(operator, l_sql, r_sql, l_type, r_type)
|
|
924
920
|
|
|
925
|
-
return
|
|
921
|
+
return sql, l_params + r_params, type_
|
|
926
922
|
|
|
927
|
-
def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, Any]:
|
|
923
|
+
def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
|
|
928
924
|
return self._binary_operation("+", l_tree, r_tree)
|
|
929
925
|
|
|
930
|
-
def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, Any]:
|
|
926
|
+
def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
|
|
931
927
|
return self._binary_operation("-", l_tree, r_tree)
|
|
932
928
|
|
|
933
|
-
def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, Any]:
|
|
929
|
+
def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
|
|
934
930
|
return self._binary_operation("*", l_tree, r_tree)
|
|
935
931
|
|
|
936
|
-
def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, Any]:
|
|
932
|
+
def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
|
|
937
933
|
return self._binary_operation("/", l_tree, r_tree)
|
|
938
934
|
|
|
939
|
-
def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, Any]:
|
|
940
|
-
return self._binary_operation("
|
|
935
|
+
def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
|
|
936
|
+
return self._binary_operation("%", l_tree, r_tree)
|
|
941
937
|
|
|
942
|
-
def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
938
|
+
def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
943
939
|
return self._binary_operation("==", l_tree, r_tree)
|
|
944
940
|
|
|
945
|
-
def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
941
|
+
def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
946
942
|
return self._binary_operation("in", l_tree, r_tree)
|
|
947
943
|
|
|
948
|
-
def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
944
|
+
def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
949
945
|
return self._binary_operation("=", l_tree, r_tree)
|
|
950
946
|
|
|
951
|
-
def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
947
|
+
def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
952
948
|
return self._binary_operation(">", l_tree, r_tree)
|
|
953
949
|
|
|
954
|
-
def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
950
|
+
def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
955
951
|
return self._binary_operation(">=", l_tree, r_tree)
|
|
956
952
|
|
|
957
|
-
def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
953
|
+
def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
958
954
|
return self._binary_operation("<", l_tree, r_tree)
|
|
959
955
|
|
|
960
|
-
def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
956
|
+
def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
961
957
|
return self._binary_operation("<=", l_tree, r_tree)
|
|
962
958
|
|
|
963
|
-
def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
959
|
+
def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
964
960
|
return self._binary_operation("or", l_tree, r_tree)
|
|
965
961
|
|
|
966
|
-
def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
962
|
+
def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
967
963
|
return self._binary_operation("and", l_tree, r_tree)
|
|
968
964
|
|
|
969
|
-
def not_op(self, tree: Tree) -> tuple[str, type[bool]]:
|
|
970
|
-
sql, type_ = self.visit(tree)
|
|
971
|
-
return
|
|
965
|
+
def not_op(self, tree: Tree) -> tuple[str, list[Any], type[bool]]:
|
|
966
|
+
sql, params, type_ = self.visit(tree)
|
|
967
|
+
return f"NOT {sql}", params, type_
|
|
972
968
|
|
|
973
|
-
def contains_op(
|
|
969
|
+
def contains_op(
|
|
970
|
+
self, l_tree: Tree, r_tree: Tree
|
|
971
|
+
) -> tuple[str, list[Any], type[bool]]:
|
|
974
972
|
return self._binary_operation("contains", l_tree, r_tree)
|
|
975
973
|
|
|
976
|
-
def exists_op(self, path: Token) -> tuple[str, type[bool]]:
|
|
977
|
-
|
|
978
|
-
return f"{
|
|
974
|
+
def exists_op(self, path: Token) -> tuple[str, list[Any], type[bool]]:
|
|
975
|
+
var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
|
|
976
|
+
return f"{var_sql} is not null", [], bool
|
|
979
977
|
|
|
980
978
|
def exists_with_default(self, path: Token, default: Any) -> Any:
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
return f"COALESCE({
|
|
979
|
+
var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
|
|
980
|
+
default_sql, default_params, default_type = self.visit(default)
|
|
981
|
+
return f"COALESCE({var_sql}, {default_sql})", default_params, default_type
|
|
982
|
+
|
|
983
|
+
def function_(
|
|
984
|
+
self, name: TokenWrapper, args: Tree | None
|
|
985
|
+
) -> tuple[str, list[Any], Any]:
|
|
986
|
+
function_name = name.real_value
|
|
987
|
+
|
|
988
|
+
args_sql = []
|
|
989
|
+
params = []
|
|
990
|
+
|
|
991
|
+
for arg in args.children if args else []:
|
|
992
|
+
sql, p, _ = self.visit(arg)
|
|
993
|
+
args_sql.append(sql)
|
|
994
|
+
params.extend(p)
|
|
984
995
|
|
|
985
|
-
def function_(self, name: TokenWrapper, args: Tree | None) -> tuple[str, Any]:
|
|
986
|
-
function_name = cast("str", name.real_value)
|
|
987
|
-
args = [self.visit(x) for x in (args.children if args else [])] # type: ignore
|
|
988
996
|
if function_name == "now":
|
|
989
|
-
return "localtimestamp", datetime
|
|
997
|
+
return "localtimestamp", [], datetime
|
|
990
998
|
raise EvaluationError(
|
|
991
999
|
f"Function '{name.real_value}' was not found.",
|
|
992
1000
|
line=name.line,
|
|
@@ -996,3 +1004,20 @@ class SQLInterpreter(Interpreter):
|
|
|
996
1004
|
end_column=name.end_column,
|
|
997
1005
|
end_pos=name.end_pos,
|
|
998
1006
|
)
|
|
1007
|
+
|
|
1008
|
+
@v_args(tree=True)
|
|
1009
|
+
def list(self, data: Tree) -> tuple[str, list, Any]:
|
|
1010
|
+
parts = []
|
|
1011
|
+
params = []
|
|
1012
|
+
|
|
1013
|
+
for child in data.children:
|
|
1014
|
+
if child is None:
|
|
1015
|
+
continue
|
|
1016
|
+
sql, p, _ = self.visit(child)
|
|
1017
|
+
parts.append(sql)
|
|
1018
|
+
params.extend(p)
|
|
1019
|
+
|
|
1020
|
+
if not parts:
|
|
1021
|
+
return "'{}'", [], list
|
|
1022
|
+
|
|
1023
|
+
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
|
|
@@ -30,6 +30,11 @@ def expr_transformer():
|
|
|
30
30
|
return ExpressionTransformer()
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def sql():
|
|
35
|
+
return SQLInterpreter()
|
|
36
|
+
|
|
37
|
+
|
|
33
38
|
sample_data = {
|
|
34
39
|
"Category": ["Intrusion.UserCompromise"],
|
|
35
40
|
"ConnCount": 1,
|
|
@@ -508,13 +513,8 @@ class TestFilter:
|
|
|
508
513
|
assert res
|
|
509
514
|
|
|
510
515
|
|
|
511
|
-
@pytest.fixture
|
|
512
|
-
def sql():
|
|
513
|
-
return SQLInterpreter()
|
|
514
|
-
|
|
515
|
-
|
|
516
516
|
class TestSQLInterpreter:
|
|
517
|
-
def
|
|
517
|
+
def parse(self, parser, sql, expression):
|
|
518
518
|
tree = parser.parse(expression)
|
|
519
519
|
data = {
|
|
520
520
|
"a_int": ("a", int),
|
|
@@ -526,176 +526,148 @@ class TestSQLInterpreter:
|
|
|
526
526
|
return sql.to_sql(tree, data)
|
|
527
527
|
|
|
528
528
|
@pytest.mark.parametrize(
|
|
529
|
-
("
|
|
529
|
+
("expr", "expected_sql", "expected_params"),
|
|
530
530
|
[
|
|
531
|
-
#
|
|
532
|
-
("5", "5
|
|
533
|
-
("-12", "
|
|
534
|
-
("23.22", "23.22
|
|
535
|
-
("-0.224", "
|
|
536
|
-
("13e19", "1.
|
|
537
|
-
|
|
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)]),
|
|
538
539
|
(
|
|
539
540
|
"2025-01-01T22:14:24.142+02:00",
|
|
540
|
-
"TIMESTAMP WITH TIME ZONE
|
|
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
|
+
],
|
|
541
554
|
),
|
|
542
|
-
|
|
543
|
-
("
|
|
544
|
-
("
|
|
545
|
-
("
|
|
546
|
-
|
|
547
|
-
("
|
|
548
|
-
("
|
|
549
|
-
("
|
|
550
|
-
|
|
551
|
-
("
|
|
552
|
-
|
|
553
|
-
("[1, 2, 3]", "ARRAY[1, 2, 3]"),
|
|
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")]),
|
|
554
566
|
(
|
|
555
|
-
"
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
"TIMESTAMP '2025-12-12T00:00:00']"
|
|
559
|
-
),
|
|
567
|
+
"192.168.0.10-192.168.0.50",
|
|
568
|
+
"%s::iprange",
|
|
569
|
+
[from_str("192.168.0.10-192.168.0.50")],
|
|
560
570
|
),
|
|
561
|
-
("
|
|
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.
|
|
562
579
|
(
|
|
563
580
|
"100..1",
|
|
564
581
|
(
|
|
565
|
-
"CASE WHEN
|
|
566
|
-
"THEN int4range(
|
|
567
|
-
"ELSE int4range(
|
|
582
|
+
"CASE WHEN %s < %s "
|
|
583
|
+
"THEN int4range(%s, %s, '[]') "
|
|
584
|
+
"ELSE int4range(%s, %s, '[]') END"
|
|
568
585
|
),
|
|
586
|
+
[100, 1, 100, 1, 1, 100],
|
|
569
587
|
),
|
|
570
588
|
(
|
|
571
589
|
"100.12..1.14",
|
|
572
590
|
(
|
|
573
|
-
"CASE WHEN
|
|
574
|
-
"THEN numrange(
|
|
575
|
-
"ELSE numrange(
|
|
576
|
-
),
|
|
577
|
-
),
|
|
578
|
-
(
|
|
579
|
-
"-10..10",
|
|
580
|
-
(
|
|
581
|
-
"CASE WHEN -10 < 10 "
|
|
582
|
-
"THEN int4range(-10, 10, '[]') "
|
|
583
|
-
"ELSE int4range(10, -10, '[]') END"
|
|
591
|
+
"CASE WHEN %s < %s "
|
|
592
|
+
"THEN numrange(%s, %s, '[]') "
|
|
593
|
+
"ELSE numrange(%s, %s, '[]') END"
|
|
584
594
|
),
|
|
595
|
+
[100.12, 1.14, 100.12, 1.14, 1.14, 100.12],
|
|
585
596
|
),
|
|
586
597
|
(
|
|
587
598
|
"2024-01-01T00:00:00+00:00..2024-12-31T00:00:00Z",
|
|
588
599
|
(
|
|
589
|
-
"CASE WHEN TIMESTAMP WITH TIME ZONE
|
|
590
|
-
"< TIMESTAMP WITH TIME ZONE
|
|
591
|
-
"tstzrange(TIMESTAMP WITH TIME ZONE
|
|
592
|
-
"TIMESTAMP WITH TIME ZONE
|
|
593
|
-
"tstzrange(TIMESTAMP WITH TIME ZONE
|
|
594
|
-
"TIMESTAMP WITH TIME ZONE
|
|
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"
|
|
595
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
|
+
],
|
|
596
615
|
),
|
|
597
616
|
(
|
|
598
|
-
"
|
|
599
|
-
"iprange(ipaddress '192.168.0.10', ipaddress '192.168.0.50')",
|
|
600
|
-
),
|
|
601
|
-
("fd00::1..fd00::10", "iprange(ipaddress 'fd00::1', ipaddress 'fd00::10')"),
|
|
602
|
-
# Test variable access
|
|
603
|
-
("a_int", '"a"'),
|
|
604
|
-
(
|
|
605
|
-
"a_int .. b_int",
|
|
617
|
+
"2024-01-01T00:00:00..2024-12-31T00:00:00",
|
|
606
618
|
(
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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"
|
|
610
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
|
+
],
|
|
611
640
|
),
|
|
612
|
-
# Test = operator
|
|
613
|
-
("1 = 1", "1 = 1"),
|
|
614
|
-
("1 = [1, 2, 3]", "1 = ANY(ARRAY[1, 2, 3])"),
|
|
615
|
-
("[1, 2, 3] = 1", "1 = ANY(ARRAY[1, 2, 3])"),
|
|
616
|
-
("[1, 2] = [3, 4]", "ARRAY[1, 2] && ARRAY[3, 4]"),
|
|
617
641
|
(
|
|
618
|
-
"
|
|
619
|
-
(
|
|
620
|
-
|
|
621
|
-
"THEN int4range(1, 10, '[]') "
|
|
622
|
-
"ELSE int4range(10, 1, '[]') END @> 1"
|
|
623
|
-
),
|
|
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")],
|
|
624
645
|
),
|
|
625
646
|
(
|
|
626
|
-
"1..10
|
|
627
|
-
(
|
|
628
|
-
|
|
629
|
-
"THEN int4range(1, 10, '[]') "
|
|
630
|
-
"ELSE int4range(10, 1, '[]') END @> 1"
|
|
631
|
-
),
|
|
647
|
+
"fd00::1..fd00::10",
|
|
648
|
+
"iprange(%s::ipaddress, %s::ipaddress)",
|
|
649
|
+
[from_str("fd00::1"), from_str("fd00::10")],
|
|
632
650
|
),
|
|
633
|
-
|
|
651
|
+
# Test variable access.
|
|
652
|
+
("a_int", '"a"', []),
|
|
634
653
|
(
|
|
635
|
-
"a_int..b_int
|
|
654
|
+
"a_int .. b_int",
|
|
636
655
|
(
|
|
637
656
|
'CASE WHEN "a" < "b" '
|
|
638
657
|
'THEN int4range("a", "b", \'[]\') '
|
|
639
|
-
'ELSE int4range("b", "a", \'[]\') END
|
|
640
|
-
'&& CASE WHEN "c" < "d" '
|
|
641
|
-
'THEN int4range("c", "d", \'[]\') '
|
|
642
|
-
'ELSE int4range("d", "c", \'[]\') END'
|
|
643
|
-
),
|
|
644
|
-
),
|
|
645
|
-
(
|
|
646
|
-
"10.10.10.10 = 10.10.10.10",
|
|
647
|
-
"ipaddress '10.10.10.10' && ipaddress '10.10.10.10'",
|
|
648
|
-
),
|
|
649
|
-
(
|
|
650
|
-
"10.10.10.10 = 10.10.10.0 .. 10.10.10.20",
|
|
651
|
-
(
|
|
652
|
-
"ipaddress '10.10.10.10' && "
|
|
653
|
-
"iprange(ipaddress '10.10.10.0', ipaddress '10.10.10.20')"
|
|
654
|
-
),
|
|
655
|
-
),
|
|
656
|
-
(
|
|
657
|
-
"192.168.0.5 = 192.168.0.0-192.168.0.255",
|
|
658
|
-
"ipaddress '192.168.0.5' && iprange '192.168.0.0-192.168.0.255'",
|
|
659
|
-
),
|
|
660
|
-
(
|
|
661
|
-
"192.168.0.12 = 192.168.1.0/24",
|
|
662
|
-
"ipaddress '192.168.0.12' && iprange '192.168.1.0/24'",
|
|
663
|
-
),
|
|
664
|
-
(
|
|
665
|
-
"10.10.10.10-10.10.10.20 = 10.10.10.15-10.10.10.25",
|
|
666
|
-
(
|
|
667
|
-
"iprange '10.10.10.10-10.10.10.20' && "
|
|
668
|
-
"iprange '10.10.10.15-10.10.10.25'"
|
|
669
|
-
),
|
|
670
|
-
),
|
|
671
|
-
(
|
|
672
|
-
"10.10.10.10-10.10.10.20 = 192.168.0.0/16",
|
|
673
|
-
"iprange '10.10.10.10-10.10.10.20' && iprange '192.168.0.0/16'",
|
|
674
|
-
),
|
|
675
|
-
(
|
|
676
|
-
"192.168.1.0/24 = 192.168.0.0/16",
|
|
677
|
-
"iprange '192.168.1.0/24' && iprange '192.168.0.0/16'",
|
|
678
|
-
),
|
|
679
|
-
(
|
|
680
|
-
"192.168.1.10 = [192.168.0.0/24, 192.168.0.0/16]",
|
|
681
|
-
(
|
|
682
|
-
"ipaddress '192.168.1.10' && "
|
|
683
|
-
"ANY(ARRAY[iprange '192.168.0.0/24', iprange '192.168.0.0/16'])"
|
|
658
|
+
'ELSE int4range("b", "a", \'[]\') END'
|
|
684
659
|
),
|
|
660
|
+
[],
|
|
685
661
|
),
|
|
686
|
-
# Test comparisons
|
|
687
|
-
("2 >= 2 and 4 <= 12.89 or 4 == 4", "2 >= 2 AND 4 <= 12.89 OR 4 = 4"),
|
|
688
|
-
("3 > 2 or 2 < 1 and 1 > 2", "3 > 2 OR 2 < 1 AND 1 > 2"),
|
|
689
|
-
("2 < 3 and 3 > 1 or 1 < 0", "2 < 3 AND 3 > 1 OR 1 < 0"),
|
|
690
|
-
("3 > 2 and not 2 < 1", "3 > 2 AND NOT 2 < 1"),
|
|
691
662
|
],
|
|
692
663
|
)
|
|
693
|
-
def
|
|
694
|
-
|
|
695
|
-
|
|
664
|
+
def test_parse_sql(self, parser, sql, expr, expected_sql, expected_params):
|
|
665
|
+
sql_str, params = self.parse(parser, sql, expr)
|
|
666
|
+
|
|
667
|
+
assert sql_str == expected_sql
|
|
668
|
+
assert params == expected_params
|
|
696
669
|
|
|
697
670
|
def to_sql_and_expect_error(self, parser, sql, expression):
|
|
698
|
-
"""Parse the expression and expect an exception to be raised."""
|
|
699
671
|
tree = parser.parse(expression)
|
|
700
672
|
with pytest.raises(RansackError):
|
|
701
673
|
sql.to_sql(tree)
|
|
@@ -703,8 +675,8 @@ class TestSQLInterpreter:
|
|
|
703
675
|
@pytest.mark.parametrize(
|
|
704
676
|
"expression",
|
|
705
677
|
[
|
|
706
|
-
"2000-12-24..192.168.0.50", # Unsupported type of range
|
|
707
|
-
"not_there", # Unknown column
|
|
678
|
+
"2000-12-24..192.168.0.50", # Unsupported type of range.
|
|
679
|
+
"not_there", # Unknown column.
|
|
708
680
|
],
|
|
709
681
|
)
|
|
710
682
|
def test_invalid_cases(self, parser, sql, expression):
|
|
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
|