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.
Files changed (20) hide show
  1. {ransacklib-1.1.0.dev3/ransacklib.egg-info → ransacklib-1.1.0.dev5}/PKG-INFO +1 -1
  2. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/pyproject.toml +2 -1
  3. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/operator.py +1 -1
  4. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/transformer.py +89 -64
  5. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5/ransacklib.egg-info}/PKG-INFO +1 -1
  6. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/tests/test_transformer.py +113 -141
  7. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/LICENSE +0 -0
  8. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/README.rst +0 -0
  9. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/__init__.py +0 -0
  10. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/exceptions.py +0 -0
  11. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/function.py +0 -0
  12. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/parser.py +0 -0
  13. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransack/py.typed +0 -0
  14. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/SOURCES.txt +0 -0
  15. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/dependency_links.txt +0 -0
  16. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/requires.txt +0 -0
  17. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/top_level.txt +0 -0
  18. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/setup.cfg +0 -0
  19. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/tests/test_operator.py +0 -0
  20. {ransacklib-1.1.0.dev3 → ransacklib-1.1.0.dev5}/tests/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ransacklib
3
- Version: 1.1.0.dev3
3
+ Version: 1.1.0.dev5
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ransacklib"
7
- version = "1.1.0.dev3"
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}", timedelta),
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 (str(token.real_value), type(token.real_value))
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
- type_ = "TIMESTAMP WITH TIME ZONE"
836
+ cast = "TIMESTAMP WITH TIME ZONE"
837
837
  else:
838
- type_ = "TIMESTAMP"
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 (f"INTERVAL '{token.real_value!s}'", type(token.real_value))
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 (f"'{token.real_value}'", str)
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} '{token.real_value!s}'", type(token.real_value))
853
+ return (f"%s::{cast}", [token.real_value], type(token.real_value))
857
854
 
858
- def neg(self, tree: Tree) -> tuple[str, Any]:
859
- value, type_ = self.visit(tree)
860
- return (f"-{value}", type_)
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 binary_operation_sql(operator, l_sql, r_sql, l_type, r_type)
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("-", l_tree, r_tree)
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 (f"NOT {sql}", type_)
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(self, l_tree: Tree, r_tree: Tree) -> tuple[str, type[bool]]:
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
- var, _ = self.var_from_data(TokenWrapper(path, path))
978
- return f"{var!s} is not null", bool
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
- var, _ = self.var_from_data(TokenWrapper(path, path))
982
- sql, type_ = self.visit(default)
983
- return f"COALESCE({var!s}, {sql})", type_
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ransacklib
3
- Version: 1.1.0.dev3
3
+ Version: 1.1.0.dev5
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
@@ -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 parse_to_sql(self, parser, sql, expression):
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
- ("expression", "expected"),
529
+ ("expr", "expected_sql", "expected_params"),
530
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 '2025-01-01T00:00:00'"),
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 '2025-01-01T22:14:24.142000+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
+ ],
541
554
  ),
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]"),
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
- "[2024-12-12, 2025-12-12]",
556
- (
557
- "ARRAY[TIMESTAMP '2024-12-12T00:00:00', "
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
- ("[]", "ARRAY[]"),
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 100 < 1 "
566
- "THEN int4range(100, 1, '[]') "
567
- "ELSE int4range(1, 100, '[]') END"
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 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"
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 '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"
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
- "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",
617
+ "2024-01-01T00:00:00..2024-12-31T00:00:00",
606
618
  (
607
- 'CASE WHEN "a" < "b" '
608
- 'THEN int4range("a", "b", \'[]\') '
609
- 'ELSE int4range("b", "a", \'[]\') END'
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
- "1 = 1..10",
619
- (
620
- "CASE WHEN 1 < 10 "
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 = 1",
627
- (
628
- "CASE WHEN 1 < 10 "
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
- ("0D12:42:24 = 12:42:24", "INTERVAL '12:42:24' = INTERVAL '12:42:24'"),
651
+ # Test variable access.
652
+ ("a_int", '"a"', []),
634
653
  (
635
- "a_int..b_int = c_int..d_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 test_to_sql(self, parser, sql, expression, expected):
694
- result = self.parse_to_sql(parser, sql, expression)
695
- assert result == expected
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