ransacklib 1.1.0.dev6__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.
- {ransacklib-1.1.0.dev6/ransacklib.egg-info → ransacklib-1.1.0.dev7}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/pyproject.toml +1 -1
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransack/operator.py +74 -4
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransack/parser.py +10 -2
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransack/transformer.py +39 -11
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7/ransacklib.egg-info}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/tests/test_transformer.py +87 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/LICENSE +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/README.rst +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransack/__init__.py +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransack/exceptions.py +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransack/function.py +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransack/py.typed +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/SOURCES.txt +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/dependency_links.txt +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/requires.txt +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/ransacklib.egg-info/top_level.txt +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/setup.cfg +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/tests/test_operator.py +0 -0
- {ransacklib-1.1.0.dev6 → ransacklib-1.1.0.dev7}/tests/test_parser.py +0 -0
|
@@ -501,9 +501,40 @@ def binary_operation(operator: str, left: Any, right: Any) -> Any:
|
|
|
501
501
|
|
|
502
502
|
|
|
503
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
|
+
|
|
504
525
|
def _get_comp_dict_sql(
|
|
505
526
|
op: str, tuple_repr_f
|
|
506
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
|
+
"""
|
|
507
538
|
return {
|
|
508
539
|
(Number, Number): (f"{l_sql} {op} {r_sql}", False),
|
|
509
540
|
(Number, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
@@ -515,10 +546,10 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any, bool]:
|
|
|
515
546
|
(timedelta, list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
516
547
|
("ip", "ip"): (f"{l_sql} {op} {r_sql}", False),
|
|
517
548
|
("ip", list): (f"{l_sql} {op} ANY({r_sql})", False),
|
|
518
|
-
# (list, list): f"{l_sql} && {r_sql}",
|
|
519
|
-
# (tuple, tuple): f"upper({l_sql}) > lower({r_sql})",
|
|
520
549
|
}
|
|
521
550
|
|
|
551
|
+
# Comparison and logical operators (result is always boolean).
|
|
552
|
+
# The boolean flag indicates whether operand order was reversed.
|
|
522
553
|
d: dict[str, dict] = {
|
|
523
554
|
"=": {
|
|
524
555
|
(Number, Number): (f"{l_sql} = {r_sql}", False),
|
|
@@ -546,8 +577,16 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any, bool]:
|
|
|
546
577
|
| {(tuple, tuple): (f"lower({l_sql}) <= upper({r_sql})", False)},
|
|
547
578
|
"and": {(bool, bool): (f"{l_sql} AND {r_sql}", False)},
|
|
548
579
|
"or": {(bool, bool): (f"({l_sql} OR {r_sql})", False)},
|
|
549
|
-
"contains": {
|
|
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
|
+
},
|
|
550
587
|
}
|
|
588
|
+
|
|
589
|
+
# Arithmetic operators (result type varies, operands are not swapped).
|
|
551
590
|
d2: dict[str, dict] = {
|
|
552
591
|
"+": {
|
|
553
592
|
(Number, Number): (f"({l_sql} + {r_sql})", Number),
|
|
@@ -572,14 +611,21 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any, bool]:
|
|
|
572
611
|
(Number, Number): (f"({l_sql} % {r_sql})", Number),
|
|
573
612
|
},
|
|
574
613
|
}
|
|
575
|
-
|
|
614
|
+
|
|
615
|
+
if op == "==" and t1 == t2:
|
|
576
616
|
return (f"{l_sql} = {r_sql}", bool, False)
|
|
617
|
+
|
|
618
|
+
# 'in' operator behaves the same as '=' operator.
|
|
577
619
|
if op == "in":
|
|
578
620
|
sql, switch = d["="][(t1, t2)]
|
|
579
621
|
return sql, bool, switch
|
|
622
|
+
|
|
623
|
+
# Arithmetic operator handling.
|
|
580
624
|
if op in d2:
|
|
581
625
|
sql, type_ = d2[op][(t1, t2)]
|
|
582
626
|
return sql, type_, False
|
|
627
|
+
|
|
628
|
+
# Default: comparison/logical operator.
|
|
583
629
|
sql, switch = d[op][(t1, t2)]
|
|
584
630
|
return sql, bool, switch
|
|
585
631
|
|
|
@@ -587,6 +633,30 @@ def _operator_map_sql(op, l_sql, r_sql, t1, t2) -> tuple[str, Any, bool]:
|
|
|
587
633
|
def binary_operation_sql(
|
|
588
634
|
operator: str, left: str, right: str, l_type: Any, r_type: Any
|
|
589
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
|
+
"""
|
|
590
660
|
# Normalize numeric types
|
|
591
661
|
l_type = Number if l_type in (int, float) else l_type
|
|
592
662
|
r_type = Number if r_type in (int, float) else r_type
|
|
@@ -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__(
|
|
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__(
|
|
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
|
-
[
|
|
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
|
|
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
|
|
|
@@ -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
|
|
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,
|
|
@@ -903,7 +924,7 @@ class SQLInterpreter(Interpreter):
|
|
|
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,
|
|
@@ -988,14 +1009,21 @@ class SQLInterpreter(Interpreter):
|
|
|
988
1009
|
|
|
989
1010
|
args_sql = []
|
|
990
1011
|
params = []
|
|
1012
|
+
types = []
|
|
991
1013
|
|
|
992
1014
|
for arg in args.children if args else []:
|
|
993
|
-
sql, p,
|
|
1015
|
+
sql, p, type_ = self.visit(arg)
|
|
994
1016
|
args_sql.append(sql)
|
|
995
1017
|
params.extend(p)
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
|
999
1027
|
raise EvaluationError(
|
|
1000
1028
|
f"Function '{name.real_value}' was not found.",
|
|
1001
1029
|
line=name.line,
|
|
@@ -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
|
|
|
@@ -676,6 +739,16 @@ class TestSQLInterpreter:
|
|
|
676
739
|
("a_int??14", 'COALESCE("a", %s)', [14]),
|
|
677
740
|
# Test contains operator.
|
|
678
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
|
+
),
|
|
679
752
|
# Test = operator.
|
|
680
753
|
("1 = 1", "%s = %s", [1, 1]),
|
|
681
754
|
("1 = [1, 2, 3]", "%s = ANY(ARRAY[%s, %s, %s])", [1, 1, 2, 3]),
|
|
@@ -762,6 +835,20 @@ class TestSQLInterpreter:
|
|
|
762
835
|
from_str("192.168.0.0/16"),
|
|
763
836
|
],
|
|
764
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]),
|
|
765
852
|
],
|
|
766
853
|
)
|
|
767
854
|
def test_parse_sql(self, parser, sql, expr, expected_sql, expected_params):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|