pylegend 0.11.0__py3-none-any.whl → 0.13.0__py3-none-any.whl
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.
- pylegend/core/database/sql_to_string/db_extension.py +244 -6
- pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +190 -5
- pylegend/core/language/pandas_api/pandas_api_series.py +3 -0
- pylegend/core/language/shared/expression.py +5 -0
- pylegend/core/language/shared/literal_expressions.py +22 -1
- pylegend/core/language/shared/operations/boolean_operation_expressions.py +144 -0
- pylegend/core/language/shared/operations/date_operation_expressions.py +91 -0
- pylegend/core/language/shared/operations/integer_operation_expressions.py +183 -1
- pylegend/core/language/shared/operations/string_operation_expressions.py +31 -1
- pylegend/core/language/shared/primitives/boolean.py +40 -0
- pylegend/core/language/shared/primitives/date.py +39 -0
- pylegend/core/language/shared/primitives/datetime.py +18 -0
- pylegend/core/language/shared/primitives/integer.py +54 -1
- pylegend/core/language/shared/primitives/strictdate.py +25 -1
- pylegend/core/language/shared/primitives/string.py +16 -2
- pylegend/core/sql/metamodel.py +54 -2
- pylegend/core/sql/metamodel_extension.py +77 -1
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +53 -7
- pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +146 -4
- pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +33 -2
- pylegend/core/tds/pandas_api/frames/functions/assign_function.py +65 -23
- pylegend/core/tds/pandas_api/frames/functions/drop.py +3 -3
- pylegend/core/tds/pandas_api/frames/functions/dropna.py +167 -0
- pylegend/core/tds/pandas_api/frames/functions/fillna.py +162 -0
- pylegend/core/tds/pandas_api/frames/functions/filter.py +10 -5
- pylegend/core/tds/pandas_api/frames/functions/iloc.py +99 -0
- pylegend/core/tds/pandas_api/frames/functions/loc.py +136 -0
- pylegend/core/tds/pandas_api/frames/functions/truncate_function.py +151 -120
- pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +7 -3
- pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +340 -34
- pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +90 -9
- pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_function_input_frame.py +9 -4
- pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_service_input_frame.py +12 -5
- pylegend/extensions/tds/pandas_api/frames/pandas_api_table_spec_input_frame.py +12 -4
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/METADATA +1 -1
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/RECORD +40 -36
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/WHEEL +1 -1
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE.spdx +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -72,6 +72,14 @@ from pylegend.core.sql.metamodel import (
|
|
|
72
72
|
Window,
|
|
73
73
|
TableFunction,
|
|
74
74
|
Union,
|
|
75
|
+
WindowFrame,
|
|
76
|
+
WindowFrameMode,
|
|
77
|
+
FrameBound,
|
|
78
|
+
FrameBoundType,
|
|
79
|
+
BitwiseShiftExpression,
|
|
80
|
+
BitwiseShiftDirection,
|
|
81
|
+
BitwiseBinaryExpression,
|
|
82
|
+
BitwiseBinaryOperator
|
|
75
83
|
)
|
|
76
84
|
from pylegend.core.sql.metamodel_extension import (
|
|
77
85
|
StringLengthExpression,
|
|
@@ -133,6 +141,11 @@ from pylegend.core.sql.metamodel_extension import (
|
|
|
133
141
|
WindowExpression,
|
|
134
142
|
ConstantExpression,
|
|
135
143
|
StringSubStringExpression,
|
|
144
|
+
DateAdjustExpression,
|
|
145
|
+
BitwiseNotExpression,
|
|
146
|
+
DateDiffExpression,
|
|
147
|
+
DateTimeBucketExpression,
|
|
148
|
+
DateType,
|
|
136
149
|
)
|
|
137
150
|
|
|
138
151
|
__all__: PyLegendSequence[str] = [
|
|
@@ -456,6 +469,18 @@ def expression_processor(
|
|
|
456
469
|
return expression.name
|
|
457
470
|
elif isinstance(expression, StringSubStringExpression):
|
|
458
471
|
return extension.process_string_substring_expression(expression, config)
|
|
472
|
+
elif isinstance(expression, DateAdjustExpression):
|
|
473
|
+
return extension.process_date_adjust_expression(expression, config)
|
|
474
|
+
elif isinstance(expression, DateDiffExpression):
|
|
475
|
+
return extension.process_date_diff_expression(expression, config)
|
|
476
|
+
elif isinstance(expression, DateTimeBucketExpression):
|
|
477
|
+
return extension.process_date_time_bucket_expression(expression, config)
|
|
478
|
+
elif isinstance(expression, BitwiseNotExpression):
|
|
479
|
+
return extension.process_bitwise_not_expression(expression, config)
|
|
480
|
+
elif isinstance(expression, BitwiseShiftExpression):
|
|
481
|
+
return extension.process_bitwise_shift_expression(expression, config)
|
|
482
|
+
elif isinstance(expression, BitwiseBinaryExpression):
|
|
483
|
+
return extension.process_bitwise_binary_expression(expression, config)
|
|
459
484
|
|
|
460
485
|
else:
|
|
461
486
|
raise ValueError("Unsupported expression type: " + str(type(expression))) # pragma: no cover
|
|
@@ -516,6 +541,139 @@ def logical_binary_expression_processor(
|
|
|
516
541
|
return f"({left} {op} {right})"
|
|
517
542
|
|
|
518
543
|
|
|
544
|
+
def bitwise_binary_expression_processor(
|
|
545
|
+
bitwise: BitwiseBinaryExpression,
|
|
546
|
+
extension: "SqlToStringDbExtension",
|
|
547
|
+
config: SqlToStringConfig
|
|
548
|
+
) -> str:
|
|
549
|
+
op_type = bitwise.operator
|
|
550
|
+
if op_type == BitwiseBinaryOperator.AND:
|
|
551
|
+
op = "&"
|
|
552
|
+
elif op_type == BitwiseBinaryOperator.OR:
|
|
553
|
+
op = "|"
|
|
554
|
+
elif op_type == BitwiseBinaryOperator.XOR:
|
|
555
|
+
op = "#"
|
|
556
|
+
else:
|
|
557
|
+
raise ValueError("Unknown bitwise binary operator type: " + str(op_type)) # pragma: no cover
|
|
558
|
+
|
|
559
|
+
left = extension.process_expression(bitwise.left, config)
|
|
560
|
+
right = extension.process_expression(bitwise.right, config)
|
|
561
|
+
return f"({left} {op} {right})"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def date_diff_processor(
|
|
565
|
+
date_diff: DateDiffExpression,
|
|
566
|
+
extension: "SqlToStringDbExtension",
|
|
567
|
+
config: SqlToStringConfig
|
|
568
|
+
) -> str:
|
|
569
|
+
unit = date_diff.duration_unit.value
|
|
570
|
+
|
|
571
|
+
end = extension.process_expression(date_diff.end_date, config)
|
|
572
|
+
start = extension.process_expression(date_diff.start_date, config)
|
|
573
|
+
|
|
574
|
+
def extract_diff(part: str) -> str:
|
|
575
|
+
return f"(EXTRACT({part} FROM {end}) - EXTRACT({part} FROM {start}))"
|
|
576
|
+
|
|
577
|
+
year_diff = extract_diff("YEAR")
|
|
578
|
+
month_diff = extract_diff("MONTH")
|
|
579
|
+
# d1 - d2 → Pure dateDiff(d1, d2) → evaluated as d2 - d1
|
|
580
|
+
# Reverse to preserve expected semantics.
|
|
581
|
+
# only for days
|
|
582
|
+
day_diff = f"CAST(CAST({start} AS DATE) - CAST({end} AS DATE) AS INTEGER)"
|
|
583
|
+
epoch_diff = f"(EXTRACT(EPOCH FROM {end}) - EXTRACT(EPOCH FROM {start}))"
|
|
584
|
+
|
|
585
|
+
if unit == "YEARS":
|
|
586
|
+
return year_diff
|
|
587
|
+
|
|
588
|
+
if unit == "MONTHS":
|
|
589
|
+
return f"({year_diff} * 12 + {month_diff})"
|
|
590
|
+
|
|
591
|
+
if unit == "DAYS":
|
|
592
|
+
return day_diff
|
|
593
|
+
|
|
594
|
+
if unit == "WEEKS":
|
|
595
|
+
return f"CAST(FLOOR({day_diff} / 7) AS INTEGER)"
|
|
596
|
+
|
|
597
|
+
if unit == "HOURS":
|
|
598
|
+
return f"CAST(FLOOR({epoch_diff} / 3600) AS INTEGER)"
|
|
599
|
+
|
|
600
|
+
if unit == "MINUTES":
|
|
601
|
+
return f"CAST(FLOOR({epoch_diff} / 60) AS INTEGER)"
|
|
602
|
+
|
|
603
|
+
if unit == "SECONDS":
|
|
604
|
+
return f"CAST({epoch_diff} AS BIGINT)"
|
|
605
|
+
|
|
606
|
+
if unit == "MILLISECONDS":
|
|
607
|
+
return f"CAST({epoch_diff} * 1000 AS BIGINT)"
|
|
608
|
+
|
|
609
|
+
raise ValueError(f"Unsupported DATE DIFF unit: {unit}") # pragma: no cover
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def date_time_bucket_processor(
|
|
613
|
+
expression: DateTimeBucketExpression,
|
|
614
|
+
extension: "SqlToStringDbExtension",
|
|
615
|
+
config: SqlToStringConfig
|
|
616
|
+
) -> str:
|
|
617
|
+
unit = expression.duration_unit.value
|
|
618
|
+
ts = extension.process_expression(expression.date, config)
|
|
619
|
+
q = extension.process_expression(expression.quantity, config)
|
|
620
|
+
|
|
621
|
+
def coerce_to_datetime(sql: str) -> str:
|
|
622
|
+
return (
|
|
623
|
+
f"(({sql}) + INTERVAL '0 second')"
|
|
624
|
+
if expression.date_type == DateType.DateTime
|
|
625
|
+
else sql
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def epoch() -> str:
|
|
629
|
+
return (
|
|
630
|
+
f"EXTRACT(EPOCH FROM {ts})"
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
if unit == "YEARS":
|
|
634
|
+
return coerce_to_datetime(
|
|
635
|
+
f"make_date(1970,1,1) + "
|
|
636
|
+
f"(FLOOR((EXTRACT(YEAR FROM {ts}) - 1970) / {q}) * {q}) * INTERVAL '1 year'"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
if unit == "MONTHS":
|
|
640
|
+
total_months_sql = f"((EXTRACT(YEAR FROM {ts}) - 1970) * 12 + (EXTRACT(MONTH FROM {ts}) - 1))"
|
|
641
|
+
return coerce_to_datetime(
|
|
642
|
+
f"make_date(1970,1,1) + "
|
|
643
|
+
f"(FLOOR({total_months_sql} / {q}) * {q}) * INTERVAL '1 month'"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
if unit == "WEEKS":
|
|
647
|
+
return coerce_to_datetime(
|
|
648
|
+
f"make_date(1969,12,29) + ("
|
|
649
|
+
f"FLOOR(("
|
|
650
|
+
f"{epoch()} - EXTRACT(EPOCH FROM make_date(1969,12,29))"
|
|
651
|
+
f") / (86400 * {q} * 7))"
|
|
652
|
+
f") * ({q} * 7) * INTERVAL '1 day'"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if unit == "DAYS":
|
|
656
|
+
days_from_1970 = f"({epoch()} / 86400)"
|
|
657
|
+
return coerce_to_datetime(
|
|
658
|
+
f"make_date(1970,1,1) + "
|
|
659
|
+
f"(FLOOR({days_from_1970} / {q}) * {q}) * INTERVAL '1 day'"
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
unit_seconds_map = {
|
|
663
|
+
"HOURS": 3600,
|
|
664
|
+
"MINUTES": 60,
|
|
665
|
+
"SECONDS": 1
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if unit in unit_seconds_map:
|
|
669
|
+
seconds_per_unit = unit_seconds_map[unit]
|
|
670
|
+
return (f"(make_date(1970,1,1) + "
|
|
671
|
+
f"(FLOOR({epoch()} / ({q} * {seconds_per_unit})) * ({q} * {seconds_per_unit})) "
|
|
672
|
+
f"* INTERVAL '1 second')")
|
|
673
|
+
|
|
674
|
+
raise ValueError(f"Unsupported TIME BUCKET unit: {unit}") # pragma: no cover
|
|
675
|
+
|
|
676
|
+
|
|
519
677
|
def not_expression_processor(
|
|
520
678
|
not_expression: NotExpression,
|
|
521
679
|
extension: "SqlToStringDbExtension",
|
|
@@ -864,14 +1022,27 @@ def window_processor(
|
|
|
864
1022
|
if window.windowRef:
|
|
865
1023
|
return window.windowRef
|
|
866
1024
|
|
|
867
|
-
|
|
868
|
-
if window.partitions else ""
|
|
1025
|
+
clauses: list[str] = []
|
|
869
1026
|
|
|
870
|
-
|
|
871
|
-
|
|
1027
|
+
if window.partitions:
|
|
1028
|
+
partition_clause = ", ".join(
|
|
1029
|
+
extension.process_expression(expr, config)
|
|
1030
|
+
for expr in window.partitions
|
|
1031
|
+
)
|
|
1032
|
+
clauses.append(f"PARTITION BY {partition_clause}")
|
|
1033
|
+
|
|
1034
|
+
if window.orderBy:
|
|
1035
|
+
order_clause = ", ".join(
|
|
1036
|
+
extension.process_sort_item(item, config)
|
|
1037
|
+
for item in window.orderBy
|
|
1038
|
+
)
|
|
1039
|
+
clauses.append(f"ORDER BY {order_clause}")
|
|
872
1040
|
|
|
873
|
-
|
|
874
|
-
|
|
1041
|
+
if window.windowFrame:
|
|
1042
|
+
frame_clause = extension.process_window_frame(window.windowFrame, config)
|
|
1043
|
+
clauses.append(frame_clause)
|
|
1044
|
+
|
|
1045
|
+
return f"OVER ({' '.join(clauses)})"
|
|
875
1046
|
|
|
876
1047
|
|
|
877
1048
|
def table_function_processor(
|
|
@@ -901,6 +1072,45 @@ def union_processor(
|
|
|
901
1072
|
return f"{left}{sep0}{union_str}{sep0}{right}"
|
|
902
1073
|
|
|
903
1074
|
|
|
1075
|
+
def frame_bound_processor(
|
|
1076
|
+
frame_bound: FrameBound,
|
|
1077
|
+
extension: "SqlToStringDbExtension",
|
|
1078
|
+
config: SqlToStringConfig,
|
|
1079
|
+
) -> str:
|
|
1080
|
+
bound_sql = {
|
|
1081
|
+
FrameBoundType.UNBOUNDED_PRECEDING: "UNBOUNDED PRECEDING",
|
|
1082
|
+
FrameBoundType.PRECEDING: "PRECEDING",
|
|
1083
|
+
FrameBoundType.FOLLOWING: "FOLLOWING",
|
|
1084
|
+
FrameBoundType.CURRENT_ROW: "CURRENT ROW",
|
|
1085
|
+
FrameBoundType.UNBOUNDED_FOLLOWING: "UNBOUNDED FOLLOWING",
|
|
1086
|
+
}[frame_bound.type_]
|
|
1087
|
+
|
|
1088
|
+
if frame_bound.value is None:
|
|
1089
|
+
return bound_sql
|
|
1090
|
+
|
|
1091
|
+
offset_expr = extension.process_expression(frame_bound.value, config)
|
|
1092
|
+
|
|
1093
|
+
offset_sql = (
|
|
1094
|
+
f"INTERVAL '{offset_expr} {frame_bound.duration_unit.value}'"
|
|
1095
|
+
if frame_bound.duration_unit
|
|
1096
|
+
else offset_expr
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
return f"{offset_sql} {bound_sql}"
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def window_frame_processor(
|
|
1103
|
+
frame: WindowFrame,
|
|
1104
|
+
extension: "SqlToStringDbExtension",
|
|
1105
|
+
config: SqlToStringConfig,
|
|
1106
|
+
) -> str:
|
|
1107
|
+
mode = "ROWS" if frame.mode == WindowFrameMode.ROWS else "RANGE"
|
|
1108
|
+
start = extension.process_frame_bound(frame.start, config)
|
|
1109
|
+
end = extension.process_frame_bound(frame.end, config) if frame.end else "UNBOUNDED FOLLOWING"
|
|
1110
|
+
|
|
1111
|
+
return f"{mode} BETWEEN {start} AND {end}"
|
|
1112
|
+
|
|
1113
|
+
|
|
904
1114
|
class SqlToStringDbExtension:
|
|
905
1115
|
@classmethod
|
|
906
1116
|
def reserved_keywords(cls) -> PyLegendList[str]:
|
|
@@ -1260,3 +1470,31 @@ class SqlToStringDbExtension:
|
|
|
1260
1470
|
|
|
1261
1471
|
def process_union(self, union: Union, config: SqlToStringConfig, nested_subquery: bool = False) -> str:
|
|
1262
1472
|
return union_processor(union, self, config, nested_subquery)
|
|
1473
|
+
|
|
1474
|
+
def process_window_frame(self, frame: WindowFrame, config: SqlToStringConfig) -> str:
|
|
1475
|
+
return window_frame_processor(frame, self, config)
|
|
1476
|
+
|
|
1477
|
+
def process_frame_bound(self, frame_bound: FrameBound, config: SqlToStringConfig) -> str:
|
|
1478
|
+
return frame_bound_processor(frame_bound, self, config)
|
|
1479
|
+
|
|
1480
|
+
def process_date_adjust_expression(self, expr: DateAdjustExpression, config: SqlToStringConfig) -> str:
|
|
1481
|
+
return (f"({self.process_expression(expr.date, config)}::DATE + "
|
|
1482
|
+
f"(INTERVAL '{self.process_expression(expr.number, config)} "
|
|
1483
|
+
f"{expr.duration_unit.value.upper()}'))::DATE")
|
|
1484
|
+
|
|
1485
|
+
def process_bitwise_not_expression(self, expr: BitwiseNotExpression, config: SqlToStringConfig) -> str:
|
|
1486
|
+
return f"~({self.process_expression(expr.value, config)})"
|
|
1487
|
+
|
|
1488
|
+
def process_bitwise_shift_expression(self, expr: BitwiseShiftExpression, config: SqlToStringConfig) -> str:
|
|
1489
|
+
return (f"({self.process_expression(expr.value, config)} "
|
|
1490
|
+
f"{'>>' if expr.direction == BitwiseShiftDirection.RIGHT else '<<'} "
|
|
1491
|
+
f"{self.process_expression(expr.shift, config)})")
|
|
1492
|
+
|
|
1493
|
+
def process_bitwise_binary_expression(self, expr: BitwiseBinaryExpression, config: SqlToStringConfig) -> str:
|
|
1494
|
+
return bitwise_binary_expression_processor(expr, self, config)
|
|
1495
|
+
|
|
1496
|
+
def process_date_diff_expression(self, expr: DateDiffExpression, config: SqlToStringConfig) -> str:
|
|
1497
|
+
return date_diff_processor(expr, self, config)
|
|
1498
|
+
|
|
1499
|
+
def process_date_time_bucket_expression(self, expr: DateTimeBucketExpression, config: SqlToStringConfig) -> str:
|
|
1500
|
+
return date_time_bucket_processor(expr, self, config)
|
|
@@ -27,12 +27,14 @@ from pylegend.core.language import (
|
|
|
27
27
|
PyLegendColumnExpression,
|
|
28
28
|
PyLegendExpressionIntegerReturn,
|
|
29
29
|
PyLegendExpressionFloatReturn,
|
|
30
|
+
convert_literal_to_literal_expression,
|
|
30
31
|
)
|
|
31
32
|
from pylegend._typing import (
|
|
32
33
|
PyLegendSequence,
|
|
33
34
|
PyLegendOptional,
|
|
34
35
|
PyLegendList,
|
|
35
36
|
PyLegendDict,
|
|
37
|
+
PyLegendUnion,
|
|
36
38
|
)
|
|
37
39
|
from pylegend.core.language.shared.helpers import escape_column_name
|
|
38
40
|
from pylegend.core.sql.metamodel import (
|
|
@@ -44,7 +46,13 @@ from pylegend.core.sql.metamodel import (
|
|
|
44
46
|
SortItemNullOrdering,
|
|
45
47
|
Window,
|
|
46
48
|
FunctionCall,
|
|
47
|
-
QualifiedName,
|
|
49
|
+
QualifiedName,
|
|
50
|
+
IntegerLiteral,
|
|
51
|
+
WindowFrame,
|
|
52
|
+
WindowFrameMode,
|
|
53
|
+
FrameBound,
|
|
54
|
+
FrameBoundType,
|
|
55
|
+
StringLiteral
|
|
48
56
|
)
|
|
49
57
|
from pylegend.core.tds.tds_frame import FrameToSqlConfig, FrameToPureConfig
|
|
50
58
|
from typing import TYPE_CHECKING
|
|
@@ -64,6 +72,11 @@ __all__: PyLegendSequence[str] = [
|
|
|
64
72
|
"LegendQLApiWindow",
|
|
65
73
|
"LegendQLApiPartialFrame",
|
|
66
74
|
"LegendQLApiWindowReference",
|
|
75
|
+
"LegendQLApiWindowFrameBound",
|
|
76
|
+
"LegendQLApiWindowFrameMode",
|
|
77
|
+
"LegendQLApiWindowFrame",
|
|
78
|
+
"LegendQLApiDurationUnit",
|
|
79
|
+
"LegendQLApiWindowFrameBoundType"
|
|
67
80
|
]
|
|
68
81
|
|
|
69
82
|
|
|
@@ -168,8 +181,174 @@ class LegendQLApiSortInfo:
|
|
|
168
181
|
return f"{func}(~{escape_column_name(self.__column)})"
|
|
169
182
|
|
|
170
183
|
|
|
171
|
-
class
|
|
172
|
-
|
|
184
|
+
class LegendQLApiWindowFrameMode(Enum):
|
|
185
|
+
ROWS = 1
|
|
186
|
+
RANGE = 2
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class LegendQLApiWindowFrameBoundType(Enum):
|
|
190
|
+
UNBOUNDED_PRECEDING = 1
|
|
191
|
+
PRECEDING = 2
|
|
192
|
+
CURRENT_ROW = 3
|
|
193
|
+
FOLLOWING = 4
|
|
194
|
+
UNBOUNDED_FOLLOWING = 5
|
|
195
|
+
|
|
196
|
+
def to_sql_node(
|
|
197
|
+
self,
|
|
198
|
+
query: QuerySpecification,
|
|
199
|
+
config: FrameToSqlConfig,
|
|
200
|
+
) -> FrameBoundType:
|
|
201
|
+
mapping = {
|
|
202
|
+
LegendQLApiWindowFrameBoundType.UNBOUNDED_PRECEDING: FrameBoundType.UNBOUNDED_PRECEDING,
|
|
203
|
+
LegendQLApiWindowFrameBoundType.PRECEDING: FrameBoundType.PRECEDING,
|
|
204
|
+
LegendQLApiWindowFrameBoundType.CURRENT_ROW: FrameBoundType.CURRENT_ROW,
|
|
205
|
+
LegendQLApiWindowFrameBoundType.FOLLOWING: FrameBoundType.FOLLOWING,
|
|
206
|
+
LegendQLApiWindowFrameBoundType.UNBOUNDED_FOLLOWING: FrameBoundType.UNBOUNDED_FOLLOWING
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return mapping[self]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class LegendQLApiDurationUnit(Enum):
|
|
213
|
+
YEARS = 1
|
|
214
|
+
MONTHS = 2
|
|
215
|
+
WEEKS = 3
|
|
216
|
+
DAYS = 4
|
|
217
|
+
HOURS = 5
|
|
218
|
+
MINUTES = 6
|
|
219
|
+
SECONDS = 7
|
|
220
|
+
MILLISECONDS = 8
|
|
221
|
+
MICROSECONDS = 9
|
|
222
|
+
NANOSECONDS = 10
|
|
223
|
+
|
|
224
|
+
def to_pure_expression(self, config: FrameToPureConfig) -> str:
|
|
225
|
+
return self.name
|
|
226
|
+
|
|
227
|
+
def to_sql_node(
|
|
228
|
+
self,
|
|
229
|
+
query: QuerySpecification,
|
|
230
|
+
config: FrameToSqlConfig
|
|
231
|
+
) -> StringLiteral:
|
|
232
|
+
mapping = {
|
|
233
|
+
LegendQLApiDurationUnit.YEARS: "YEAR",
|
|
234
|
+
LegendQLApiDurationUnit.MONTHS: "MONTH",
|
|
235
|
+
LegendQLApiDurationUnit.WEEKS: "WEEK",
|
|
236
|
+
LegendQLApiDurationUnit.DAYS: "DAY",
|
|
237
|
+
LegendQLApiDurationUnit.HOURS: "HOUR",
|
|
238
|
+
LegendQLApiDurationUnit.MINUTES: "MINUTE",
|
|
239
|
+
LegendQLApiDurationUnit.SECONDS: "SECOND",
|
|
240
|
+
LegendQLApiDurationUnit.MILLISECONDS: "MILLISECOND",
|
|
241
|
+
LegendQLApiDurationUnit.MICROSECONDS: "MICROSECOND",
|
|
242
|
+
LegendQLApiDurationUnit.NANOSECONDS: "NANOSECOND",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return StringLiteral(mapping[self], quoted=False)
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def from_string(cls, value: str) -> "LegendQLApiDurationUnit":
|
|
249
|
+
try:
|
|
250
|
+
return cls[value.upper()]
|
|
251
|
+
except KeyError:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"Invalid duration unit '{value}'. "
|
|
254
|
+
f"Supported values: {[u.name.lower() for u in cls]}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class LegendQLApiWindowFrameBound:
|
|
259
|
+
__bound_type: LegendQLApiWindowFrameBoundType
|
|
260
|
+
__row_offset: PyLegendOptional[PyLegendUnion[int, float]]
|
|
261
|
+
__duration_unit: PyLegendOptional[LegendQLApiDurationUnit]
|
|
262
|
+
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
bound_type: LegendQLApiWindowFrameBoundType,
|
|
266
|
+
row_offset: PyLegendOptional[PyLegendUnion[int, float]] = None,
|
|
267
|
+
duration_unit: PyLegendOptional[LegendQLApiDurationUnit] = None,
|
|
268
|
+
) -> None:
|
|
269
|
+
if bound_type in (
|
|
270
|
+
LegendQLApiWindowFrameBoundType.PRECEDING,
|
|
271
|
+
LegendQLApiWindowFrameBoundType.FOLLOWING,
|
|
272
|
+
) and row_offset is None:
|
|
273
|
+
raise ValueError(f"row_offset must be provided for bound_type {bound_type.name}")
|
|
274
|
+
|
|
275
|
+
if bound_type not in (
|
|
276
|
+
LegendQLApiWindowFrameBoundType.PRECEDING,
|
|
277
|
+
LegendQLApiWindowFrameBoundType.FOLLOWING,
|
|
278
|
+
) and row_offset is not None:
|
|
279
|
+
raise ValueError(f"row_offset is not allowed for bound_type {bound_type.name}")
|
|
280
|
+
|
|
281
|
+
self.__bound_type = bound_type
|
|
282
|
+
self.__row_offset = row_offset
|
|
283
|
+
self.__duration_unit = duration_unit
|
|
284
|
+
|
|
285
|
+
def to_pure_expression(self, config: FrameToPureConfig) -> str:
|
|
286
|
+
if (self.__bound_type == LegendQLApiWindowFrameBoundType.UNBOUNDED_FOLLOWING
|
|
287
|
+
or self.__bound_type == LegendQLApiWindowFrameBoundType.UNBOUNDED_PRECEDING):
|
|
288
|
+
return "unbounded()"
|
|
289
|
+
|
|
290
|
+
elif self.__bound_type == LegendQLApiWindowFrameBoundType.CURRENT_ROW:
|
|
291
|
+
expr = "0"
|
|
292
|
+
|
|
293
|
+
else:
|
|
294
|
+
expr = convert_literal_to_literal_expression(self.__row_offset).to_pure_expression(
|
|
295
|
+
config) if self.__row_offset is not None else ""
|
|
296
|
+
|
|
297
|
+
if self.__duration_unit is not None:
|
|
298
|
+
expr += f", DurationUnit.{self.__duration_unit.to_pure_expression(config)}"
|
|
299
|
+
|
|
300
|
+
return expr
|
|
301
|
+
|
|
302
|
+
def to_sql_node(
|
|
303
|
+
self,
|
|
304
|
+
query: QuerySpecification,
|
|
305
|
+
config: FrameToSqlConfig,
|
|
306
|
+
) -> FrameBound:
|
|
307
|
+
value = (convert_literal_to_literal_expression(abs(self.__row_offset))
|
|
308
|
+
.to_sql_expression({"w": query}, config)) \
|
|
309
|
+
if self.__row_offset is not None else None
|
|
310
|
+
|
|
311
|
+
frame_bound_type = self.__bound_type.to_sql_node(query, config)
|
|
312
|
+
|
|
313
|
+
duration_unit = self.__duration_unit.to_sql_node(
|
|
314
|
+
query,
|
|
315
|
+
config) if self.__duration_unit is not None else None
|
|
316
|
+
|
|
317
|
+
return FrameBound(frame_bound_type, value, duration_unit)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class LegendQLApiWindowFrame:
|
|
321
|
+
__mode: LegendQLApiWindowFrameMode
|
|
322
|
+
__start_bound: LegendQLApiWindowFrameBound
|
|
323
|
+
__end_bound: LegendQLApiWindowFrameBound
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
mode: LegendQLApiWindowFrameMode,
|
|
328
|
+
start_bound: LegendQLApiWindowFrameBound,
|
|
329
|
+
end_bound: LegendQLApiWindowFrameBound,
|
|
330
|
+
) -> None:
|
|
331
|
+
self.__mode = mode
|
|
332
|
+
self.__start_bound = start_bound
|
|
333
|
+
self.__end_bound = end_bound
|
|
334
|
+
|
|
335
|
+
def to_pure_expression(self, config: FrameToPureConfig) -> str:
|
|
336
|
+
mode_str = "rows" if self.__mode == LegendQLApiWindowFrameMode.ROWS else "_range"
|
|
337
|
+
start_expr = self.__start_bound.to_pure_expression(config)
|
|
338
|
+
end_expr = self.__end_bound.to_pure_expression(config)
|
|
339
|
+
|
|
340
|
+
return f"{mode_str}({start_expr}, {end_expr})"
|
|
341
|
+
|
|
342
|
+
def to_sql_node(
|
|
343
|
+
self,
|
|
344
|
+
query: QuerySpecification,
|
|
345
|
+
config: FrameToSqlConfig
|
|
346
|
+
) -> WindowFrame:
|
|
347
|
+
return WindowFrame(
|
|
348
|
+
mode=WindowFrameMode.ROWS if self.__mode == LegendQLApiWindowFrameMode.ROWS else WindowFrameMode.RANGE,
|
|
349
|
+
start=self.__start_bound.to_sql_node(query, config),
|
|
350
|
+
end=self.__end_bound.to_sql_node(query, config),
|
|
351
|
+
)
|
|
173
352
|
|
|
174
353
|
|
|
175
354
|
class LegendQLApiWindow:
|
|
@@ -211,7 +390,10 @@ class LegendQLApiWindow:
|
|
|
211
390
|
[] if self.__order_by is None else
|
|
212
391
|
[sort_info.to_sql_node(query, config) for sort_info in self.__order_by]
|
|
213
392
|
),
|
|
214
|
-
windowFrame=
|
|
393
|
+
windowFrame=(
|
|
394
|
+
None if self.__frame is None else
|
|
395
|
+
self.__frame.to_sql_node(query, config)
|
|
396
|
+
),
|
|
215
397
|
)
|
|
216
398
|
|
|
217
399
|
@staticmethod
|
|
@@ -235,7 +417,10 @@ class LegendQLApiWindow:
|
|
|
235
417
|
"[]" if self.__order_by is None or len(self.__order_by) == 0
|
|
236
418
|
else "[" + (', '.join([s.to_pure_expression(config) for s in self.__order_by])) + "]"
|
|
237
419
|
)
|
|
238
|
-
|
|
420
|
+
|
|
421
|
+
frame_str = f", {self.__frame.to_pure_expression(config)}" if self.__frame else ""
|
|
422
|
+
|
|
423
|
+
return f"over({partitions_str}, {sorts_str}{frame_str})"
|
|
239
424
|
|
|
240
425
|
|
|
241
426
|
class LegendQLApiPartialFrame:
|
|
@@ -77,6 +77,9 @@ class Series(PyLegendColumnExpression, PyLegendPrimitive, BaseTdsFrame):
|
|
|
77
77
|
def value(self) -> PyLegendColumnExpression:
|
|
78
78
|
return self
|
|
79
79
|
|
|
80
|
+
def get_base_frame(self) -> "PandasApiTdsFrame":
|
|
81
|
+
return self.__base_frame
|
|
82
|
+
|
|
80
83
|
def to_sql_expression(
|
|
81
84
|
self,
|
|
82
85
|
frame_name_to_base_query_map: PyLegendDict[str, QuerySpecification],
|
|
@@ -36,6 +36,7 @@ __all__: PyLegendSequence[str] = [
|
|
|
36
36
|
"PyLegendExpressionDateReturn",
|
|
37
37
|
"PyLegendExpressionDateTimeReturn",
|
|
38
38
|
"PyLegendExpressionStrictDateReturn",
|
|
39
|
+
"PyLegendExpressionNullReturn"
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
|
|
@@ -86,3 +87,7 @@ class PyLegendExpressionDateTimeReturn(PyLegendExpressionDateReturn, metaclass=A
|
|
|
86
87
|
|
|
87
88
|
class PyLegendExpressionStrictDateReturn(PyLegendExpressionDateReturn, metaclass=ABCMeta):
|
|
88
89
|
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PyLegendExpressionNullReturn(PyLegendExpression, metaclass=ABCMeta):
|
|
93
|
+
pass
|
|
@@ -27,6 +27,7 @@ from pylegend.core.language.shared.expression import (
|
|
|
27
27
|
PyLegendExpressionFloatReturn,
|
|
28
28
|
PyLegendExpressionDateTimeReturn,
|
|
29
29
|
PyLegendExpressionStrictDateReturn,
|
|
30
|
+
PyLegendExpressionNullReturn,
|
|
30
31
|
)
|
|
31
32
|
from pylegend.core.sql.metamodel import (
|
|
32
33
|
Expression,
|
|
@@ -37,6 +38,7 @@ from pylegend.core.sql.metamodel import (
|
|
|
37
38
|
QuerySpecification,
|
|
38
39
|
Cast,
|
|
39
40
|
ColumnType,
|
|
41
|
+
NullLiteral,
|
|
40
42
|
)
|
|
41
43
|
from pylegend.core.tds.tds_frame import FrameToSqlConfig
|
|
42
44
|
from pylegend.core.tds.tds_frame import FrameToPureConfig
|
|
@@ -180,8 +182,25 @@ class PyLegendStrictDateLiteralExpression(PyLegendExpressionStrictDateReturn):
|
|
|
180
182
|
return True
|
|
181
183
|
|
|
182
184
|
|
|
185
|
+
class PyLegendNullLiteralExpression(PyLegendExpressionNullReturn):
|
|
186
|
+
__value: None
|
|
187
|
+
|
|
188
|
+
def __init__(self) -> None:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
def to_sql_expression(
|
|
192
|
+
self,
|
|
193
|
+
frame_name_to_base_query_map: PyLegendDict[str, QuerySpecification],
|
|
194
|
+
config: FrameToSqlConfig
|
|
195
|
+
) -> Expression:
|
|
196
|
+
return NullLiteral()
|
|
197
|
+
|
|
198
|
+
def to_pure_expression(self, config: FrameToPureConfig) -> str:
|
|
199
|
+
return "[]"
|
|
200
|
+
|
|
201
|
+
|
|
183
202
|
def convert_literal_to_literal_expression(
|
|
184
|
-
literal: PyLegendUnion[int, float, bool, str, datetime, date]
|
|
203
|
+
literal: PyLegendUnion[int, float, bool, str, datetime, date, None]
|
|
185
204
|
) -> PyLegendExpression:
|
|
186
205
|
if isinstance(literal, bool):
|
|
187
206
|
return PyLegendBooleanLiteralExpression(literal)
|
|
@@ -195,5 +214,7 @@ def convert_literal_to_literal_expression(
|
|
|
195
214
|
return PyLegendDateTimeLiteralExpression(literal)
|
|
196
215
|
if isinstance(literal, date):
|
|
197
216
|
return PyLegendStrictDateLiteralExpression(literal)
|
|
217
|
+
if isinstance(literal, type(None)):
|
|
218
|
+
return PyLegendNullLiteralExpression()
|
|
198
219
|
|
|
199
220
|
raise TypeError(f"Cannot convert value - {literal} of type {type(literal)} to literal expression")
|