ransacklib 0.1.11__tar.gz → 1.1.0.dev0__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-0.1.11/ransacklib.egg-info → ransacklib-1.1.0.dev0}/PKG-INFO +11 -8
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/pyproject.toml +10 -8
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/__init__.py +1 -1
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/operator.py +75 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/parser.py +1 -1
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/transformer.py +157 -1
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0/ransacklib.egg-info}/PKG-INFO +11 -8
- ransacklib-1.1.0.dev0/ransacklib.egg-info/requires.txt +12 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/tests/test_parser.py +10 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/tests/test_transformer.py +214 -1
- ransacklib-0.1.11/ransacklib.egg-info/requires.txt +0 -10
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/LICENSE +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/README.rst +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/exceptions.py +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/function.py +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/py.typed +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransacklib.egg-info/SOURCES.txt +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransacklib.egg-info/dependency_links.txt +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransacklib.egg-info/top_level.txt +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/setup.cfg +0 -0
- {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/tests/test_operator.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ransacklib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0.dev0
|
|
4
4
|
Summary: A modern, extensible language for manipulation with structured data
|
|
5
5
|
Author-email: "Rajmund H. Hruška" <rajmund.hruska@cesnet.cz>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Repository, https://gitlab.cesnet.cz/713/mentat/ransack
|
|
8
8
|
Project-URL: Documentation, https://ransack-125e0a.gitlab-pages.cesnet.cz/index.html
|
|
9
9
|
Project-URL: Issues, https://gitlab.cesnet.cz/713/mentat/ransack/-/issues
|
|
10
|
+
Project-URL: Changelog, https://gitlab.cesnet.cz/713/mentat/ransack/-/blob/master/CHANGELOG.md
|
|
10
11
|
Classifier: Programming Language :: Python :: 3
|
|
11
12
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -14,15 +15,17 @@ Classifier: Operating System :: OS Independent
|
|
|
14
15
|
Requires-Python: >=3.10
|
|
15
16
|
Description-Content-Type: text/x-rst
|
|
16
17
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: lark<1.
|
|
18
|
+
Requires-Dist: lark<1.4.0,>=1.3.0
|
|
18
19
|
Requires-Dist: ipranges<0.2,>=0.1.12
|
|
19
20
|
Provides-Extra: dev
|
|
20
|
-
Requires-Dist: pytest; extra == "dev"
|
|
21
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
-
Requires-Dist: sphinx; extra == "dev"
|
|
23
|
-
Requires-Dist: sphinx_rtd_theme; extra == "dev"
|
|
24
|
-
Requires-Dist: mypy; extra == "dev"
|
|
25
|
-
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest<9.0,>=8.4; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov<8.0,>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: sphinx<8.2,>=8.1; extra == "dev"
|
|
24
|
+
Requires-Dist: sphinx_rtd_theme<4.0,>=3.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy<2.0,>=1.18; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff<0.15,>=0.14; extra == "dev"
|
|
27
|
+
Requires-Dist: build<1.4,>=1.3; extra == "dev"
|
|
28
|
+
Requires-Dist: twine<7.0,>=6.2; extra == "dev"
|
|
26
29
|
Dynamic: license-file
|
|
27
30
|
|
|
28
31
|
Welcome to ransack
|
|
@@ -9,7 +9,7 @@ description = "A modern, extensible language for manipulation with structured da
|
|
|
9
9
|
license = "MIT"
|
|
10
10
|
readme = "README.rst"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"lark>=1.
|
|
12
|
+
"lark>=1.3.0,<1.4.0",
|
|
13
13
|
"ipranges>=0.1.12,<0.2",
|
|
14
14
|
]
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -25,18 +25,21 @@ classifiers = [
|
|
|
25
25
|
|
|
26
26
|
[project.optional-dependencies]
|
|
27
27
|
dev = [
|
|
28
|
-
"pytest",
|
|
29
|
-
"pytest-cov",
|
|
30
|
-
"sphinx",
|
|
31
|
-
"sphinx_rtd_theme",
|
|
32
|
-
"mypy",
|
|
33
|
-
"ruff",
|
|
28
|
+
"pytest>=8.4,<9.0",
|
|
29
|
+
"pytest-cov>=7.0,<8.0",
|
|
30
|
+
"sphinx>=8.1,<8.2", # version 8.2 requires Python>=3.11
|
|
31
|
+
"sphinx_rtd_theme>=3.0,<4.0",
|
|
32
|
+
"mypy>=1.18,<2.0",
|
|
33
|
+
"ruff>=0.14,<0.15",
|
|
34
|
+
"build>=1.3,<1.4",
|
|
35
|
+
"twine>=6.2,<7.0",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.urls]
|
|
37
39
|
Repository = "https://gitlab.cesnet.cz/713/mentat/ransack"
|
|
38
40
|
Documentation = "https://ransack-125e0a.gitlab-pages.cesnet.cz/index.html"
|
|
39
41
|
Issues = "https://gitlab.cesnet.cz/713/mentat/ransack/-/issues"
|
|
42
|
+
Changelog = "https://gitlab.cesnet.cz/713/mentat/ransack/-/blob/master/CHANGELOG.md"
|
|
40
43
|
|
|
41
44
|
[tool.setuptools]
|
|
42
45
|
packages = ["ransack"]
|
|
@@ -80,5 +83,4 @@ select = [
|
|
|
80
83
|
|
|
81
84
|
ignore = [
|
|
82
85
|
"PLR0913", # too-many-arguments
|
|
83
|
-
"UP038", # deprecated, will be removed
|
|
84
86
|
]
|
|
@@ -498,3 +498,78 @@ def binary_operation(operator: str, left: Any, right: Any) -> Any:
|
|
|
498
498
|
pass
|
|
499
499
|
# Raise an error if the operator is not found in either order
|
|
500
500
|
raise OperatorNotFoundError(operator, (t1, t2), (left, right)) from None
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any]:
|
|
504
|
+
def _get_comp_dict_sql(op: str, tuple_repr_f) -> dict[tuple[Operand, Operand], str]:
|
|
505
|
+
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})",
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
d: dict[str, dict] = {
|
|
521
|
+
"=": {
|
|
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}",
|
|
536
|
+
},
|
|
537
|
+
">": _get_comp_dict_sql(">", "lower")
|
|
538
|
+
| {(tuple, tuple): f"upper({l_sql}) > lower({r_sql})"},
|
|
539
|
+
">=": _get_comp_dict_sql(">=", "lower")
|
|
540
|
+
| {(tuple, tuple): f"upper({l_sql}) >= lower({r_sql})"},
|
|
541
|
+
"<": _get_comp_dict_sql("<", "upper")
|
|
542
|
+
| {(tuple, tuple): f"lower({l_sql}) < upper({r_sql})"},
|
|
543
|
+
"<=": _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"},
|
|
548
|
+
}
|
|
549
|
+
if op == "==":
|
|
550
|
+
return (f"{l_sql} = {r_sql}", bool)
|
|
551
|
+
if op == "in":
|
|
552
|
+
return (d["="][(t1, t2)], bool)
|
|
553
|
+
return (d[op][(t1, t2)], bool)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def binary_operation_sql(
|
|
557
|
+
operator: str, left: str, right: str, l_type: Any, r_type: Any
|
|
558
|
+
) -> tuple[str, Any]:
|
|
559
|
+
# Normalize numeric types
|
|
560
|
+
l_type = Number if l_type in (int, float) else l_type
|
|
561
|
+
r_type = Number if r_type in (int, float) else r_type
|
|
562
|
+
l_type = (
|
|
563
|
+
"ip" if l_type in (IP4, IP4Net, IP4Range, IP6, IP6Net, IP6Range) else l_type
|
|
564
|
+
)
|
|
565
|
+
r_type = (
|
|
566
|
+
"ip" if r_type in (IP4, IP4Net, IP4Range, IP6, IP6Net, IP6Range) else r_type
|
|
567
|
+
)
|
|
568
|
+
try:
|
|
569
|
+
return _operator_map_sql(operator, left, right, l_type, r_type)
|
|
570
|
+
except KeyError:
|
|
571
|
+
try:
|
|
572
|
+
return _operator_map_sql(operator, right, left, r_type, l_type)
|
|
573
|
+
except KeyError:
|
|
574
|
+
pass
|
|
575
|
+
raise OperatorNotFoundError(operator, (l_type, r_type), (left, right)) from None
|
|
@@ -112,7 +112,7 @@ class Parser:
|
|
|
112
112
|
DATE.2: /[0-9]{4}-[0-9]{2}-[0-9]{2}/
|
|
113
113
|
TIME.2: /[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:[Zz]|(?:[+-][0-9]{2}:[0-9]{2}))?/
|
|
114
114
|
TIMEDELTA.2: /([0-9]+[D|d])?[0-9]{2}:[0-9]{2}:[0-9]{2}/
|
|
115
|
-
STRING: /"([^"]
|
|
115
|
+
STRING: /"([^"]*)"|\'([^\']*)\'/
|
|
116
116
|
VARIABLE: /\.?[_a-zA-Z][-_a-zA-Z0-9]*(?:\.?[_a-zA-Z][-_a-zA-Z0-9]*)*/
|
|
117
117
|
FUNCTION.2: /[_a-zA-Z][_a-zA-Z0-9]*\(/
|
|
118
118
|
|
|
@@ -30,7 +30,7 @@ from lark.visitors import Interpreter
|
|
|
30
30
|
|
|
31
31
|
from .exceptions import EvaluationError, RansackError
|
|
32
32
|
from .function import predefined_functions
|
|
33
|
-
from .operator import binary_operation
|
|
33
|
+
from .operator import IP, binary_operation, binary_operation_sql
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class TokenWrapper:
|
|
@@ -807,3 +807,159 @@ class Filter(Interpreter):
|
|
|
807
807
|
end_column=name.end_column,
|
|
808
808
|
end_pos=name.end_pos,
|
|
809
809
|
)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
@v_args(inline=True)
|
|
813
|
+
class SQLInterpreter(Interpreter):
|
|
814
|
+
"""
|
|
815
|
+
A class providing a method for translating the query from 'ransack' language to SQL.
|
|
816
|
+
"""
|
|
817
|
+
|
|
818
|
+
def to_sql(self, tree: Tree, data: dict | None = None) -> str:
|
|
819
|
+
"""
|
|
820
|
+
Translates the given tree to PostgreSQL query using the provided data.
|
|
821
|
+
|
|
822
|
+
Parameters:
|
|
823
|
+
tree: The parse tree to translate to SQL.
|
|
824
|
+
data: Optional dictionary containing the definition of queried columns.
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
A part of the SQL query - the part after 'SELECT' or 'WHERE'.
|
|
828
|
+
"""
|
|
829
|
+
self.data = data if data is not None else {}
|
|
830
|
+
|
|
831
|
+
result, _ = self.visit(tree)
|
|
832
|
+
|
|
833
|
+
self.data = {}
|
|
834
|
+
return result
|
|
835
|
+
|
|
836
|
+
def number(self, token: TokenWrapper) -> tuple[str, type[int | float]]:
|
|
837
|
+
return (str(token.real_value), type(token.real_value))
|
|
838
|
+
|
|
839
|
+
def datetime(self, token: TokenWrapper) -> tuple[str, type[datetime]]:
|
|
840
|
+
# When using 'real_value', the time zone is always defined
|
|
841
|
+
return (
|
|
842
|
+
f"TIMESTAMP WITH TIME ZONE '{token.real_value.isoformat()}'",
|
|
843
|
+
type(token.real_value),
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
def timedelta_(self, token: TokenWrapper) -> tuple[str, type[timedelta]]:
|
|
847
|
+
return (f"INTERVAL '{token.real_value!s}'", type(token.real_value))
|
|
848
|
+
|
|
849
|
+
def string_(self, token: TokenWrapper) -> tuple[str, type[str]]:
|
|
850
|
+
return (f"'{token.real_value}'", str)
|
|
851
|
+
|
|
852
|
+
def ip(self, token: TokenWrapper) -> tuple[str, type[IP]]:
|
|
853
|
+
match token.real_value:
|
|
854
|
+
case IP4() | IP6():
|
|
855
|
+
cast = "ipaddress"
|
|
856
|
+
case IP4Range() | IP6Range() | IP4Net() | IP6Net():
|
|
857
|
+
cast = "iprange"
|
|
858
|
+
return (f"{cast} '{token.real_value!s}'", type(token.real_value))
|
|
859
|
+
|
|
860
|
+
def neg(self, tree: Tree) -> tuple[str, Any]:
|
|
861
|
+
value, type_ = self.visit(tree)
|
|
862
|
+
return (f"-{value}", type_)
|
|
863
|
+
|
|
864
|
+
@v_args(tree=True)
|
|
865
|
+
def list(self, data: Tree) -> tuple[str, type[list]]:
|
|
866
|
+
elements = ", ".join(self.visit(x)[0] for x in data.children if x is not None)
|
|
867
|
+
return (f"ARRAY[{elements}]", list)
|
|
868
|
+
|
|
869
|
+
def range_op(
|
|
870
|
+
self, l_tree: Tree, r_tree: Tree
|
|
871
|
+
) -> tuple[str, type[tuple] | type[IP]]:
|
|
872
|
+
l_sql, l_type = self.visit(l_tree)
|
|
873
|
+
r_sql, r_type = self.visit(r_tree)
|
|
874
|
+
|
|
875
|
+
if issubclass(l_type, int) and issubclass(r_type, int):
|
|
876
|
+
range_type = "int4range"
|
|
877
|
+
elif issubclass(l_type, float) and issubclass(r_type, float):
|
|
878
|
+
range_type = "numrange"
|
|
879
|
+
elif issubclass(l_type, datetime) and issubclass(r_type, datetime):
|
|
880
|
+
range_type = "tstzrange"
|
|
881
|
+
elif issubclass(l_type, (IP4, IP6)) and issubclass(r_type, (IP4, IP6)):
|
|
882
|
+
return (
|
|
883
|
+
f"iprange({l_sql}, {r_sql})",
|
|
884
|
+
IP4Range if issubclass(l_type, IP4) else IP6Range,
|
|
885
|
+
)
|
|
886
|
+
else:
|
|
887
|
+
raise EvaluationError(
|
|
888
|
+
f"Range operator is not supported for types '{l_type}' and '{r_type}'",
|
|
889
|
+
line=l_tree.meta.line,
|
|
890
|
+
column=l_tree.meta.column,
|
|
891
|
+
start_pos=l_tree.meta.start_pos,
|
|
892
|
+
end_line=r_tree.meta.end_line,
|
|
893
|
+
end_column=r_tree.meta.end_column,
|
|
894
|
+
end_pos=r_tree.meta.end_pos,
|
|
895
|
+
) from None
|
|
896
|
+
|
|
897
|
+
return (
|
|
898
|
+
(
|
|
899
|
+
f"CASE WHEN {l_sql} < {r_sql} "
|
|
900
|
+
f"THEN {range_type}({l_sql}, {r_sql}, '[]') "
|
|
901
|
+
f"ELSE {range_type}({r_sql}, {l_sql}, '[]') END"
|
|
902
|
+
),
|
|
903
|
+
tuple,
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
def var_from_data(self, var: TokenWrapper) -> tuple[str, Any]:
|
|
907
|
+
if var.real_value in self.data:
|
|
908
|
+
column, type_ = self.data[var.real_value]
|
|
909
|
+
return (f'"{column}"', type_)
|
|
910
|
+
raise EvaluationError(
|
|
911
|
+
f"The type of column '{var.real_value}' was not defined",
|
|
912
|
+
line=var.line,
|
|
913
|
+
column=var.column,
|
|
914
|
+
start_pos=var.start_pos,
|
|
915
|
+
end_line=var.end_line,
|
|
916
|
+
end_column=var.end_column,
|
|
917
|
+
end_pos=var.end_pos,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, Any]:
|
|
921
|
+
l_sql, l_type = self.visit(l_tree)
|
|
922
|
+
r_sql, r_type = self.visit(r_tree)
|
|
923
|
+
|
|
924
|
+
return binary_operation_sql(operator, l_sql, r_sql, l_type, r_type)
|
|
925
|
+
|
|
926
|
+
def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
927
|
+
return self._binary_operation("==", l_tree, r_tree)
|
|
928
|
+
|
|
929
|
+
def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
930
|
+
return self._binary_operation("in", l_tree, r_tree)
|
|
931
|
+
|
|
932
|
+
def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
933
|
+
return self._binary_operation("=", l_tree, r_tree)
|
|
934
|
+
|
|
935
|
+
def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
936
|
+
return self._binary_operation(">", l_tree, r_tree)
|
|
937
|
+
|
|
938
|
+
def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
939
|
+
return self._binary_operation(">=", l_tree, r_tree)
|
|
940
|
+
|
|
941
|
+
def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
942
|
+
return self._binary_operation("<", l_tree, r_tree)
|
|
943
|
+
|
|
944
|
+
def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
945
|
+
return self._binary_operation("<=", l_tree, r_tree)
|
|
946
|
+
|
|
947
|
+
def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
948
|
+
return self._binary_operation("or", l_tree, r_tree)
|
|
949
|
+
|
|
950
|
+
def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
951
|
+
return self._binary_operation("and", l_tree, r_tree)
|
|
952
|
+
|
|
953
|
+
def not_op(self, tree: Tree) -> tuple[str, type[bool]]:
|
|
954
|
+
sql, type_ = self.visit(tree)
|
|
955
|
+
return (f"NOT {sql}", type_)
|
|
956
|
+
|
|
957
|
+
def contains_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
|
|
958
|
+
return self._binary_operation("contains", l_tree, r_tree)
|
|
959
|
+
|
|
960
|
+
def exists_op(self, path: Token) -> tuple[str, type[bool]]:
|
|
961
|
+
return f"{path!s} is not null", bool
|
|
962
|
+
|
|
963
|
+
def exists_with_default(self, path: Token, default: Any) -> Any:
|
|
964
|
+
sql, type_ = self.visit(default)
|
|
965
|
+
return f"COALESCE({path!s}, {sql})", type_
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ransacklib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0.dev0
|
|
4
4
|
Summary: A modern, extensible language for manipulation with structured data
|
|
5
5
|
Author-email: "Rajmund H. Hruška" <rajmund.hruska@cesnet.cz>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Repository, https://gitlab.cesnet.cz/713/mentat/ransack
|
|
8
8
|
Project-URL: Documentation, https://ransack-125e0a.gitlab-pages.cesnet.cz/index.html
|
|
9
9
|
Project-URL: Issues, https://gitlab.cesnet.cz/713/mentat/ransack/-/issues
|
|
10
|
+
Project-URL: Changelog, https://gitlab.cesnet.cz/713/mentat/ransack/-/blob/master/CHANGELOG.md
|
|
10
11
|
Classifier: Programming Language :: Python :: 3
|
|
11
12
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -14,15 +15,17 @@ Classifier: Operating System :: OS Independent
|
|
|
14
15
|
Requires-Python: >=3.10
|
|
15
16
|
Description-Content-Type: text/x-rst
|
|
16
17
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: lark<1.
|
|
18
|
+
Requires-Dist: lark<1.4.0,>=1.3.0
|
|
18
19
|
Requires-Dist: ipranges<0.2,>=0.1.12
|
|
19
20
|
Provides-Extra: dev
|
|
20
|
-
Requires-Dist: pytest; extra == "dev"
|
|
21
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
-
Requires-Dist: sphinx; extra == "dev"
|
|
23
|
-
Requires-Dist: sphinx_rtd_theme; extra == "dev"
|
|
24
|
-
Requires-Dist: mypy; extra == "dev"
|
|
25
|
-
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest<9.0,>=8.4; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov<8.0,>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: sphinx<8.2,>=8.1; extra == "dev"
|
|
24
|
+
Requires-Dist: sphinx_rtd_theme<4.0,>=3.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy<2.0,>=1.18; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff<0.15,>=0.14; extra == "dev"
|
|
27
|
+
Requires-Dist: build<1.4,>=1.3; extra == "dev"
|
|
28
|
+
Requires-Dist: twine<7.0,>=6.2; extra == "dev"
|
|
26
29
|
Dynamic: license-file
|
|
27
30
|
|
|
28
31
|
Welcome to ransack
|
|
@@ -327,6 +327,16 @@ def test_string_with_special_chars(parser):
|
|
|
327
327
|
assert special_input in tree.pretty()
|
|
328
328
|
|
|
329
329
|
|
|
330
|
+
# Test empty strings
|
|
331
|
+
def test_string_empty(parser):
|
|
332
|
+
tree = parser.parse_only('""')
|
|
333
|
+
assert tree is not None
|
|
334
|
+
assert "string" in tree.pretty()
|
|
335
|
+
tree = parser.parse_only("''")
|
|
336
|
+
assert tree is not None
|
|
337
|
+
assert "string" in tree.pretty()
|
|
338
|
+
|
|
339
|
+
|
|
330
340
|
# Test for strings in a larger expression (within arithmetic expression)
|
|
331
341
|
def test_string_in_expression(parser):
|
|
332
342
|
tree = parser.parse_only('"hello" + "world"')
|
|
@@ -6,7 +6,13 @@ from lark import Token
|
|
|
6
6
|
|
|
7
7
|
from ransack.exceptions import RansackError, ShapeError
|
|
8
8
|
from ransack.parser import Parser
|
|
9
|
-
from ransack.transformer import
|
|
9
|
+
from ransack.transformer import (
|
|
10
|
+
ExpressionTransformer,
|
|
11
|
+
Filter,
|
|
12
|
+
SQLInterpreter,
|
|
13
|
+
TokenWrapper,
|
|
14
|
+
get_values,
|
|
15
|
+
)
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
@pytest.fixture
|
|
@@ -500,3 +506,210 @@ class TestFilter:
|
|
|
500
506
|
exp_tree = expr_transformer.transform(parser.parse_only("my_var == 14"))
|
|
501
507
|
res = filter_.transform(exp_tree)
|
|
502
508
|
assert res
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@pytest.fixture
|
|
512
|
+
def sql():
|
|
513
|
+
return SQLInterpreter()
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class TestSQLInterpreter:
|
|
517
|
+
def parse_to_sql(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
|
+
("expression", "expected"),
|
|
530
|
+
[
|
|
531
|
+
# Test basic types
|
|
532
|
+
("5", "5"),
|
|
533
|
+
("-12", "-12"),
|
|
534
|
+
("23.22", "23.22"),
|
|
535
|
+
("-0.224", "-0.224"),
|
|
536
|
+
("13e19", "1.3e+20"),
|
|
537
|
+
("2025-01-01", "TIMESTAMP WITH TIME ZONE '2025-01-01T00:00:00+00:00'"),
|
|
538
|
+
(
|
|
539
|
+
"2025-01-01T22:14:24.142+02:00",
|
|
540
|
+
"TIMESTAMP WITH TIME ZONE '2025-01-01T22:14:24.142000+02:00'",
|
|
541
|
+
),
|
|
542
|
+
("01:23:34", "INTERVAL '1:23:34'"),
|
|
543
|
+
("10D23:59:59", "INTERVAL '10 days, 23:59:59'"),
|
|
544
|
+
("99:59:59", "INTERVAL '4 days, 3:59:59'"),
|
|
545
|
+
("'string'", "'string'"),
|
|
546
|
+
("192.168.0.0", "ipaddress '192.168.0.0'"),
|
|
547
|
+
("192.168.0.0/16", "iprange '192.168.0.0/16'"),
|
|
548
|
+
("192.168.0.10-192.168.0.50", "iprange '192.168.0.10-192.168.0.50'"),
|
|
549
|
+
("fd00::1", "ipaddress 'fd00::1'"),
|
|
550
|
+
("fd00::/8", "iprange 'fd00::/8'"),
|
|
551
|
+
("fd00::1-fd00::10", "iprange 'fd00::1-fd00::10'"),
|
|
552
|
+
# Test collections
|
|
553
|
+
("[1, 2, 3]", "ARRAY[1, 2, 3]"),
|
|
554
|
+
(
|
|
555
|
+
"[2024-12-12, 2025-12-12]",
|
|
556
|
+
(
|
|
557
|
+
"ARRAY[TIMESTAMP WITH TIME ZONE '2024-12-12T00:00:00+00:00', "
|
|
558
|
+
"TIMESTAMP WITH TIME ZONE '2025-12-12T00:00:00+00:00']"
|
|
559
|
+
),
|
|
560
|
+
),
|
|
561
|
+
("[]", "ARRAY[]"),
|
|
562
|
+
(
|
|
563
|
+
"100..1",
|
|
564
|
+
(
|
|
565
|
+
"CASE WHEN 100 < 1 "
|
|
566
|
+
"THEN int4range(100, 1, '[]') "
|
|
567
|
+
"ELSE int4range(1, 100, '[]') END"
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
(
|
|
571
|
+
"100.12..1.14",
|
|
572
|
+
(
|
|
573
|
+
"CASE WHEN 100.12 < 1.14 "
|
|
574
|
+
"THEN numrange(100.12, 1.14, '[]') "
|
|
575
|
+
"ELSE numrange(1.14, 100.12, '[]') END"
|
|
576
|
+
),
|
|
577
|
+
),
|
|
578
|
+
(
|
|
579
|
+
"-10..10",
|
|
580
|
+
(
|
|
581
|
+
"CASE WHEN -10 < 10 "
|
|
582
|
+
"THEN int4range(-10, 10, '[]') "
|
|
583
|
+
"ELSE int4range(10, -10, '[]') END"
|
|
584
|
+
),
|
|
585
|
+
),
|
|
586
|
+
(
|
|
587
|
+
"2024-01-01..2024-12-31",
|
|
588
|
+
(
|
|
589
|
+
"CASE WHEN TIMESTAMP WITH TIME ZONE '2024-01-01T00:00:00+00:00' "
|
|
590
|
+
"< TIMESTAMP WITH TIME ZONE '2024-12-31T00:00:00+00:00' THEN "
|
|
591
|
+
"tstzrange(TIMESTAMP WITH TIME ZONE '2024-01-01T00:00:00+00:00', "
|
|
592
|
+
"TIMESTAMP WITH TIME ZONE '2024-12-31T00:00:00+00:00', '[]') ELSE "
|
|
593
|
+
"tstzrange(TIMESTAMP WITH TIME ZONE '2024-12-31T00:00:00+00:00', "
|
|
594
|
+
"TIMESTAMP WITH TIME ZONE '2024-01-01T00:00:00+00:00', '[]') END"
|
|
595
|
+
),
|
|
596
|
+
),
|
|
597
|
+
(
|
|
598
|
+
"192.168.0.10..192.168.0.50",
|
|
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",
|
|
606
|
+
(
|
|
607
|
+
'CASE WHEN "a" < "b" '
|
|
608
|
+
'THEN int4range("a", "b", \'[]\') '
|
|
609
|
+
'ELSE int4range("b", "a", \'[]\') END'
|
|
610
|
+
),
|
|
611
|
+
),
|
|
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
|
+
(
|
|
618
|
+
"1 = 1..10",
|
|
619
|
+
(
|
|
620
|
+
"CASE WHEN 1 < 10 "
|
|
621
|
+
"THEN int4range(1, 10, '[]') "
|
|
622
|
+
"ELSE int4range(10, 1, '[]') END @> 1"
|
|
623
|
+
),
|
|
624
|
+
),
|
|
625
|
+
(
|
|
626
|
+
"1..10 = 1",
|
|
627
|
+
(
|
|
628
|
+
"CASE WHEN 1 < 10 "
|
|
629
|
+
"THEN int4range(1, 10, '[]') "
|
|
630
|
+
"ELSE int4range(10, 1, '[]') END @> 1"
|
|
631
|
+
),
|
|
632
|
+
),
|
|
633
|
+
("0D12:42:24 = 12:42:24", "INTERVAL '12:42:24' = INTERVAL '12:42:24'"),
|
|
634
|
+
(
|
|
635
|
+
"a_int..b_int = c_int..d_int",
|
|
636
|
+
(
|
|
637
|
+
'CASE WHEN "a" < "b" '
|
|
638
|
+
'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'])"
|
|
684
|
+
),
|
|
685
|
+
),
|
|
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
|
+
],
|
|
692
|
+
)
|
|
693
|
+
def test_to_sql(self, parser, sql, expression, expected):
|
|
694
|
+
result = self.parse_to_sql(parser, sql, expression)
|
|
695
|
+
assert result == expected
|
|
696
|
+
|
|
697
|
+
def to_sql_and_expect_error(self, parser, sql, expression):
|
|
698
|
+
"""Parse the expression and expect an exception to be raised."""
|
|
699
|
+
tree = parser.parse(expression)
|
|
700
|
+
with pytest.raises(RansackError):
|
|
701
|
+
sql.to_sql(tree)
|
|
702
|
+
|
|
703
|
+
@pytest.mark.parametrize(
|
|
704
|
+
"expression",
|
|
705
|
+
[
|
|
706
|
+
"2000-12-24..192.168.0.50", # Unsupported type of range
|
|
707
|
+
"not_there", # Unknown column
|
|
708
|
+
],
|
|
709
|
+
)
|
|
710
|
+
def test_invalid_cases(self, parser, sql, expression):
|
|
711
|
+
self.to_sql_and_expect_error(
|
|
712
|
+
parser,
|
|
713
|
+
sql,
|
|
714
|
+
expression,
|
|
715
|
+
)
|
|
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
|