ransacklib 1.1.0.dev4__tar.gz → 1.1.0.dev6__tar.gz

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