pylegend 0.11.0__py3-none-any.whl → 0.12.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 +68 -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/sql/metamodel.py +4 -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/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 +300 -34
- pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +78 -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.12.0.dist-info}/METADATA +1 -1
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/RECORD +25 -23
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/WHEEL +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE.spdx +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -72,6 +72,10 @@ from pylegend.core.sql.metamodel import (
|
|
|
72
72
|
Window,
|
|
73
73
|
TableFunction,
|
|
74
74
|
Union,
|
|
75
|
+
WindowFrame,
|
|
76
|
+
WindowFrameMode,
|
|
77
|
+
FrameBound,
|
|
78
|
+
FrameBoundType
|
|
75
79
|
)
|
|
76
80
|
from pylegend.core.sql.metamodel_extension import (
|
|
77
81
|
StringLengthExpression,
|
|
@@ -864,14 +868,27 @@ def window_processor(
|
|
|
864
868
|
if window.windowRef:
|
|
865
869
|
return window.windowRef
|
|
866
870
|
|
|
867
|
-
|
|
868
|
-
if window.partitions else ""
|
|
871
|
+
clauses: list[str] = []
|
|
869
872
|
|
|
870
|
-
|
|
871
|
-
|
|
873
|
+
if window.partitions:
|
|
874
|
+
partition_clause = ", ".join(
|
|
875
|
+
extension.process_expression(expr, config)
|
|
876
|
+
for expr in window.partitions
|
|
877
|
+
)
|
|
878
|
+
clauses.append(f"PARTITION BY {partition_clause}")
|
|
879
|
+
|
|
880
|
+
if window.orderBy:
|
|
881
|
+
order_clause = ", ".join(
|
|
882
|
+
extension.process_sort_item(item, config)
|
|
883
|
+
for item in window.orderBy
|
|
884
|
+
)
|
|
885
|
+
clauses.append(f"ORDER BY {order_clause}")
|
|
872
886
|
|
|
873
|
-
|
|
874
|
-
|
|
887
|
+
if window.windowFrame:
|
|
888
|
+
frame_clause = extension.process_window_frame(window.windowFrame, config)
|
|
889
|
+
clauses.append(frame_clause)
|
|
890
|
+
|
|
891
|
+
return f"OVER ({' '.join(clauses)})"
|
|
875
892
|
|
|
876
893
|
|
|
877
894
|
def table_function_processor(
|
|
@@ -901,6 +918,45 @@ def union_processor(
|
|
|
901
918
|
return f"{left}{sep0}{union_str}{sep0}{right}"
|
|
902
919
|
|
|
903
920
|
|
|
921
|
+
def frame_bound_processor(
|
|
922
|
+
frame_bound: FrameBound,
|
|
923
|
+
extension: "SqlToStringDbExtension",
|
|
924
|
+
config: SqlToStringConfig,
|
|
925
|
+
) -> str:
|
|
926
|
+
bound_sql = {
|
|
927
|
+
FrameBoundType.UNBOUNDED_PRECEDING: "UNBOUNDED PRECEDING",
|
|
928
|
+
FrameBoundType.PRECEDING: "PRECEDING",
|
|
929
|
+
FrameBoundType.FOLLOWING: "FOLLOWING",
|
|
930
|
+
FrameBoundType.CURRENT_ROW: "CURRENT ROW",
|
|
931
|
+
FrameBoundType.UNBOUNDED_FOLLOWING: "UNBOUNDED FOLLOWING",
|
|
932
|
+
}[frame_bound.type_]
|
|
933
|
+
|
|
934
|
+
if frame_bound.value is None:
|
|
935
|
+
return bound_sql
|
|
936
|
+
|
|
937
|
+
offset_expr = extension.process_expression(frame_bound.value, config)
|
|
938
|
+
|
|
939
|
+
offset_sql = (
|
|
940
|
+
f"INTERVAL '{offset_expr} {frame_bound.duration_unit.value}'"
|
|
941
|
+
if frame_bound.duration_unit
|
|
942
|
+
else offset_expr
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
return f"{offset_sql} {bound_sql}"
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def window_frame_processor(
|
|
949
|
+
frame: WindowFrame,
|
|
950
|
+
extension: "SqlToStringDbExtension",
|
|
951
|
+
config: SqlToStringConfig,
|
|
952
|
+
) -> str:
|
|
953
|
+
mode = "ROWS" if frame.mode == WindowFrameMode.ROWS else "RANGE"
|
|
954
|
+
start = extension.process_frame_bound(frame.start, config)
|
|
955
|
+
end = extension.process_frame_bound(frame.end, config) if frame.end else "UNBOUNDED FOLLOWING"
|
|
956
|
+
|
|
957
|
+
return f"{mode} BETWEEN {start} AND {end}"
|
|
958
|
+
|
|
959
|
+
|
|
904
960
|
class SqlToStringDbExtension:
|
|
905
961
|
@classmethod
|
|
906
962
|
def reserved_keywords(cls) -> PyLegendList[str]:
|
|
@@ -1260,3 +1316,9 @@ class SqlToStringDbExtension:
|
|
|
1260
1316
|
|
|
1261
1317
|
def process_union(self, union: Union, config: SqlToStringConfig, nested_subquery: bool = False) -> str:
|
|
1262
1318
|
return union_processor(union, self, config, nested_subquery)
|
|
1319
|
+
|
|
1320
|
+
def process_window_frame(self, frame: WindowFrame, config: SqlToStringConfig) -> str:
|
|
1321
|
+
return window_frame_processor(frame, self, config)
|
|
1322
|
+
|
|
1323
|
+
def process_frame_bound(self, frame_bound: FrameBound, config: SqlToStringConfig) -> str:
|
|
1324
|
+
return frame_bound_processor(frame_bound, 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],
|
pylegend/core/sql/metamodel.py
CHANGED
|
@@ -918,12 +918,15 @@ class WindowFrame(Node):
|
|
|
918
918
|
class FrameBound(Node):
|
|
919
919
|
type_: "FrameBoundType"
|
|
920
920
|
value: "PyLegendOptional[Expression]"
|
|
921
|
+
duration_unit: "PyLegendOptional[StringLiteral]"
|
|
921
922
|
|
|
922
923
|
def __init__(
|
|
923
924
|
self,
|
|
924
925
|
type_: "FrameBoundType",
|
|
925
|
-
value: "PyLegendOptional[Expression]"
|
|
926
|
+
value: "PyLegendOptional[Expression]",
|
|
927
|
+
duration_unit: "PyLegendOptional[StringLiteral]" = None
|
|
926
928
|
) -> None:
|
|
927
929
|
super().__init__(_type="frameBound")
|
|
928
930
|
self.type_ = type_
|
|
929
931
|
self.value = value
|
|
932
|
+
self.duration_unit = duration_unit
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
from pylegend._typing import (
|
|
16
16
|
PyLegendList,
|
|
17
|
-
PyLegendSequence
|
|
17
|
+
PyLegendSequence,
|
|
18
|
+
PyLegendUnion,
|
|
19
|
+
PyLegendOptional,
|
|
20
|
+
PyLegendCallable
|
|
18
21
|
)
|
|
19
22
|
from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
|
|
20
23
|
from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
|
|
@@ -25,7 +28,10 @@ from pylegend.core.tds.tds_column import TdsColumn
|
|
|
25
28
|
from pylegend.core.tds.tds_frame import FrameToSqlConfig
|
|
26
29
|
from pylegend.core.tds.tds_frame import FrameToPureConfig
|
|
27
30
|
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
28
|
-
|
|
31
|
+
from pylegend.core.language.legendql_api.legendql_api_custom_expressions import LegendQLApiPrimitive
|
|
32
|
+
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
33
|
+
from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import infer_columns_from_frame
|
|
34
|
+
from pylegend.core.language.shared.helpers import escape_column_name
|
|
29
35
|
|
|
30
36
|
__all__: PyLegendSequence[str] = [
|
|
31
37
|
"LegendQLApiDistinctFunction"
|
|
@@ -34,27 +40,56 @@ __all__: PyLegendSequence[str] = [
|
|
|
34
40
|
|
|
35
41
|
class LegendQLApiDistinctFunction(LegendQLApiAppliedFunction):
|
|
36
42
|
__base_frame: LegendQLApiBaseTdsFrame
|
|
43
|
+
__column_name_list: PyLegendOptional[PyLegendList[str]]
|
|
37
44
|
|
|
38
45
|
@classmethod
|
|
39
46
|
def name(cls) -> str:
|
|
40
47
|
return "distinct"
|
|
41
48
|
|
|
42
|
-
def __init__(
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
52
|
+
columns: PyLegendOptional[PyLegendUnion[
|
|
53
|
+
str,
|
|
54
|
+
PyLegendList[str],
|
|
55
|
+
PyLegendCallable[
|
|
56
|
+
[LegendQLApiTdsRow],
|
|
57
|
+
PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
|
|
58
|
+
]
|
|
59
|
+
]] = None
|
|
60
|
+
) -> None:
|
|
43
61
|
self.__base_frame = base_frame
|
|
62
|
+
self.__column_name_list = infer_columns_from_frame(base_frame, columns,
|
|
63
|
+
"'distinct' function 'columns'") if columns is not None else None
|
|
44
64
|
|
|
45
65
|
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
46
66
|
base_query = self.__base_frame.to_sql_query_object(config)
|
|
47
|
-
should_create_sub_query = (base_query.offset is not None) or (base_query.limit is not None)
|
|
67
|
+
should_create_sub_query = (base_query.offset is not None) or (base_query.limit is not None) or (
|
|
68
|
+
self.__column_name_list is not None)
|
|
69
|
+
|
|
70
|
+
quoted_columns = None
|
|
71
|
+
if self.__column_name_list is not None:
|
|
72
|
+
db_extension = config.sql_to_string_generator().get_db_extension()
|
|
73
|
+
quoted_columns = [
|
|
74
|
+
db_extension.quote_identifier(col)
|
|
75
|
+
for col in self.__column_name_list
|
|
76
|
+
]
|
|
77
|
+
|
|
48
78
|
new_query = (
|
|
49
|
-
create_sub_query(base_query, config, "root") if should_create_sub_query else
|
|
79
|
+
create_sub_query(base_query, config, "root", quoted_columns) if should_create_sub_query else
|
|
50
80
|
copy_query(base_query)
|
|
51
81
|
)
|
|
52
82
|
new_query.select.distinct = True
|
|
83
|
+
|
|
53
84
|
return new_query
|
|
54
85
|
|
|
55
86
|
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
87
|
+
columns_expr = (
|
|
88
|
+
f"~[{', '.join(map(escape_column_name, self.__column_name_list))}]"
|
|
89
|
+
if self.__column_name_list else ""
|
|
90
|
+
)
|
|
56
91
|
return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}"
|
|
57
|
-
f"->distinct()")
|
|
92
|
+
f"->distinct({columns_expr})")
|
|
58
93
|
|
|
59
94
|
def base_frame(self) -> LegendQLApiBaseTdsFrame:
|
|
60
95
|
return self.__base_frame
|
|
@@ -63,7 +98,18 @@ class LegendQLApiDistinctFunction(LegendQLApiAppliedFunction):
|
|
|
63
98
|
return []
|
|
64
99
|
|
|
65
100
|
def calculate_columns(self) -> PyLegendSequence[TdsColumn]:
|
|
66
|
-
|
|
101
|
+
if not self.__column_name_list:
|
|
102
|
+
return [c.copy() for c in self.__base_frame.columns()]
|
|
103
|
+
|
|
104
|
+
base_columns = self.__base_frame.columns()
|
|
105
|
+
new_columns = []
|
|
106
|
+
for name in self.__column_name_list:
|
|
107
|
+
for col in base_columns:
|
|
108
|
+
if col.get_name() == name:
|
|
109
|
+
new_columns.append(col.copy())
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
return new_columns
|
|
67
113
|
|
|
68
114
|
def validate(self) -> bool:
|
|
69
115
|
return True
|
|
@@ -30,6 +30,11 @@ from pylegend.core.language.legendql_api.legendql_api_custom_expressions import
|
|
|
30
30
|
LegendQLApiWindow,
|
|
31
31
|
LegendQLApiPartialFrame,
|
|
32
32
|
LegendQLApiWindowReference,
|
|
33
|
+
LegendQLApiWindowFrame,
|
|
34
|
+
LegendQLApiWindowFrameMode,
|
|
35
|
+
LegendQLApiWindowFrameBound,
|
|
36
|
+
LegendQLApiWindowFrameBoundType,
|
|
37
|
+
LegendQLApiDurationUnit
|
|
33
38
|
)
|
|
34
39
|
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
35
40
|
from pylegend.core.tds.abstract.frames.base_tds_frame import BaseTdsFrame
|
|
@@ -59,14 +64,23 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
|
|
|
59
64
|
def limit(self, row_count: int = 5) -> "LegendQLApiTdsFrame":
|
|
60
65
|
return self.head(row_count=row_count)
|
|
61
66
|
|
|
62
|
-
def distinct(
|
|
67
|
+
def distinct(
|
|
68
|
+
self,
|
|
69
|
+
columns: PyLegendOptional[PyLegendUnion[
|
|
70
|
+
str,
|
|
71
|
+
PyLegendList[str],
|
|
72
|
+
PyLegendCallable[
|
|
73
|
+
[LegendQLApiTdsRow],
|
|
74
|
+
PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
|
|
75
|
+
]
|
|
76
|
+
]] = None) -> "LegendQLApiTdsFrame":
|
|
63
77
|
from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import (
|
|
64
78
|
LegendQLApiAppliedFunctionTdsFrame
|
|
65
79
|
)
|
|
66
80
|
from pylegend.core.tds.legendql_api.frames.functions.legendql_api_distinct_function import (
|
|
67
81
|
LegendQLApiDistinctFunction
|
|
68
82
|
)
|
|
69
|
-
return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiDistinctFunction(self))
|
|
83
|
+
return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiDistinctFunction(self, columns))
|
|
70
84
|
|
|
71
85
|
def select(
|
|
72
86
|
self,
|
|
@@ -304,6 +318,80 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
|
|
|
304
318
|
LegendQLApiGroupByFunction(self, grouping_columns, aggregate_specifications)
|
|
305
319
|
)
|
|
306
320
|
|
|
321
|
+
def rows(
|
|
322
|
+
self,
|
|
323
|
+
start: PyLegendUnion[str, int],
|
|
324
|
+
end: PyLegendUnion[str, int]) -> LegendQLApiWindowFrame:
|
|
325
|
+
return LegendQLApiWindowFrame(
|
|
326
|
+
LegendQLApiWindowFrameMode.ROWS,
|
|
327
|
+
_infer_window_frame_bound(start, is_start_bound=True),
|
|
328
|
+
_infer_window_frame_bound(end)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def range(
|
|
332
|
+
self,
|
|
333
|
+
*,
|
|
334
|
+
number_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
335
|
+
number_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
336
|
+
duration_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
337
|
+
duration_start_unit: PyLegendOptional[str] = None,
|
|
338
|
+
duration_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
339
|
+
duration_end_unit: PyLegendOptional[str] = None) -> LegendQLApiWindowFrame:
|
|
340
|
+
|
|
341
|
+
has_number = number_start is not None or number_end is not None
|
|
342
|
+
has_duration = any([
|
|
343
|
+
duration_start is not None,
|
|
344
|
+
duration_end is not None,
|
|
345
|
+
duration_start_unit is not None,
|
|
346
|
+
duration_end_unit is not None,
|
|
347
|
+
])
|
|
348
|
+
|
|
349
|
+
if not has_number and not has_duration:
|
|
350
|
+
raise ValueError(
|
|
351
|
+
"Either numeric range or duration range must be provided. "
|
|
352
|
+
"Specify number_start and number_end, or duration_start and duration_end "
|
|
353
|
+
"(with duration_start_unit and duration_end_unit as needed)."
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if has_number and has_duration:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
"Numeric range and duration range cannot be used together. "
|
|
359
|
+
"Use either (number_start, number_end) or (duration_start, duration_end)."
|
|
360
|
+
"(with duration_start_unit and duration_end_unit as needed)."
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if has_number:
|
|
364
|
+
if number_start is None or number_end is None:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
"Both number_start and number_end must be provided together."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return LegendQLApiWindowFrame(
|
|
370
|
+
LegendQLApiWindowFrameMode.RANGE,
|
|
371
|
+
_infer_window_frame_bound(number_start, is_start_bound=True),
|
|
372
|
+
_infer_window_frame_bound(number_end),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if duration_start is None or duration_end is None:
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"Both duration_start and duration_end must be provided."
|
|
378
|
+
"(with duration_start_unit and duration_end_unit as needed).")
|
|
379
|
+
|
|
380
|
+
def is_unbounded(value: object) -> bool:
|
|
381
|
+
return isinstance(value, str) and value.lower() == "unbounded"
|
|
382
|
+
|
|
383
|
+
if not is_unbounded(duration_start) and duration_start_unit is None:
|
|
384
|
+
raise ValueError("duration_start_unit is required for bounded duration_start.")
|
|
385
|
+
|
|
386
|
+
if not is_unbounded(duration_end) and duration_end_unit is None:
|
|
387
|
+
raise ValueError("duration_end_unit is required for bounded duration_end.")
|
|
388
|
+
|
|
389
|
+
return LegendQLApiWindowFrame(
|
|
390
|
+
LegendQLApiWindowFrameMode.RANGE,
|
|
391
|
+
_infer_window_frame_bound(duration_start, is_start_bound=True, duration_unit=duration_start_unit),
|
|
392
|
+
_infer_window_frame_bound(duration_end, duration_unit=duration_end_unit)
|
|
393
|
+
)
|
|
394
|
+
|
|
307
395
|
def window(
|
|
308
396
|
self,
|
|
309
397
|
partition_by: PyLegendOptional[
|
|
@@ -329,7 +417,8 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
|
|
|
329
417
|
]
|
|
330
418
|
]
|
|
331
419
|
]
|
|
332
|
-
] = None
|
|
420
|
+
] = None,
|
|
421
|
+
frame: PyLegendOptional[LegendQLApiWindowFrame] = None
|
|
333
422
|
) -> "LegendQLApiWindow":
|
|
334
423
|
from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import (
|
|
335
424
|
infer_columns_from_frame,
|
|
@@ -344,7 +433,7 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
|
|
|
344
433
|
None if order_by is None else
|
|
345
434
|
infer_sorts_from_frame(self, order_by, "'window' function order_by")
|
|
346
435
|
),
|
|
347
|
-
frame=
|
|
436
|
+
frame=frame
|
|
348
437
|
)
|
|
349
438
|
|
|
350
439
|
def window_extend(
|
|
@@ -417,3 +506,56 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
|
|
|
417
506
|
LegendQLApiProjectFunction
|
|
418
507
|
)
|
|
419
508
|
return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiProjectFunction(self, project_columns))
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _infer_window_frame_bound(
|
|
512
|
+
value: PyLegendOptional[
|
|
513
|
+
PyLegendUnion[str, int, float]
|
|
514
|
+
] = None,
|
|
515
|
+
*,
|
|
516
|
+
is_start_bound: bool = False,
|
|
517
|
+
duration_unit: PyLegendOptional[str] = None,
|
|
518
|
+
) -> LegendQLApiWindowFrameBound:
|
|
519
|
+
if isinstance(value, str):
|
|
520
|
+
if value.lower() != "unbounded":
|
|
521
|
+
raise ValueError(
|
|
522
|
+
f"Invalid window frame boundary '{value}'. "
|
|
523
|
+
"Only 'unbounded' is supported as a string. "
|
|
524
|
+
"Otherwise, provide a numeric offset where "
|
|
525
|
+
"positive = FOLLOWING, negative = PRECEDING, "
|
|
526
|
+
"and 0 = CURRENT ROW."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
bound_type = (
|
|
530
|
+
LegendQLApiWindowFrameBoundType.UNBOUNDED_PRECEDING
|
|
531
|
+
if is_start_bound
|
|
532
|
+
else LegendQLApiWindowFrameBoundType.UNBOUNDED_FOLLOWING
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
return LegendQLApiWindowFrameBound(bound_type)
|
|
536
|
+
|
|
537
|
+
if not isinstance(value, (int, float)):
|
|
538
|
+
raise TypeError(
|
|
539
|
+
f"Invalid type for window frame boundary: {type(value).__name__}. "
|
|
540
|
+
"Expected 'unbounded' (str) or numeric offset (int | float)."
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
duration_unit_enum = (
|
|
544
|
+
LegendQLApiDurationUnit.from_string(duration_unit)
|
|
545
|
+
if duration_unit
|
|
546
|
+
else None
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
if value == 0:
|
|
550
|
+
return LegendQLApiWindowFrameBound(
|
|
551
|
+
LegendQLApiWindowFrameBoundType.CURRENT_ROW,
|
|
552
|
+
row_offset=None,
|
|
553
|
+
duration_unit=duration_unit_enum
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if value > 0:
|
|
557
|
+
bound_type = LegendQLApiWindowFrameBoundType.FOLLOWING
|
|
558
|
+
else:
|
|
559
|
+
bound_type = LegendQLApiWindowFrameBoundType.PRECEDING
|
|
560
|
+
|
|
561
|
+
return LegendQLApiWindowFrameBound(bound_type, value, duration_unit_enum)
|