pylegend 0.2.2__py3-none-any.whl → 0.4.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/__init__.py +9 -5
- pylegend/core/{databse → database}/sql_to_string/__init__.py +3 -3
- pylegend/core/{databse → database}/sql_to_string/db_extension.py +11 -5
- pylegend/core/{databse → database}/sql_to_string/generator.py +2 -2
- pylegend/core/language/__init__.py +10 -10
- pylegend/core/language/legacy_api/__init__.py +13 -0
- pylegend/core/language/{aggregate_specification.py → legacy_api/aggregate_specification.py} +10 -10
- pylegend/core/language/legacy_api/legacy_api_tds_row.py +32 -0
- pylegend/core/language/legendql_api/__init__.py +13 -0
- pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +541 -0
- pylegend/core/language/legendql_api/legendql_api_tds_row.py +292 -0
- pylegend/core/language/shared/__init__.py +13 -0
- pylegend/core/language/{column_expressions.py → shared/column_expressions.py} +32 -31
- pylegend/core/language/{expression.py → shared/expression.py} +8 -0
- pylegend/core/language/{functions.py → shared/functions.py} +3 -3
- pylegend/core/language/shared/helpers.py +75 -0
- pylegend/core/language/{literal_expressions.py → shared/literal_expressions.py} +39 -1
- pylegend/core/language/{operations → shared/operations}/binary_expression.py +34 -2
- pylegend/core/language/{operations → shared/operations}/boolean_operation_expressions.py +34 -6
- pylegend/core/language/{operations → shared/operations}/collection_operation_expressions.py +146 -26
- pylegend/core/language/{operations → shared/operations}/date_operation_expressions.py +164 -24
- pylegend/core/language/{operations → shared/operations}/float_operation_expressions.py +53 -8
- pylegend/core/language/{operations → shared/operations}/integer_operation_expressions.py +62 -9
- pylegend/core/language/{operations → shared/operations}/nullary_expression.py +9 -2
- pylegend/core/language/{operations → shared/operations}/number_operation_expressions.py +211 -30
- pylegend/core/language/{operations → shared/operations}/primitive_operation_expressions.py +42 -3
- pylegend/core/language/{operations → shared/operations}/string_operation_expressions.py +169 -21
- pylegend/core/language/{operations → shared/operations}/unary_expression.py +10 -2
- pylegend/core/language/{primitive_collection.py → shared/primitive_collection.py} +2 -2
- pylegend/core/language/{primitives → shared/primitives}/__init__.py +9 -9
- pylegend/core/language/{primitives → shared/primitives}/boolean.py +9 -5
- pylegend/core/language/{primitives → shared/primitives}/date.py +23 -15
- pylegend/core/language/{primitives → shared/primitives}/datetime.py +4 -5
- pylegend/core/language/{primitives → shared/primitives}/float.py +6 -6
- pylegend/core/language/{primitives → shared/primitives}/integer.py +6 -6
- pylegend/core/language/{primitives → shared/primitives}/number.py +16 -13
- pylegend/core/language/{primitives → shared/primitives}/primitive.py +25 -5
- pylegend/core/language/{primitives → shared/primitives}/strictdate.py +4 -5
- pylegend/core/language/{primitives → shared/primitives}/string.py +18 -19
- pylegend/core/language/{tds_row.py → shared/tds_row.py} +46 -16
- pylegend/core/request/__init__.py +8 -1
- pylegend/core/request/auth.py +89 -11
- pylegend/core/request/legend_client.py +32 -0
- pylegend/core/sql/metamodel_extension.py +16 -0
- pylegend/core/tds/abstract/__init__.py +13 -0
- pylegend/core/tds/abstract/frames/__init__.py +13 -0
- pylegend/core/tds/{legend_api/frames/legend_api_applied_function_tds_frame.py → abstract/frames/applied_function_tds_frame.py} +19 -13
- pylegend/core/tds/abstract/frames/base_tds_frame.py +125 -0
- pylegend/core/tds/{legend_api/frames/legend_api_input_tds_frame.py → abstract/frames/input_tds_frame.py} +9 -12
- pylegend/core/tds/{legend_api/frames/functions → abstract}/function_helpers.py +1 -1
- pylegend/core/tds/{legend_api/frames/functions/concatenate_function.py → legacy_api/frames/functions/legacy_api_concatenate_function.py} +25 -13
- pylegend/core/tds/{legend_api/frames/functions/distinct_function.py → legacy_api/frames/functions/legacy_api_distinct_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/drop_function.py → legacy_api/frames/functions/legacy_api_drop_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/extend_function.py → legacy_api/frames/functions/legacy_api_extend_function.py} +36 -16
- pylegend/core/tds/{legend_api/frames/functions/filter_function.py → legacy_api/frames/functions/legacy_api_filter_function.py} +25 -13
- pylegend/core/tds/{legend_api/frames/functions/group_by_function.py → legacy_api/frames/functions/legacy_api_group_by_function.py} +44 -17
- pylegend/core/tds/{legend_api/frames/functions/head_function.py → legacy_api/frames/functions/legacy_api_head_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/join_by_columns_function.py → legacy_api/frames/functions/legacy_api_join_by_columns_function.py} +40 -13
- pylegend/core/tds/{legend_api/frames/functions/join_function.py → legacy_api/frames/functions/legacy_api_join_function.py} +44 -20
- pylegend/core/tds/{legend_api/frames/functions/rename_columns_function.py → legacy_api/frames/functions/legacy_api_rename_columns_function.py} +20 -8
- pylegend/core/tds/{legend_api/frames/functions/restrict_function.py → legacy_api/frames/functions/legacy_api_restrict_function.py} +17 -8
- pylegend/core/tds/{legend_api/frames/functions/slice_function.py → legacy_api/frames/functions/legacy_api_slice_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/sort_function.py → legacy_api/frames/functions/legacy_api_sort_function.py} +19 -8
- pylegend/core/tds/legacy_api/frames/legacy_api_applied_function_tds_frame.py +37 -0
- pylegend/core/tds/legacy_api/frames/legacy_api_base_tds_frame.py +204 -0
- pylegend/core/tds/legacy_api/frames/legacy_api_input_tds_frame.py +51 -0
- pylegend/core/tds/{legend_api/frames/legend_api_tds_frame.py → legacy_api/frames/legacy_api_tds_frame.py} +28 -28
- pylegend/core/tds/legendql_api/__init__.py +13 -0
- pylegend/core/tds/legendql_api/frames/__init__.py +13 -0
- pylegend/core/tds/legendql_api/frames/functions/__init__.py +13 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_asofjoin_function.py +156 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_concatenate_function.py +139 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +69 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_drop_function.py +74 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_extend_function.py +256 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_filter_function.py +121 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_function_helpers.py +137 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_groupby_function.py +256 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_head_function.py +74 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_join_function.py +214 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_project_function.py +169 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_rename_function.py +189 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_select_function.py +131 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_slice_function.py +82 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_sort_function.py +93 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_window_extend_function.py +283 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_applied_function_tds_frame.py +37 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +419 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_input_tds_frame.py +50 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +327 -0
- pylegend/core/tds/pandas_api/frames/functions/assign_function.py +6 -6
- pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +4 -0
- pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +11 -3
- pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +2 -2
- pylegend/core/tds/tds_frame.py +32 -2
- pylegend/extensions/database/vendors/postgres/postgres_sql_to_string.py +1 -1
- pylegend/extensions/tds/abstract/legend_function_input_frame.py +4 -0
- pylegend/extensions/tds/abstract/legend_service_input_frame.py +4 -0
- pylegend/extensions/tds/abstract/table_spec_input_frame.py +4 -0
- pylegend/extensions/tds/{legend_api/frames/legend_api_legend_function_input_frame.py → legacy_api/frames/legacy_api_legend_function_input_frame.py} +5 -5
- pylegend/extensions/tds/{legend_api/frames/legend_api_legend_service_input_frame.py → legacy_api/frames/legacy_api_legend_service_input_frame.py} +6 -6
- pylegend/extensions/tds/{legend_api/frames/legend_api_table_spec_input_frame.py → legacy_api/frames/legacy_api_table_spec_input_frame.py} +5 -5
- pylegend/extensions/tds/legendql_api/__init__.py +13 -0
- pylegend/extensions/tds/legendql_api/frames/__init__.py +13 -0
- pylegend/extensions/tds/legendql_api/frames/legendql_api_legend_service_input_frame.py +46 -0
- pylegend/extensions/tds/legendql_api/frames/legendql_api_table_spec_input_frame.py +36 -0
- pylegend/{legend_api_tds_client.py → legacy_api_tds_client.py} +15 -15
- {pylegend-0.2.2.dist-info → pylegend-0.4.0.dist-info}/METADATA +7 -8
- pylegend-0.4.0.dist-info/NOTICE +5 -0
- pylegend-0.4.0.dist-info/RECORD +155 -0
- {pylegend-0.2.2.dist-info → pylegend-0.4.0.dist-info}/WHEEL +1 -1
- pylegend/core/tds/legend_api/frames/legend_api_base_tds_frame.py +0 -294
- pylegend-0.2.2.dist-info/RECORD +0 -115
- /pylegend/core/{databse → database}/__init__.py +0 -0
- /pylegend/core/{databse → database}/sql_to_string/config.py +0 -0
- /pylegend/core/language/{operations → shared/operations}/__init__.py +0 -0
- /pylegend/core/tds/{legend_api → legacy_api}/__init__.py +0 -0
- /pylegend/core/tds/{legend_api → legacy_api}/frames/__init__.py +0 -0
- /pylegend/core/tds/{legend_api → legacy_api}/frames/functions/__init__.py +0 -0
- /pylegend/extensions/tds/{legend_api → legacy_api}/__init__.py +0 -0
- /pylegend/extensions/tds/{legend_api → legacy_api}/frames/__init__.py +0 -0
- {pylegend-0.2.2.dist-info → pylegend-0.4.0.dist-info}/LICENSE +0 -0
- {pylegend-0.2.2.dist-info → pylegend-0.4.0.dist-info}/LICENSE.spdx +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Copyright 2025 Goldman Sachs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from pylegend._typing import (
|
|
16
|
+
PyLegendList,
|
|
17
|
+
PyLegendSequence,
|
|
18
|
+
PyLegendCallable,
|
|
19
|
+
PyLegendUnion,
|
|
20
|
+
)
|
|
21
|
+
from pylegend.core.language import PyLegendColumnExpression
|
|
22
|
+
from pylegend.core.language.legendql_api.legendql_api_custom_expressions import (
|
|
23
|
+
LegendQLApiPrimitive,
|
|
24
|
+
LegendQLApiSortInfo,
|
|
25
|
+
LegendQLApiSortDirection,
|
|
26
|
+
)
|
|
27
|
+
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
28
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__: PyLegendSequence[str] = [
|
|
32
|
+
"infer_columns_from_frame",
|
|
33
|
+
"infer_sorts_from_frame",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def infer_columns_from_frame(
|
|
38
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
39
|
+
columns: PyLegendUnion[
|
|
40
|
+
str,
|
|
41
|
+
PyLegendList[str],
|
|
42
|
+
PyLegendCallable[
|
|
43
|
+
[LegendQLApiTdsRow],
|
|
44
|
+
PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
|
|
45
|
+
]
|
|
46
|
+
],
|
|
47
|
+
context: str
|
|
48
|
+
) -> PyLegendList[str]:
|
|
49
|
+
if isinstance(columns, str):
|
|
50
|
+
return [columns]
|
|
51
|
+
if isinstance(columns, list) and all([isinstance(c, str) for c in columns]):
|
|
52
|
+
return columns
|
|
53
|
+
|
|
54
|
+
if isinstance(columns, type(lambda x: 0)) and (columns.__code__.co_argcount == 1):
|
|
55
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("frame", base_frame)
|
|
56
|
+
try:
|
|
57
|
+
result = columns(tds_row)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"{context} argument lambda incompatible. Error occurred while evaluating. Message: " + str(e)
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
list_result = result if isinstance(result, list) else [result]
|
|
64
|
+
|
|
65
|
+
columns_list = []
|
|
66
|
+
for (i, r) in enumerate(list_result):
|
|
67
|
+
if isinstance(r, LegendQLApiPrimitive) and isinstance(r.value(), PyLegendColumnExpression):
|
|
68
|
+
col_expr: PyLegendColumnExpression = r.value()
|
|
69
|
+
columns_list.append(col_expr.get_column())
|
|
70
|
+
else:
|
|
71
|
+
raise TypeError(
|
|
72
|
+
f"{context} argument lambda incompatible. Columns can be simple column expressions "
|
|
73
|
+
f"(E.g - lambda r: [r.column1, r.column2, r['column with spaces']). "
|
|
74
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
75
|
+
)
|
|
76
|
+
return columns_list
|
|
77
|
+
|
|
78
|
+
raise TypeError(f"{context} argument can either be a list of strings (column names) or "
|
|
79
|
+
f"a lambda function which takes one argument (LegendQLApiTdsRow)")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def infer_sorts_from_frame(
|
|
83
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
84
|
+
sort_infos: PyLegendUnion[
|
|
85
|
+
str,
|
|
86
|
+
PyLegendList[str],
|
|
87
|
+
PyLegendCallable[
|
|
88
|
+
[LegendQLApiTdsRow],
|
|
89
|
+
PyLegendUnion[
|
|
90
|
+
LegendQLApiPrimitive,
|
|
91
|
+
LegendQLApiSortInfo,
|
|
92
|
+
PyLegendList[PyLegendUnion[LegendQLApiPrimitive, LegendQLApiSortInfo]],
|
|
93
|
+
]
|
|
94
|
+
]
|
|
95
|
+
],
|
|
96
|
+
context: str
|
|
97
|
+
) -> PyLegendList[LegendQLApiSortInfo]:
|
|
98
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("frame", base_frame)
|
|
99
|
+
sort_info_list = []
|
|
100
|
+
|
|
101
|
+
if isinstance(sort_infos, str):
|
|
102
|
+
col_expr1: PyLegendColumnExpression = tds_row[sort_infos].value() # type: ignore
|
|
103
|
+
sort_info_list.append(LegendQLApiSortInfo(col_expr1, LegendQLApiSortDirection.ASC))
|
|
104
|
+
|
|
105
|
+
elif isinstance(sort_infos, list) and all([isinstance(s, str) for s in sort_infos]):
|
|
106
|
+
for s in sort_infos:
|
|
107
|
+
col_expr2: PyLegendColumnExpression = tds_row[s].value() # type: ignore
|
|
108
|
+
sort_info_list.append(LegendQLApiSortInfo(col_expr2, LegendQLApiSortDirection.ASC))
|
|
109
|
+
|
|
110
|
+
elif isinstance(sort_infos, type(lambda x: 0)) and (sort_infos.__code__.co_argcount == 1):
|
|
111
|
+
try:
|
|
112
|
+
result = sort_infos(tds_row)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
f"{context} argument lambda incompatible. "
|
|
116
|
+
"Error occurred while evaluating. Message: " + str(e)
|
|
117
|
+
) from e
|
|
118
|
+
|
|
119
|
+
list_result = result if isinstance(result, list) else [result]
|
|
120
|
+
for (i, r) in enumerate(list_result):
|
|
121
|
+
if isinstance(r, LegendQLApiPrimitive) and isinstance(r.value(), PyLegendColumnExpression):
|
|
122
|
+
col_expr3: PyLegendColumnExpression = r.value()
|
|
123
|
+
sort_info_list.append(LegendQLApiSortInfo(col_expr3, LegendQLApiSortDirection.ASC))
|
|
124
|
+
elif isinstance(r, LegendQLApiSortInfo):
|
|
125
|
+
sort_info_list.append(r)
|
|
126
|
+
else:
|
|
127
|
+
raise TypeError(
|
|
128
|
+
f"{context} argument lambda incompatible. Columns can be simple column "
|
|
129
|
+
f"expressions or sort infos. "
|
|
130
|
+
f"(E.g - lambda r: [r.column1, r['column with spaces'].descending(), r.column3.ascending()). "
|
|
131
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
raise TypeError(f"{context} argument can either be a list of strings (column names) or "
|
|
135
|
+
"a lambda function which takes one argument (LegendQLApiTdsRow)")
|
|
136
|
+
|
|
137
|
+
return sort_info_list
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Copyright 2025 Goldman Sachs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from datetime import date, datetime
|
|
16
|
+
from pylegend._typing import (
|
|
17
|
+
PyLegendList,
|
|
18
|
+
PyLegendSequence,
|
|
19
|
+
PyLegendTuple,
|
|
20
|
+
PyLegendCallable,
|
|
21
|
+
PyLegendUnion,
|
|
22
|
+
)
|
|
23
|
+
from pylegend.core.language import (
|
|
24
|
+
PyLegendPrimitiveOrPythonPrimitive,
|
|
25
|
+
PyLegendPrimitiveCollection,
|
|
26
|
+
PyLegendPrimitive,
|
|
27
|
+
create_primitive_collection,
|
|
28
|
+
convert_literal_to_literal_expression
|
|
29
|
+
)
|
|
30
|
+
from pylegend.core.language.legendql_api.legendql_api_custom_expressions import LegendQLApiPrimitive
|
|
31
|
+
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
32
|
+
from pylegend.core.tds.abstract.function_helpers import tds_column_for_primitive
|
|
33
|
+
from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import infer_columns_from_frame
|
|
34
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
|
|
35
|
+
from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
|
|
36
|
+
from pylegend.core.sql.metamodel import (
|
|
37
|
+
QuerySpecification,
|
|
38
|
+
SingleColumn,
|
|
39
|
+
SelectItem
|
|
40
|
+
)
|
|
41
|
+
from pylegend.core.tds.tds_column import TdsColumn
|
|
42
|
+
from pylegend.core.tds.tds_frame import FrameToSqlConfig
|
|
43
|
+
from pylegend.core.tds.tds_frame import FrameToPureConfig
|
|
44
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
45
|
+
from pylegend.core.language.shared.helpers import escape_column_name, generate_pure_lambda
|
|
46
|
+
|
|
47
|
+
__all__: PyLegendSequence[str] = [
|
|
48
|
+
"LegendQLApiGroupByFunction"
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LegendQLApiGroupByFunction(LegendQLApiAppliedFunction):
|
|
53
|
+
__base_frame: LegendQLApiBaseTdsFrame
|
|
54
|
+
__grouping_column_name_list: PyLegendList[str]
|
|
55
|
+
__aggregates_list: PyLegendList[PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive, PyLegendPrimitive]]
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def name(cls) -> str:
|
|
59
|
+
return "groupBy"
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
64
|
+
grouping_columns: PyLegendUnion[
|
|
65
|
+
str,
|
|
66
|
+
PyLegendList[str],
|
|
67
|
+
PyLegendCallable[
|
|
68
|
+
[LegendQLApiTdsRow],
|
|
69
|
+
PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
|
|
70
|
+
]
|
|
71
|
+
],
|
|
72
|
+
aggregate_specifications: PyLegendUnion[
|
|
73
|
+
PyLegendTuple[
|
|
74
|
+
str,
|
|
75
|
+
PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive],
|
|
76
|
+
PyLegendCallable[[PyLegendPrimitiveCollection], PyLegendPrimitive]
|
|
77
|
+
],
|
|
78
|
+
PyLegendList[
|
|
79
|
+
PyLegendTuple[
|
|
80
|
+
str,
|
|
81
|
+
PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive],
|
|
82
|
+
PyLegendCallable[[PyLegendPrimitiveCollection], PyLegendPrimitive]
|
|
83
|
+
]
|
|
84
|
+
]
|
|
85
|
+
]
|
|
86
|
+
) -> None:
|
|
87
|
+
self.__base_frame = base_frame
|
|
88
|
+
self.__grouping_column_name_list = infer_columns_from_frame(
|
|
89
|
+
base_frame, grouping_columns, "'group_by' function grouping columns"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("r", self.__base_frame)
|
|
93
|
+
list_result = (
|
|
94
|
+
aggregate_specifications if isinstance(aggregate_specifications, list) else [aggregate_specifications]
|
|
95
|
+
)
|
|
96
|
+
aggregates_list: PyLegendList[PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive, PyLegendPrimitive]] = []
|
|
97
|
+
for (i, agg_spec) in enumerate(list_result):
|
|
98
|
+
error = (
|
|
99
|
+
"'group_by' function aggregate specifications incompatible. "
|
|
100
|
+
"Each aggregate specification should be a triplet with first element being the aggregation column "
|
|
101
|
+
"name, second element being a mapper function (single argument lambda) and third element being the "
|
|
102
|
+
"aggregation function (single argument lambda). "
|
|
103
|
+
"E.g - ('count_col', lambda r: r['col1'], lambda c: c.count()). "
|
|
104
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if isinstance(agg_spec, tuple) and isinstance(agg_spec[0], str):
|
|
108
|
+
|
|
109
|
+
if not isinstance(agg_spec[1], type(lambda x: 0)) or (agg_spec[1].__code__.co_argcount != 1):
|
|
110
|
+
raise TypeError(error)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
map_result = agg_spec[1](tds_row)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
"'group_by' function aggregate specifications incompatible. "
|
|
117
|
+
f"Error occurred while evaluating mapper lambda in the aggregate specification at "
|
|
118
|
+
f"index {i} (0-indexed). Message: " + str(e)
|
|
119
|
+
) from e
|
|
120
|
+
|
|
121
|
+
if not isinstance(map_result, (int, float, bool, str, date, datetime, PyLegendPrimitive)):
|
|
122
|
+
raise TypeError(
|
|
123
|
+
"'group_by' function aggregate specifications incompatible. "
|
|
124
|
+
f"Mapper lambda in the aggregate specification at index {i} (0-indexed) "
|
|
125
|
+
f"returns non-primitive - {str(type(map_result))}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
collection = create_primitive_collection(map_result)
|
|
129
|
+
|
|
130
|
+
if not isinstance(agg_spec[2], type(lambda x: 0)) or (agg_spec[2].__code__.co_argcount != 1):
|
|
131
|
+
raise TypeError(error)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
agg_result = agg_spec[2](collection)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"'group_by' function aggregate specifications incompatible. "
|
|
138
|
+
f"Error occurred while evaluating aggregation lambda in the aggregate specification at "
|
|
139
|
+
f"index {i} (0-indexed). Message: " + str(e)
|
|
140
|
+
) from e
|
|
141
|
+
|
|
142
|
+
if not isinstance(agg_result, PyLegendPrimitive):
|
|
143
|
+
raise TypeError(
|
|
144
|
+
"'group_by' function aggregate specifications incompatible. "
|
|
145
|
+
f"Aggregation lambda in the aggregate specification at index {i} (0-indexed) "
|
|
146
|
+
f"returns non-primitive - {str(type(agg_result))}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
aggregates_list.append((agg_spec[0], map_result, agg_result))
|
|
150
|
+
|
|
151
|
+
else:
|
|
152
|
+
raise TypeError(error)
|
|
153
|
+
|
|
154
|
+
self.__aggregates_list = aggregates_list
|
|
155
|
+
|
|
156
|
+
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
157
|
+
db_extension = config.sql_to_string_generator().get_db_extension()
|
|
158
|
+
base_query = self.__base_frame.to_sql_query_object(config)
|
|
159
|
+
|
|
160
|
+
should_create_sub_query = (len(base_query.groupBy) > 0) or base_query.select.distinct or \
|
|
161
|
+
(base_query.offset is not None) or (base_query.limit is not None)
|
|
162
|
+
|
|
163
|
+
columns_to_retain = [db_extension.quote_identifier(x) for x in self.__grouping_column_name_list]
|
|
164
|
+
if should_create_sub_query:
|
|
165
|
+
new_query = create_sub_query(base_query, config, "root")
|
|
166
|
+
else:
|
|
167
|
+
new_query = copy_query(base_query)
|
|
168
|
+
|
|
169
|
+
new_cols_with_index: PyLegendList[PyLegendTuple[int, 'SelectItem']] = []
|
|
170
|
+
for col in new_query.select.selectItems:
|
|
171
|
+
if not isinstance(col, SingleColumn):
|
|
172
|
+
raise ValueError("Group By operation not supported for queries "
|
|
173
|
+
"with columns other than SingleColumn") # pragma: no cover
|
|
174
|
+
if col.alias is None:
|
|
175
|
+
raise ValueError("Group By operation not supported for queries "
|
|
176
|
+
"with SingleColumns with missing alias") # pragma: no cover
|
|
177
|
+
if col.alias in columns_to_retain:
|
|
178
|
+
new_cols_with_index.append((columns_to_retain.index(col.alias), col))
|
|
179
|
+
|
|
180
|
+
new_select_items = [y[1] for y in sorted(new_cols_with_index, key=lambda x: x[0])]
|
|
181
|
+
|
|
182
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("r", self.__base_frame)
|
|
183
|
+
for agg in self.__aggregates_list:
|
|
184
|
+
agg_sql_expr = agg[2].to_sql_expression({"r": new_query}, config)
|
|
185
|
+
|
|
186
|
+
new_select_items.append(
|
|
187
|
+
SingleColumn(alias=db_extension.quote_identifier(agg[0]), expression=agg_sql_expr)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
new_query.select.selectItems = new_select_items
|
|
191
|
+
new_query.groupBy = [
|
|
192
|
+
(lambda x: x[c])(tds_row).to_sql_expression({"r": new_query}, config)
|
|
193
|
+
for c in self.__grouping_column_name_list
|
|
194
|
+
]
|
|
195
|
+
return new_query
|
|
196
|
+
|
|
197
|
+
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
198
|
+
group_strings = []
|
|
199
|
+
for col_name in self.__grouping_column_name_list:
|
|
200
|
+
group_strings.append(escape_column_name(col_name))
|
|
201
|
+
|
|
202
|
+
agg_strings = []
|
|
203
|
+
for agg in self.__aggregates_list:
|
|
204
|
+
map_expr_string = (agg[1].to_pure_expression(config) if isinstance(agg[1], PyLegendPrimitive)
|
|
205
|
+
else convert_literal_to_literal_expression(agg[1]).to_pure_expression(config))
|
|
206
|
+
agg_expr_string = agg[2].to_pure_expression(config).replace(map_expr_string, "$c")
|
|
207
|
+
agg_strings.append(f"{escape_column_name(agg[0])}:{generate_pure_lambda('r', map_expr_string)}:"
|
|
208
|
+
f"{generate_pure_lambda('c', agg_expr_string)}")
|
|
209
|
+
|
|
210
|
+
return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}" +
|
|
211
|
+
f"->groupBy({config.separator(2)}"
|
|
212
|
+
f"~[{', '.join(group_strings)}],{config.separator(2, True)}"
|
|
213
|
+
f"~[{', '.join(agg_strings)}]{config.separator(1)}"
|
|
214
|
+
f")")
|
|
215
|
+
|
|
216
|
+
def base_frame(self) -> LegendQLApiBaseTdsFrame:
|
|
217
|
+
return self.__base_frame
|
|
218
|
+
|
|
219
|
+
def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
223
|
+
base_columns = self.__base_frame.columns()
|
|
224
|
+
new_columns = []
|
|
225
|
+
for c in self.__grouping_column_name_list:
|
|
226
|
+
for base_col in base_columns:
|
|
227
|
+
if base_col.get_name() == c:
|
|
228
|
+
new_columns.append(base_col.copy())
|
|
229
|
+
break
|
|
230
|
+
for agg in self.__aggregates_list:
|
|
231
|
+
new_columns.append(tds_column_for_primitive(agg[0], agg[2]))
|
|
232
|
+
return new_columns
|
|
233
|
+
|
|
234
|
+
def validate(self) -> bool:
|
|
235
|
+
base_columns = self.__base_frame.columns()
|
|
236
|
+
for c in self.__grouping_column_name_list:
|
|
237
|
+
found_col = False
|
|
238
|
+
for base_col in base_columns:
|
|
239
|
+
if base_col.get_name() == c:
|
|
240
|
+
found_col = True
|
|
241
|
+
break
|
|
242
|
+
if not found_col:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Column - '{c}' in group_by columns list doesn't exist in the current frame. "
|
|
245
|
+
f"Current frame columns: {[x.get_name() for x in base_columns]}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
agg_cols = [c[0] for c in self.__aggregates_list]
|
|
249
|
+
new_cols = self.__grouping_column_name_list + agg_cols
|
|
250
|
+
if len(new_cols) == 0:
|
|
251
|
+
raise ValueError("At-least one grouping column or aggregate specification must be provided "
|
|
252
|
+
"when using group_by function")
|
|
253
|
+
if len(new_cols) != len(set(new_cols)):
|
|
254
|
+
raise ValueError("Found duplicate column names in grouping columns and aggregation columns. "
|
|
255
|
+
f"Grouping columns - {self.__grouping_column_name_list}, Aggregation columns - {agg_cols}")
|
|
256
|
+
return True
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copyright 2025 Goldman Sachs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from pylegend._typing import (
|
|
16
|
+
PyLegendList,
|
|
17
|
+
PyLegendSequence,
|
|
18
|
+
)
|
|
19
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
|
|
20
|
+
from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
|
|
21
|
+
from pylegend.core.sql.metamodel import (
|
|
22
|
+
QuerySpecification,
|
|
23
|
+
LongLiteral,
|
|
24
|
+
)
|
|
25
|
+
from pylegend.core.tds.tds_column import TdsColumn
|
|
26
|
+
from pylegend.core.tds.tds_frame import FrameToSqlConfig
|
|
27
|
+
from pylegend.core.tds.tds_frame import FrameToPureConfig
|
|
28
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__: PyLegendSequence[str] = [
|
|
32
|
+
"LegendQLApiHeadFunction"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LegendQLApiHeadFunction(LegendQLApiAppliedFunction):
|
|
37
|
+
__base_frame: LegendQLApiBaseTdsFrame
|
|
38
|
+
__row_count: int
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def name(cls) -> str:
|
|
42
|
+
return "head"
|
|
43
|
+
|
|
44
|
+
def __init__(self, base_frame: LegendQLApiBaseTdsFrame, row_count: int) -> None:
|
|
45
|
+
self.__base_frame = base_frame
|
|
46
|
+
self.__row_count = row_count
|
|
47
|
+
|
|
48
|
+
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
49
|
+
base_query = self.__base_frame.to_sql_query_object(config)
|
|
50
|
+
should_create_sub_query = (base_query.limit is not None)
|
|
51
|
+
new_query = (
|
|
52
|
+
create_sub_query(base_query, config, "root") if should_create_sub_query else
|
|
53
|
+
copy_query(base_query)
|
|
54
|
+
)
|
|
55
|
+
new_query.limit = LongLiteral(value=self.__row_count)
|
|
56
|
+
return new_query
|
|
57
|
+
|
|
58
|
+
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
59
|
+
return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}"
|
|
60
|
+
f"->limit({self.__row_count})")
|
|
61
|
+
|
|
62
|
+
def base_frame(self) -> LegendQLApiBaseTdsFrame:
|
|
63
|
+
return self.__base_frame
|
|
64
|
+
|
|
65
|
+
def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
69
|
+
return [c.copy() for c in self.__base_frame.columns()]
|
|
70
|
+
|
|
71
|
+
def validate(self) -> bool:
|
|
72
|
+
if self.__row_count < 0:
|
|
73
|
+
raise ValueError("Row count argument of head/limit function cannot be negative")
|
|
74
|
+
return True
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Copyright 2025 Goldman Sachs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from pylegend._typing import (
|
|
16
|
+
PyLegendList,
|
|
17
|
+
PyLegendSequence,
|
|
18
|
+
PyLegendUnion,
|
|
19
|
+
PyLegendCallable,
|
|
20
|
+
)
|
|
21
|
+
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
22
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
|
|
23
|
+
from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query, extract_columns_for_subquery
|
|
24
|
+
from pylegend.core.sql.metamodel import (
|
|
25
|
+
QuerySpecification,
|
|
26
|
+
Select,
|
|
27
|
+
SelectItem,
|
|
28
|
+
SingleColumn,
|
|
29
|
+
AliasedRelation,
|
|
30
|
+
TableSubquery,
|
|
31
|
+
Query,
|
|
32
|
+
Join,
|
|
33
|
+
JoinType,
|
|
34
|
+
JoinOn,
|
|
35
|
+
QualifiedNameReference,
|
|
36
|
+
QualifiedName,
|
|
37
|
+
)
|
|
38
|
+
from pylegend.core.tds.tds_column import TdsColumn
|
|
39
|
+
from pylegend.core.tds.tds_frame import FrameToSqlConfig
|
|
40
|
+
from pylegend.core.tds.tds_frame import FrameToPureConfig
|
|
41
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
42
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_tds_frame import LegendQLApiTdsFrame
|
|
43
|
+
from pylegend.core.language import (
|
|
44
|
+
PyLegendBoolean,
|
|
45
|
+
PyLegendBooleanLiteralExpression,
|
|
46
|
+
PyLegendPrimitive,
|
|
47
|
+
convert_literal_to_literal_expression,
|
|
48
|
+
)
|
|
49
|
+
from pylegend.core.language.shared.helpers import generate_pure_lambda
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__: PyLegendSequence[str] = [
|
|
53
|
+
"LegendQLApiJoinFunction"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LegendQLApiJoinFunction(LegendQLApiAppliedFunction):
|
|
58
|
+
__base_frame: LegendQLApiBaseTdsFrame
|
|
59
|
+
__other_frame: LegendQLApiBaseTdsFrame
|
|
60
|
+
__join_condition: PyLegendCallable[[LegendQLApiTdsRow, LegendQLApiTdsRow], PyLegendUnion[bool, PyLegendBoolean]]
|
|
61
|
+
__join_type: str
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def name(cls) -> str:
|
|
65
|
+
return "join"
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
70
|
+
other_frame: LegendQLApiTdsFrame,
|
|
71
|
+
join_condition: PyLegendCallable[[LegendQLApiTdsRow, LegendQLApiTdsRow], PyLegendUnion[bool, PyLegendBoolean]],
|
|
72
|
+
join_type: str
|
|
73
|
+
) -> None:
|
|
74
|
+
self.__base_frame = base_frame
|
|
75
|
+
if not isinstance(other_frame, LegendQLApiBaseTdsFrame):
|
|
76
|
+
raise ValueError("Expected LegendQLApiBaseTdsFrame") # pragma: no cover
|
|
77
|
+
self.__other_frame = other_frame
|
|
78
|
+
self.__join_condition = join_condition
|
|
79
|
+
self.__join_type = join_type
|
|
80
|
+
|
|
81
|
+
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
82
|
+
db_extension = config.sql_to_string_generator().get_db_extension()
|
|
83
|
+
base_query = copy_query(self.__base_frame.to_sql_query_object(config))
|
|
84
|
+
other_query = copy_query(self.__other_frame.to_sql_query_object(config))
|
|
85
|
+
|
|
86
|
+
join_type = (
|
|
87
|
+
JoinType.INNER if self.__join_type.lower() == 'inner' else (
|
|
88
|
+
JoinType.LEFT if self.__join_type.lower() in ('left_outer', 'leftouter') else
|
|
89
|
+
JoinType.RIGHT
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
left_row = LegendQLApiTdsRow.from_tds_frame('left', self.__base_frame)
|
|
94
|
+
right_row = LegendQLApiTdsRow.from_tds_frame('right', self.__other_frame)
|
|
95
|
+
|
|
96
|
+
join_expr = self.__join_condition(left_row, right_row)
|
|
97
|
+
if isinstance(join_expr, bool):
|
|
98
|
+
join_expr = PyLegendBoolean(PyLegendBooleanLiteralExpression(join_expr))
|
|
99
|
+
join_sql_expr = join_expr.to_sql_expression(
|
|
100
|
+
{
|
|
101
|
+
'left': create_sub_query(base_query, config, 'left'),
|
|
102
|
+
'right': create_sub_query(other_query, config, 'right'),
|
|
103
|
+
},
|
|
104
|
+
config
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
left_alias = db_extension.quote_identifier('left')
|
|
108
|
+
right_alias = db_extension.quote_identifier('right')
|
|
109
|
+
new_select_items: PyLegendList[SelectItem] = []
|
|
110
|
+
for c in self.__base_frame.columns():
|
|
111
|
+
q = db_extension.quote_identifier(c.get_name())
|
|
112
|
+
new_select_items.append(SingleColumn(q, QualifiedNameReference(name=QualifiedName(parts=[left_alias, q]))))
|
|
113
|
+
for c in self.__other_frame.columns():
|
|
114
|
+
q = db_extension.quote_identifier(c.get_name())
|
|
115
|
+
new_select_items.append(SingleColumn(q, QualifiedNameReference(name=QualifiedName(parts=[right_alias, q]))))
|
|
116
|
+
|
|
117
|
+
join_query = QuerySpecification(
|
|
118
|
+
select=Select(
|
|
119
|
+
selectItems=new_select_items,
|
|
120
|
+
distinct=False
|
|
121
|
+
),
|
|
122
|
+
from_=[
|
|
123
|
+
Join(
|
|
124
|
+
type_=join_type,
|
|
125
|
+
left=AliasedRelation(
|
|
126
|
+
relation=TableSubquery(query=Query(queryBody=base_query, limit=None, offset=None, orderBy=[])),
|
|
127
|
+
alias=left_alias,
|
|
128
|
+
columnNames=extract_columns_for_subquery(base_query)
|
|
129
|
+
),
|
|
130
|
+
right=AliasedRelation(
|
|
131
|
+
relation=TableSubquery(query=Query(queryBody=other_query, limit=None, offset=None, orderBy=[])),
|
|
132
|
+
alias=right_alias,
|
|
133
|
+
columnNames=extract_columns_for_subquery(other_query)
|
|
134
|
+
),
|
|
135
|
+
criteria=JoinOn(expression=join_sql_expr)
|
|
136
|
+
)
|
|
137
|
+
],
|
|
138
|
+
where=None,
|
|
139
|
+
groupBy=[],
|
|
140
|
+
having=None,
|
|
141
|
+
orderBy=[],
|
|
142
|
+
limit=None,
|
|
143
|
+
offset=None
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
wrapped_join_query = create_sub_query(join_query, config, "root")
|
|
147
|
+
return wrapped_join_query
|
|
148
|
+
|
|
149
|
+
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
150
|
+
left_row = LegendQLApiTdsRow.from_tds_frame("l", self.__base_frame)
|
|
151
|
+
right_row = LegendQLApiTdsRow.from_tds_frame("r", self.__other_frame)
|
|
152
|
+
join_expr = self.__join_condition(left_row, right_row)
|
|
153
|
+
join_expr_string = (join_expr.to_pure_expression(config.push_indent(2))
|
|
154
|
+
if isinstance(join_expr, PyLegendPrimitive) else
|
|
155
|
+
convert_literal_to_literal_expression(join_expr).to_pure_expression(config.push_indent(2)))
|
|
156
|
+
join_kind = (
|
|
157
|
+
"INNER" if self.__join_type.lower() == 'inner' else
|
|
158
|
+
"LEFT" if self.__join_type.lower() in ('left_outer', 'leftouter') else
|
|
159
|
+
"RIGHT" if self.__join_type.lower() in ('right_outer', 'rightouter') else
|
|
160
|
+
"FULL"
|
|
161
|
+
)
|
|
162
|
+
return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}" +
|
|
163
|
+
f"->join({config.separator(2)}"
|
|
164
|
+
f"{self.__other_frame.to_pure(config.push_indent(2))},{config.separator(2, True)}"
|
|
165
|
+
f"JoinKind.{join_kind},{config.separator(2, True)}"
|
|
166
|
+
f"{generate_pure_lambda('l, r', join_expr_string)}{config.separator(1)}"
|
|
167
|
+
f")")
|
|
168
|
+
|
|
169
|
+
def base_frame(self) -> LegendQLApiBaseTdsFrame:
|
|
170
|
+
return self.__base_frame
|
|
171
|
+
|
|
172
|
+
def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
|
|
173
|
+
return [self.__other_frame]
|
|
174
|
+
|
|
175
|
+
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
176
|
+
return (
|
|
177
|
+
[c.copy() for c in self.__base_frame.columns()] +
|
|
178
|
+
[c.copy() for c in self.__other_frame.columns()]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def validate(self) -> bool:
|
|
182
|
+
copy = self.__join_condition # For MyPy
|
|
183
|
+
if not isinstance(copy, type(lambda x: 0)) or (copy.__code__.co_argcount != 2):
|
|
184
|
+
raise TypeError("Join condition function should be a lambda which takes two arguments (TDSRow, TDSRow)")
|
|
185
|
+
|
|
186
|
+
left_row = LegendQLApiTdsRow.from_tds_frame("left", self.__base_frame)
|
|
187
|
+
right_row = LegendQLApiTdsRow.from_tds_frame("right", self.__other_frame)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
result = self.__join_condition(left_row, right_row)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
"Join condition function incompatible. Error occurred while evaluating. Message: " + str(e)
|
|
194
|
+
) from e
|
|
195
|
+
|
|
196
|
+
if not isinstance(result, (bool, PyLegendBoolean)):
|
|
197
|
+
raise RuntimeError("Join condition function incompatible. Returns non boolean - " + str(type(result)))
|
|
198
|
+
|
|
199
|
+
left_cols = [c.get_name() for c in self.__base_frame.columns()]
|
|
200
|
+
right_cols = [c.get_name() for c in self.__other_frame.columns()]
|
|
201
|
+
|
|
202
|
+
final_cols = left_cols + right_cols
|
|
203
|
+
if len(final_cols) != len(set(final_cols)):
|
|
204
|
+
raise ValueError(
|
|
205
|
+
"Found duplicate columns in joined frames. Use rename function to ensure there are no duplicate columns "
|
|
206
|
+
f"in joined frames. Columns - Left Frame: {left_cols}, Right Frame: {right_cols}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if self.__join_type.lower() not in ('inner', 'left_outer', 'right_outer', 'leftouter', 'rightouter'):
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"Unknown join type - {self.__join_type}. Supported types are - INNER, LEFT_OUTER, RIGHT_OUTER"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return True
|