ransacklib 1.1.0.dev5__tar.gz → 1.1.0.dev7__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.dev5/ransacklib.egg-info → ransacklib-1.1.0.dev7}/PKG-INFO +1 -1
  2. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/pyproject.toml +1 -1
  3. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/operator.py +128 -50
  4. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/parser.py +10 -2
  5. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/transformer.py +67 -38
  6. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7/ransacklib.egg-info}/PKG-INFO +1 -1
  7. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/tests/test_transformer.py +193 -3
  8. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/LICENSE +0 -0
  9. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/README.rst +0 -0
  10. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/__init__.py +0 -0
  11. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/exceptions.py +0 -0
  12. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/function.py +0 -0
  13. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransack/py.typed +0 -0
  14. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/SOURCES.txt +0 -0
  15. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/dependency_links.txt +0 -0
  16. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/requires.txt +0 -0
  17. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/top_level.txt +0 -0
  18. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/setup.cfg +0 -0
  19. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/tests/test_operator.py +0 -0
  20. {ransacklib-1.1.0.dev5 → ransacklib-1.1.0.dev7}/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.dev5
3
+ Version: 1.1.0.dev7
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.dev5"
7
+ version = "1.1.0.dev7"
8
8
  description = "A modern, extensible language for manipulation with structured data"
9
9
  license = "MIT"
10
10
  readme = "README.rst"
@@ -500,63 +500,104 @@ 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
+ """
505
+ Maps an expression into its SQL representation.
506
+
507
+ Args:
508
+ op: Operator string (e.g., "=", "+", "and", "contains").
509
+ l_sql: SQL string representing the left operand.
510
+ r_sql: SQL string representing the right operand.
511
+ t1: Resolved type of the left operand.
512
+ t2: Resolved type of the right operand.
513
+
514
+ Returns:
515
+ A tuple of:
516
+ - SQL expression string,
517
+ - result type,
518
+ - switch flag (True if operands were reversed semantically).
519
+
520
+ Raises:
521
+ KeyError:
522
+ If no mapping exists for the given operator and operand types.
523
+ """
524
+
525
+ def _get_comp_dict_sql(
526
+ op: str, tuple_repr_f
527
+ ) -> dict[tuple[Operand, Operand], tuple[str, bool]]:
528
+ """
529
+ Generate comparison operator mappings for multiple type combinations.
530
+
531
+ Args:
532
+ op: SQL comparison operator (e.g., ">", "<=").
533
+ tuple_repr_f: SQL function name used for tuple bounds (e.g. "lower").
534
+
535
+ Returns:
536
+ Mapping from (type1, type2) -> (SQL string, switch flag).
537
+ """
505
538
  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})",
539
+ (Number, Number): (f"{l_sql} {op} {r_sql}", False),
540
+ (Number, list): (f"{l_sql} {op} ANY({r_sql})", False),
541
+ (Number, tuple): (f"{l_sql} {op} {tuple_repr_f}({r_sql})", False),
542
+ (datetime, datetime): (f"{l_sql} {op} {r_sql}", False),
543
+ (datetime, list): (f"{l_sql} {op} ANY({r_sql})", False),
544
+ (datetime, tuple): (f"{r_sql} {op} {tuple_repr_f}({l_sql})", True),
545
+ (timedelta, timedelta): (f"{l_sql} {op} {r_sql}", False),
546
+ (timedelta, list): (f"{l_sql} {op} ANY({r_sql})", False),
547
+ ("ip", "ip"): (f"{l_sql} {op} {r_sql}", False),
548
+ ("ip", list): (f"{l_sql} {op} ANY({r_sql})", False),
518
549
  }
519
550
 
551
+ # Comparison and logical operators (result is always boolean).
552
+ # The boolean flag indicates whether operand order was reversed.
520
553
  d: dict[str, dict] = {
521
554
  "=": {
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}",
555
+ (Number, Number): (f"{l_sql} = {r_sql}", False),
556
+ (Number, list): (f"{l_sql} = ANY({r_sql})", False),
557
+ (Number, tuple): (f"{r_sql} @> {l_sql}", True),
558
+ (datetime, datetime): (f"{l_sql} = {r_sql}", False),
559
+ (datetime, list): (f"{l_sql} = ANY({r_sql})", False),
560
+ (datetime, tuple): (f"{r_sql} @> {l_sql}", True),
561
+ (timedelta, timedelta): (f"{l_sql} = {r_sql}", False),
562
+ (timedelta, list): (f"{l_sql} = ANY({r_sql})", False),
563
+ (str, str): (f"{l_sql} = {r_sql}", False),
564
+ (str, list): (f"{l_sql} = ANY({r_sql})", False),
565
+ ("ip", "ip"): (f"{l_sql} && {r_sql}", False),
566
+ ("ip", list): (f"{l_sql} && ANY({r_sql})", False),
567
+ (list, list): (f"{l_sql} && {r_sql}", False),
568
+ (tuple, tuple): (f"{l_sql} && {r_sql}", False),
536
569
  },
537
570
  ">": _get_comp_dict_sql(">", "lower")
538
- | {(tuple, tuple): f"upper({l_sql}) > lower({r_sql})"},
571
+ | {(tuple, tuple): (f"upper({l_sql}) > lower({r_sql})", False)},
539
572
  ">=": _get_comp_dict_sql(">=", "lower")
540
- | {(tuple, tuple): f"upper({l_sql}) >= lower({r_sql})"},
573
+ | {(tuple, tuple): (f"upper({l_sql}) >= lower({r_sql})", False)},
541
574
  "<": _get_comp_dict_sql("<", "upper")
542
- | {(tuple, tuple): f"lower({l_sql}) < upper({r_sql})"},
575
+ | {(tuple, tuple): (f"lower({l_sql}) < upper({r_sql})", False)},
543
576
  "<=": _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"},
577
+ | {(tuple, tuple): (f"lower({l_sql}) <= upper({r_sql})", False)},
578
+ "and": {(bool, bool): (f"{l_sql} AND {r_sql}", False)},
579
+ "or": {(bool, bool): (f"({l_sql} OR {r_sql})", False)},
580
+ "contains": {
581
+ (str, str): (f"position({r_sql} in {l_sql})>0", True),
582
+ (list, str): (
583
+ f"position({r_sql} in array_to_string({l_sql}, ','))>0",
584
+ True,
585
+ ),
586
+ },
548
587
  }
588
+
589
+ # Arithmetic operators (result type varies, operands are not swapped).
549
590
  d2: dict[str, dict] = {
550
591
  "+": {
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),
592
+ (Number, Number): (f"({l_sql} + {r_sql})", Number),
593
+ (datetime, timedelta): (f"({l_sql} + {r_sql})", datetime),
594
+ (timedelta, timedelta): (f"({l_sql} + {r_sql})", timedelta),
554
595
  },
555
596
  "-": {
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),
597
+ (Number, Number): (f"({l_sql} - {r_sql})", Number),
598
+ (datetime, timedelta): (f"({l_sql} - {r_sql})", datetime),
599
+ (timedelta, timedelta): (f"({l_sql} - {r_sql})", timedelta),
600
+ (datetime, datetime): (f"({l_sql} - {r_sql})", timedelta),
560
601
  },
561
602
  "*": {
562
603
  (timedelta, Number): (f"{l_sql} * {r_sql}", timedelta),
@@ -567,21 +608,55 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any]:
567
608
  (Number, Number): (f"{l_sql} / {r_sql}", Number),
568
609
  },
569
610
  "%": {
570
- (Number, Number): (f"{l_sql} % {r_sql}", Number),
611
+ (Number, Number): (f"({l_sql} % {r_sql})", Number),
571
612
  },
572
613
  }
573
- if op == "==":
574
- return (f"{l_sql} = {r_sql}", bool)
614
+
615
+ if op == "==" and t1 == t2:
616
+ return (f"{l_sql} = {r_sql}", bool, False)
617
+
618
+ # 'in' operator behaves the same as '=' operator.
575
619
  if op == "in":
576
- return (d["="][(t1, t2)], bool)
620
+ sql, switch = d["="][(t1, t2)]
621
+ return sql, bool, switch
622
+
623
+ # Arithmetic operator handling.
577
624
  if op in d2:
578
- return d2[op][(t1, t2)]
579
- return (d[op][(t1, t2)], bool)
625
+ sql, type_ = d2[op][(t1, t2)]
626
+ return sql, type_, False
627
+
628
+ # Default: comparison/logical operator.
629
+ sql, switch = d[op][(t1, t2)]
630
+ return sql, bool, switch
580
631
 
581
632
 
582
633
  def binary_operation_sql(
583
634
  operator: str, left: str, right: str, l_type: Any, r_type: Any
584
- ) -> tuple[str, Any]:
635
+ ) -> tuple[str, bool, Any]:
636
+ """
637
+ Converts a binary operation into its SQL equivalent with type handling.
638
+
639
+ This function normalizes operand types (e.g., numeric types, IP types),
640
+ delegates SQL generation to `_operator_map_sql`, and handles operand
641
+ swapping if needed.
642
+
643
+ Args:
644
+ operator: Operator string (e.g., "+", "=", "in").
645
+ left: SQL string for the left operand.
646
+ right: SQL string for the right operand.
647
+ l_type: Type of the left operand.
648
+ r_type: Type of the right operand.
649
+
650
+ Returns:
651
+ A tuple of:
652
+ - SQL expression string,
653
+ - result type,
654
+ - switch flag (True if operands were reversed).
655
+
656
+ Raises:
657
+ OperatorNotFoundError:
658
+ If no valid SQL mapping exists for the operator/type combination.
659
+ """
585
660
  # Normalize numeric types
586
661
  l_type = Number if l_type in (int, float) else l_type
587
662
  r_type = Number if r_type in (int, float) else r_type
@@ -595,7 +670,10 @@ def binary_operation_sql(
595
670
  return _operator_map_sql(operator, left, right, l_type, r_type)
596
671
  except KeyError:
597
672
  try:
598
- return _operator_map_sql(operator, right, left, r_type, l_type)
673
+ sql, type_, switch = _operator_map_sql(
674
+ operator, right, left, r_type, l_type
675
+ )
676
+ return sql, type_, not switch
599
677
  except KeyError:
600
678
  pass
601
679
  raise OperatorNotFoundError(operator, (l_type, r_type), (left, right)) from None
@@ -12,6 +12,7 @@ Classes:
12
12
  """
13
13
 
14
14
  import os
15
+ from datetime import tzinfo
15
16
  from pathlib import Path
16
17
  from typing import Any, no_type_check
17
18
 
@@ -120,7 +121,11 @@ class Parser:
120
121
  %ignore WS
121
122
  """ # noqa
122
123
 
123
- def __init__(self, context: dict[str, Any] | None = None) -> None:
124
+ def __init__(
125
+ self,
126
+ context: dict[str, Any] | None = None,
127
+ default_timezone: tzinfo | None = None,
128
+ ) -> None:
124
129
  """
125
130
  Initialize the Parser.
126
131
 
@@ -128,6 +133,9 @@ class Parser:
128
133
  context (dict[str, Any] | None): Optional dictionary of variables that can
129
134
  be referenced in queries. Context variables override data variables unless
130
135
  the data variable is explicitly accessed with a leading dot.
136
+ default_timezone (tzinfo | None): Time zone to apply to parsed datetime
137
+ values that do not include time zone information. If None, naive datetimes
138
+ remain naive.
131
139
  """
132
140
  # Determine the cache file of the grammar. By default, it's in the home
133
141
  # directory. But when the environment variable is set, use the value
@@ -142,7 +150,7 @@ class Parser:
142
150
  propagate_positions=True,
143
151
  cache=cache_path,
144
152
  )
145
- self.shaper = ExpressionTransformer(context)
153
+ self.shaper = ExpressionTransformer(context, default_timezone)
146
154
 
147
155
  @no_type_check
148
156
  def parse(self, data: str) -> Tree[Token]:
@@ -20,7 +20,7 @@ Classes:
20
20
 
21
21
  import re
22
22
  from collections.abc import Mapping, MutableSequence
23
- from datetime import datetime, timedelta
23
+ from datetime import datetime, timedelta, tzinfo
24
24
  from typing import Any, cast
25
25
 
26
26
  from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
@@ -194,7 +194,11 @@ class ExpressionTransformer(Transformer):
194
194
  into appropriate objects (ipranges, datetime, list, ...).
195
195
  """
196
196
 
197
- def __init__(self, context: dict[str, Any] | None = None) -> None:
197
+ def __init__(
198
+ self,
199
+ context: dict[str, Any] | None = None,
200
+ default_timezone: tzinfo | None = None,
201
+ ) -> None:
198
202
  """
199
203
  Initializes the ExpressionTransformer with a context dictionary
200
204
  for constants.
@@ -203,8 +207,12 @@ class ExpressionTransformer(Transformer):
203
207
  context: A dictionary containing constant values that can be
204
208
  accessed by variable names within expressions.
205
209
  Defaults to an empty dictionary if none is provided.
210
+ default_timezone: Time zone to apply to parsed datetime values
211
+ that do not include time zone information.
212
+ If None, naive datetimes remain naive.
206
213
  """
207
214
  self.context = context if context is not None else {}
215
+ self.default_timezone = default_timezone
208
216
 
209
217
  def number(self, data: Token) -> Tree[TokenWrapper]:
210
218
  """
@@ -322,6 +330,11 @@ class ExpressionTransformer(Transformer):
322
330
 
323
331
  # Convert to datetime object
324
332
  dtime = datetime.fromisoformat(datetime_str)
333
+
334
+ # If no time zone was specified, then set to default time zone
335
+ if dtime.tzinfo is None:
336
+ dtime = dtime.replace(tzinfo=self.default_timezone)
337
+
325
338
  tw = TokenWrapper(_add_tokens(date, time), dtime)
326
339
 
327
340
  return Tree("datetime", [tw], _create_meta(tw))
@@ -337,7 +350,14 @@ class ExpressionTransformer(Transformer):
337
350
  """
338
351
  return Tree(
339
352
  "datetime",
340
- [TokenWrapper(date, datetime.fromisoformat(date + "T00:00:00"))],
353
+ [
354
+ TokenWrapper(
355
+ date,
356
+ datetime.fromisoformat(date + "T00:00:00").replace(
357
+ tzinfo=self.default_timezone
358
+ ),
359
+ )
360
+ ],
341
361
  _create_meta(date),
342
362
  )
343
363
 
@@ -819,7 +839,8 @@ class SQLInterpreter(Interpreter):
819
839
  data: Optional dictionary containing the definition of queried columns.
820
840
 
821
841
  Returns:
822
- A part of the SQL query - the part after 'SELECT' or 'WHERE'.
842
+ A part of the SQL query (the part after 'SELECT' or 'WHERE') and its
843
+ parameters.
823
844
  """
824
845
  self.data = data if data is not None else {}
825
846
 
@@ -852,13 +873,13 @@ class SQLInterpreter(Interpreter):
852
873
  cast = "iprange"
853
874
  return (f"%s::{cast}", [token.real_value], type(token.real_value))
854
875
 
855
- def neg(self, tree: Tree) -> tuple[str, list[Any], Any]:
876
+ def neg(self, tree: Tree) -> tuple[str, list, Any]:
856
877
  sql, params, type_ = self.visit(tree)
857
- return (f"-{sql}", params, type_)
878
+ return (f"(-{sql})", params, type_)
858
879
 
859
880
  def range_op(
860
881
  self, l_tree: Tree, r_tree: Tree
861
- ) -> tuple[str, list[Any], type[tuple] | type[IP]]:
882
+ ) -> tuple[str, list, type[tuple] | type[IP]]:
862
883
  l_sql, l_params, l_type = self.visit(l_tree)
863
884
  r_sql, r_params, r_type = self.visit(r_tree)
864
885
 
@@ -867,7 +888,7 @@ class SQLInterpreter(Interpreter):
867
888
  elif issubclass(l_type, float) and issubclass(r_type, float):
868
889
  range_type = "numrange"
869
890
  elif issubclass(l_type, datetime) and issubclass(r_type, datetime):
870
- if "WITH TIME ZONE" in l_sql or "WITH TIME ZONE" in r_sql:
891
+ if "WITH TIME ZONE" in l_sql and "WITH TIME ZONE" in r_sql:
871
892
  range_type = "tstzrange"
872
893
  else:
873
894
  range_type = "tsrange"
@@ -879,7 +900,7 @@ class SQLInterpreter(Interpreter):
879
900
  )
880
901
  else:
881
902
  raise EvaluationError(
882
- f"Range operator is not supported for types '{l_type}' and '{r_type}'",
903
+ f"Range operator is not supported for types '{l_type}' and '{r_type}'.",
883
904
  line=l_tree.meta.line,
884
905
  column=l_tree.meta.column,
885
906
  start_pos=l_tree.meta.start_pos,
@@ -898,12 +919,12 @@ class SQLInterpreter(Interpreter):
898
919
  tuple,
899
920
  )
900
921
 
901
- def var_from_data(self, var: TokenWrapper) -> tuple[str, list[Any], Any]:
922
+ def var_from_data(self, var: TokenWrapper) -> tuple[str, list, Any]:
902
923
  if var.real_value in self.data:
903
924
  column, type_ = self.data[var.real_value]
904
925
  return (f'"{column}"', [], type_)
905
926
  raise EvaluationError(
906
- f"The type of column '{var.real_value}' was not defined",
927
+ f"The type of column '{var.real_value}' was not defined.",
907
928
  line=var.line,
908
929
  column=var.column,
909
930
  start_pos=var.start_pos,
@@ -912,68 +933,69 @@ class SQLInterpreter(Interpreter):
912
933
  end_pos=var.end_pos,
913
934
  )
914
935
 
915
- def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, list[Any], Any]:
936
+ def _binary_operation(self, operator, l_tree, r_tree) -> tuple[str, list, Any]:
916
937
  l_sql, l_params, l_type = self.visit(l_tree)
917
938
  r_sql, r_params, r_type = self.visit(r_tree)
918
939
 
919
- sql, type_ = binary_operation_sql(operator, l_sql, r_sql, l_type, r_type)
940
+ sql, type_, switch = binary_operation_sql(
941
+ operator, l_sql, r_sql, l_type, r_type
942
+ )
943
+ params = l_params + r_params if not switch else r_params + l_params
920
944
 
921
- return sql, l_params + r_params, type_
945
+ return sql, params, type_
922
946
 
923
- def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
947
+ def add(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
924
948
  return self._binary_operation("+", l_tree, r_tree)
925
949
 
926
- def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
950
+ def sub(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
927
951
  return self._binary_operation("-", l_tree, r_tree)
928
952
 
929
- def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
953
+ def mul(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
930
954
  return self._binary_operation("*", l_tree, r_tree)
931
955
 
932
- def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
956
+ def div(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
933
957
  return self._binary_operation("/", l_tree, r_tree)
934
958
 
935
- def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], Any]:
959
+ def mod(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, Any]:
936
960
  return self._binary_operation("%", l_tree, r_tree)
937
961
 
938
- def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
962
+ def eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
939
963
  return self._binary_operation("==", l_tree, r_tree)
940
964
 
941
- def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
965
+ def in_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
942
966
  return self._binary_operation("in", l_tree, r_tree)
943
967
 
944
- def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
968
+ def any_eq(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
945
969
  return self._binary_operation("=", l_tree, r_tree)
946
970
 
947
- def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
971
+ def gt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
948
972
  return self._binary_operation(">", l_tree, r_tree)
949
973
 
950
- def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
974
+ def gte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
951
975
  return self._binary_operation(">=", l_tree, r_tree)
952
976
 
953
- def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
977
+ def lt(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
954
978
  return self._binary_operation("<", l_tree, r_tree)
955
979
 
956
- def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
980
+ def lte(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
957
981
  return self._binary_operation("<=", l_tree, r_tree)
958
982
 
959
- def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
983
+ def or_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
960
984
  return self._binary_operation("or", l_tree, r_tree)
961
985
 
962
- def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list[Any], type[bool]]:
986
+ def and_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
963
987
  return self._binary_operation("and", l_tree, r_tree)
964
988
 
965
- def not_op(self, tree: Tree) -> tuple[str, list[Any], type[bool]]:
989
+ def not_op(self, tree: Tree) -> tuple[str, list, type[bool]]:
966
990
  sql, params, type_ = self.visit(tree)
967
991
  return f"NOT {sql}", params, type_
968
992
 
969
- def contains_op(
970
- self, l_tree: Tree, r_tree: Tree
971
- ) -> tuple[str, list[Any], type[bool]]:
993
+ def contains_op(self, l_tree: Tree, r_tree: Tree) -> tuple[str, list, type[bool]]:
972
994
  return self._binary_operation("contains", l_tree, r_tree)
973
995
 
974
- def exists_op(self, path: Token) -> tuple[str, list[Any], type[bool]]:
996
+ def exists_op(self, path: Token) -> tuple[str, list, type[bool]]:
975
997
  var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
976
- return f"{var_sql} is not null", [], bool
998
+ return f"{var_sql} IS NOT NULL", [], bool
977
999
 
978
1000
  def exists_with_default(self, path: Token, default: Any) -> Any:
979
1001
  var_sql, _, _ = self.var_from_data(TokenWrapper(path, path))
@@ -987,14 +1009,21 @@ class SQLInterpreter(Interpreter):
987
1009
 
988
1010
  args_sql = []
989
1011
  params = []
1012
+ types = []
990
1013
 
991
1014
  for arg in args.children if args else []:
992
- sql, p, _ = self.visit(arg)
1015
+ sql, p, type_ = self.visit(arg)
993
1016
  args_sql.append(sql)
994
1017
  params.extend(p)
995
-
996
- if function_name == "now":
997
- return "localtimestamp", [], datetime
1018
+ types.append(type_)
1019
+
1020
+ if function_name == "now" and len(args_sql) == 0:
1021
+ return "localtimestamp", params, datetime
1022
+ if function_name in ("len", "length") and len(args_sql) == 1:
1023
+ if types[0] is list:
1024
+ return f"array_length({args_sql[0]}, 1)", params, int
1025
+ if types[0] is str:
1026
+ return f"length({args_sql[0]})", params, int
998
1027
  raise EvaluationError(
999
1028
  f"Function '{name.real_value}' was not found.",
1000
1029
  line=name.line,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ransacklib
3
- Version: 1.1.0.dev5
3
+ Version: 1.1.0.dev7
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
@@ -213,6 +213,68 @@ class TestExpressionTransformer:
213
213
  assert result_data.children[0].real_value == "my_var"
214
214
 
215
215
 
216
+ class TestDefaultTimezone:
217
+ def test_naive_datetime_gets_default_timezone(self):
218
+ parser = Parser(default_timezone=timezone.utc)
219
+
220
+ result = parser.parse("2024-01-01T12:00:00").children[0].real_value
221
+
222
+ assert result.tzinfo == timezone.utc
223
+ assert str(result) == "2024-01-01 12:00:00+00:00"
224
+
225
+ result = parser.parse("2024-01-01").children[0].real_value
226
+
227
+ assert result.tzinfo == timezone.utc
228
+ assert str(result) == "2024-01-01 00:00:00+00:00"
229
+
230
+ def test_naive_datetime_with_custom_timezone(self):
231
+ custom_tz = timezone(timedelta(hours=2))
232
+ parser = Parser(default_timezone=custom_tz)
233
+
234
+ result = parser.parse("2024-01-01T12:00:00").children[0].real_value
235
+
236
+ assert result.tzinfo == custom_tz
237
+ assert str(result) == "2024-01-01 12:00:00+02:00"
238
+
239
+ result = parser.parse("2024-01-01").children[0].real_value
240
+
241
+ assert result.tzinfo == custom_tz
242
+ assert str(result) == "2024-01-01 00:00:00+02:00"
243
+
244
+ def test_datetime_with_timezone_not_overridden(self):
245
+ parser = Parser(default_timezone=timezone.utc)
246
+
247
+ result = parser.parse("2024-01-01T12:00:00+02:00").children[0].real_value
248
+
249
+ # Should keep original timezone
250
+ assert result.tzinfo.utcoffset(result) == timedelta(hours=2)
251
+ assert str(result) == "2024-01-01 12:00:00+02:00"
252
+
253
+ def test_no_default_timezone_keeps_naive_datetime(self):
254
+ parser = Parser(default_timezone=None)
255
+
256
+ result = parser.parse("2024-01-01T12:00:00").children[0].real_value
257
+
258
+ assert result.tzinfo is None
259
+ assert str(result) == "2024-01-01 12:00:00"
260
+
261
+ result = parser.parse("2024-01-01").children[0].real_value
262
+
263
+ assert result.tzinfo is None
264
+ assert str(result) == "2024-01-01 00:00:00"
265
+
266
+ def test_expression_transformer_default_timezone_direct(self, parser):
267
+ transformer = ExpressionTransformer(default_timezone=timezone.utc)
268
+
269
+ result = (
270
+ transformer.transform(parser.parse_only("2024-01-01T12:00:00"))
271
+ .children[0]
272
+ .real_value
273
+ )
274
+
275
+ assert result.tzinfo == timezone.utc
276
+
277
+
216
278
  class TestFilter:
217
279
  def parse_filter(self, parser, filter_, expression):
218
280
  """Helper to parse, evaluate atoms to Python object, then evaluate."""
@@ -522,6 +584,7 @@ class TestSQLInterpreter:
522
584
  "c_int": ("c", int),
523
585
  "d_int": ("d", int),
524
586
  "Description": ("description", str),
587
+ "Category": ("category", list),
525
588
  }
526
589
  return sql.to_sql(tree, data)
527
590
 
@@ -530,9 +593,9 @@ class TestSQLInterpreter:
530
593
  [
531
594
  # Numbers.
532
595
  ("5", "%s", [5]),
533
- ("-12", "-%s", [12]),
596
+ ("-12", "(-%s)", [12]),
534
597
  ("23.22", "%s", [23.22]),
535
- ("-0.224", "-%s", [0.224]),
598
+ ("-0.224", "(-%s)", [0.224]),
536
599
  ("13e19", "%s", [1.3e20]),
537
600
  # Datetime.
538
601
  ("2025-01-01", "%s::TIMESTAMP", [datetime(2025, 1, 1, 0, 0)]),
@@ -554,7 +617,7 @@ class TestSQLInterpreter:
554
617
  ),
555
618
  # Timedelta.
556
619
  ("01:23:34", "%s", [timedelta(seconds=5014)]),
557
- ("-10D23:59:59", "-%s", [timedelta(days=10, seconds=86399)]),
620
+ ("-10D23:59:59", "(-%s)", [timedelta(days=10, seconds=86399)]),
558
621
  ("99:59:59", "%s", [timedelta(days=4, seconds=14399)]),
559
622
  # Strings.
560
623
  ("''", "%s", [""]),
@@ -659,6 +722,133 @@ class TestSQLInterpreter:
659
722
  ),
660
723
  [],
661
724
  ),
725
+ # Test arithmetic operations.
726
+ (
727
+ "(10 + 12 - 7) * (15 / (8 % 3)) - (-5)",
728
+ "(((%s + %s) - %s) * %s / (%s % %s) - (-%s))",
729
+ [10, 12, 7, 15, 8, 3, 5],
730
+ ),
731
+ # Test comparisons.
732
+ (
733
+ "10 > 12 and 12 < 20 and (17 >= 12 or 12 <= 1) and not 53 == 13",
734
+ "%s > %s AND %s < %s AND (%s >= %s OR %s <= %s) AND NOT %s = %s",
735
+ [10, 12, 12, 20, 17, 12, 12, 1, 53, 13],
736
+ ),
737
+ # Test exists operators.
738
+ ("a_int??", '"a" IS NOT NULL', []),
739
+ ("a_int??14", 'COALESCE("a", %s)', [14]),
740
+ # Test contains operator.
741
+ ("'abcdfg' contains 'bcd'", "position(%s in %s)>0", ["bcd", "abcdfg"]),
742
+ (
743
+ "Category contains 'Attempt'",
744
+ "position(%s in array_to_string(\"category\", ','))>0",
745
+ ["Attempt"],
746
+ ),
747
+ (
748
+ "['abcdfg', 'qwerty', 'azerty'] contains 'bcd'",
749
+ "position(%s in array_to_string(ARRAY[%s, %s, %s], ','))>0",
750
+ ["bcd", "abcdfg", "qwerty", "azerty"],
751
+ ),
752
+ # Test = operator.
753
+ ("1 = 1", "%s = %s", [1, 1]),
754
+ ("1 = [1, 2, 3]", "%s = ANY(ARRAY[%s, %s, %s])", [1, 1, 2, 3]),
755
+ ("[1, 2, 3] = 1", "%s = ANY(ARRAY[%s, %s, %s])", [1, 1, 2, 3]),
756
+ ("[1, 2] = [3, 4]", "ARRAY[%s, %s] && ARRAY[%s, %s]", [1, 2, 3, 4]),
757
+ (
758
+ "2 = 1..10",
759
+ (
760
+ "CASE WHEN %s < %s "
761
+ "THEN int4range(%s, %s, '[]') "
762
+ "ELSE int4range(%s, %s, '[]') END @> %s"
763
+ ),
764
+ [1, 10, 1, 10, 10, 1, 2],
765
+ ),
766
+ (
767
+ "1..10 = 2",
768
+ (
769
+ "CASE WHEN %s < %s "
770
+ "THEN int4range(%s, %s, '[]') "
771
+ "ELSE int4range(%s, %s, '[]') END @> %s"
772
+ ),
773
+ [1, 10, 1, 10, 10, 1, 2],
774
+ ),
775
+ (
776
+ "a_int..b_int = c_int..d_int",
777
+ (
778
+ 'CASE WHEN "a" < "b" '
779
+ 'THEN int4range("a", "b", \'[]\') '
780
+ 'ELSE int4range("b", "a", \'[]\') END '
781
+ '&& CASE WHEN "c" < "d" '
782
+ 'THEN int4range("c", "d", \'[]\') '
783
+ 'ELSE int4range("d", "c", \'[]\') END'
784
+ ),
785
+ [],
786
+ ),
787
+ (
788
+ "10.10.10.10 = 10.10.10.10",
789
+ "%s::ipaddress && %s::ipaddress",
790
+ [from_str("10.10.10.10"), from_str("10.10.10.10")],
791
+ ),
792
+ (
793
+ "10.10.10.10 = 10.10.10.0 .. 10.10.10.20",
794
+ "%s::ipaddress && iprange(%s::ipaddress, %s::ipaddress)",
795
+ [
796
+ from_str("10.10.10.10"),
797
+ from_str("10.10.10.0"),
798
+ from_str("10.10.10.20"),
799
+ ],
800
+ ),
801
+ (
802
+ "192.168.0.5 = 192.168.0.0-192.168.0.255",
803
+ "%s::ipaddress && %s::iprange",
804
+ [from_str("192.168.0.5"), from_str("192.168.0.0-192.168.0.255")],
805
+ ),
806
+ (
807
+ "192.168.0.12 = 192.168.1.0/24",
808
+ "%s::ipaddress && %s::iprange",
809
+ [from_str("192.168.0.12"), from_str("192.168.1.0/24")],
810
+ ),
811
+ (
812
+ "10.10.10.10-10.10.10.20 = 10.10.10.15-10.10.10.25",
813
+ "%s::iprange && %s::iprange",
814
+ [
815
+ from_str("10.10.10.10-10.10.10.20"),
816
+ from_str("10.10.10.15-10.10.10.25"),
817
+ ],
818
+ ),
819
+ (
820
+ "10.10.10.10-10.10.10.20 = 192.168.0.0/16",
821
+ "%s::iprange && %s::iprange",
822
+ [from_str("10.10.10.10-10.10.10.20"), from_str("192.168.0.0/16")],
823
+ ),
824
+ (
825
+ "192.168.1.0/24 = 192.168.0.0/16",
826
+ "%s::iprange && %s::iprange",
827
+ [from_str("192.168.1.0/24"), from_str("192.168.0.0/16")],
828
+ ),
829
+ (
830
+ "192.168.1.10 = [192.168.0.0/24, 192.168.0.0/16]",
831
+ ("%s::ipaddress && ANY(ARRAY[%s::iprange, %s::iprange])"),
832
+ [
833
+ from_str("192.168.1.10"),
834
+ from_str("192.168.0.0/24"),
835
+ from_str("192.168.0.0/16"),
836
+ ],
837
+ ),
838
+ # Test functions.
839
+ (
840
+ "now() > 2025-12-12T12:34:56Z",
841
+ "localtimestamp > %s::TIMESTAMP WITH TIME ZONE",
842
+ [datetime(2025, 12, 12, 12, 34, 56, tzinfo=timezone.utc)],
843
+ ),
844
+ ("length('')", "length(%s)", [""]),
845
+ ("length('abcde')", "length(%s)", ["abcde"]),
846
+ (
847
+ "length([1, 2, 3]) == 3",
848
+ "array_length(ARRAY[%s, %s, %s], 1) = %s",
849
+ [1, 2, 3, 3],
850
+ ),
851
+ ("len(Category) > 1", 'array_length("category", 1) > %s', [1]),
662
852
  ],
663
853
  )
664
854
  def test_parse_sql(self, parser, sql, expr, expected_sql, expected_params):
File without changes