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.
Files changed (21) hide show
  1. {ransacklib-0.1.11/ransacklib.egg-info → ransacklib-1.1.0.dev0}/PKG-INFO +11 -8
  2. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/pyproject.toml +10 -8
  3. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/__init__.py +1 -1
  4. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/operator.py +75 -0
  5. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/parser.py +1 -1
  6. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/transformer.py +157 -1
  7. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0/ransacklib.egg-info}/PKG-INFO +11 -8
  8. ransacklib-1.1.0.dev0/ransacklib.egg-info/requires.txt +12 -0
  9. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/tests/test_parser.py +10 -0
  10. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/tests/test_transformer.py +214 -1
  11. ransacklib-0.1.11/ransacklib.egg-info/requires.txt +0 -10
  12. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/LICENSE +0 -0
  13. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/README.rst +0 -0
  14. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/exceptions.py +0 -0
  15. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/function.py +0 -0
  16. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransack/py.typed +0 -0
  17. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransacklib.egg-info/SOURCES.txt +0 -0
  18. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransacklib.egg-info/dependency_links.txt +0 -0
  19. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/ransacklib.egg-info/top_level.txt +0 -0
  20. {ransacklib-0.1.11 → ransacklib-1.1.0.dev0}/setup.cfg +0 -0
  21. {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: 0.1.11
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.3.0,>=1.2.2
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.2.2,<1.3.0",
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
  ]
@@ -2,7 +2,7 @@ from .exceptions import EvaluationError, ParseError, RansackError, ShapeError
2
2
  from .parser import Parser
3
3
  from .transformer import Filter, get_values
4
4
 
5
- __version__ = "0.1.11"
5
+ __version__ = "1.1.0.dev0"
6
6
 
7
7
  __all__ = (
8
8
  "EvaluationError",
@@ -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: 0.1.11
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.3.0,>=1.2.2
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
@@ -0,0 +1,12 @@
1
+ lark<1.4.0,>=1.3.0
2
+ ipranges<0.2,>=0.1.12
3
+
4
+ [dev]
5
+ pytest<9.0,>=8.4
6
+ pytest-cov<8.0,>=7.0
7
+ sphinx<8.2,>=8.1
8
+ sphinx_rtd_theme<4.0,>=3.0
9
+ mypy<2.0,>=1.18
10
+ ruff<0.15,>=0.14
11
+ build<1.4,>=1.3
12
+ twine<7.0,>=6.2
@@ -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 ExpressionTransformer, Filter, TokenWrapper, get_values
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
+ )
@@ -1,10 +0,0 @@
1
- lark<1.3.0,>=1.2.2
2
- ipranges<0.2,>=0.1.12
3
-
4
- [dev]
5
- pytest
6
- pytest-cov
7
- sphinx
8
- sphinx_rtd_theme
9
- mypy
10
- ruff
File without changes
File without changes
File without changes