vtlengine 1.1rc1__py3-none-any.whl → 1.1.1__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.
Potentially problematic release.
This version of vtlengine might be problematic. Click here for more details.
- vtlengine/API/_InternalApi.py +231 -6
- vtlengine/API/__init__.py +258 -69
- vtlengine/AST/ASTComment.py +56 -0
- vtlengine/AST/ASTConstructor.py +71 -18
- vtlengine/AST/ASTConstructorModules/Expr.py +191 -72
- vtlengine/AST/ASTConstructorModules/ExprComponents.py +81 -38
- vtlengine/AST/ASTConstructorModules/Terminals.py +76 -31
- vtlengine/AST/ASTConstructorModules/__init__.py +50 -0
- vtlengine/AST/ASTEncoders.py +4 -0
- vtlengine/AST/ASTString.py +622 -0
- vtlengine/AST/ASTTemplate.py +28 -2
- vtlengine/AST/DAG/__init__.py +10 -1
- vtlengine/AST/__init__.py +127 -14
- vtlengine/Exceptions/messages.py +9 -0
- vtlengine/Interpreter/__init__.py +53 -8
- vtlengine/Model/__init__.py +9 -4
- vtlengine/Operators/Aggregation.py +7 -5
- vtlengine/Operators/Analytic.py +16 -11
- vtlengine/Operators/Conditional.py +20 -5
- vtlengine/Operators/Time.py +11 -10
- vtlengine/Utils/__init__.py +49 -0
- vtlengine/__init__.py +4 -2
- vtlengine/files/parser/__init__.py +16 -26
- vtlengine/files/parser/_rfc_dialect.py +1 -1
- vtlengine/py.typed +0 -0
- vtlengine-1.1.1.dist-info/METADATA +92 -0
- {vtlengine-1.1rc1.dist-info → vtlengine-1.1.1.dist-info}/RECORD +29 -26
- {vtlengine-1.1rc1.dist-info → vtlengine-1.1.1.dist-info}/WHEEL +1 -1
- vtlengine-1.1rc1.dist-info/METADATA +0 -248
- {vtlengine-1.1rc1.dist-info → vtlengine-1.1.1.dist-info}/LICENSE.md +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Dict, Union
|
|
2
|
+
|
|
3
|
+
from antlr4.ParserRuleContext import ParserRuleContext
|
|
4
|
+
from antlr4.Token import CommonToken
|
|
5
|
+
|
|
6
|
+
from vtlengine.AST.Grammar.lexer import Lexer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_token_info(token: Union[CommonToken, ParserRuleContext]) -> Dict[str, int]:
|
|
10
|
+
"""
|
|
11
|
+
Extracts the token information from a token or ParserRuleContext.
|
|
12
|
+
|
|
13
|
+
The Token information includes:
|
|
14
|
+
- column_start: The starting column of the token.
|
|
15
|
+
- column_stop: The stopping column of the token.
|
|
16
|
+
- line_start: The starting line number of the token.
|
|
17
|
+
- line_stop: The stopping line number of the token.
|
|
18
|
+
|
|
19
|
+
The overall idea is to provide the information from which line and column,
|
|
20
|
+
and to which line and column, the text is referenced by the AST object, including children.
|
|
21
|
+
|
|
22
|
+
Important Note: the keys of the dict are the same as the class attributes of the AST Object.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
token (Union[CommonToken, ParserRuleContext]): The token or ParserRuleContext to extract
|
|
26
|
+
information from.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dict[str, int]: A dictionary containing the token information.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
if isinstance(token, ParserRuleContext):
|
|
33
|
+
return {
|
|
34
|
+
"column_start": token.start.column,
|
|
35
|
+
"column_stop": token.stop.column + len(token.stop.text),
|
|
36
|
+
"line_start": token.start.line,
|
|
37
|
+
"line_stop": token.stop.line,
|
|
38
|
+
}
|
|
39
|
+
line_start = token.line
|
|
40
|
+
line_stop = token.line
|
|
41
|
+
# For block comments, we need to add the lines inside the block, marked by \n, to the stop line.
|
|
42
|
+
# The ML_COMMENT does not take into account the final \n in its grammar.
|
|
43
|
+
if token.type == Lexer.ML_COMMENT:
|
|
44
|
+
line_stop = token.line + token.text.count("\n")
|
|
45
|
+
return {
|
|
46
|
+
"column_start": token.column,
|
|
47
|
+
"column_stop": token.column + len(token.text),
|
|
48
|
+
"line_start": line_start,
|
|
49
|
+
"line_stop": line_stop,
|
|
50
|
+
}
|
vtlengine/AST/ASTEncoders.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
3
|
from vtlengine import AST
|
|
4
|
+
from vtlengine.Model import Dataset
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class ComplexEncoder(json.JSONEncoder):
|
|
7
8
|
def default(self, obj):
|
|
8
9
|
if hasattr(obj, "toJSON"):
|
|
9
10
|
return obj.toJSON()
|
|
11
|
+
# Makes a circular reference error if we do not check for this
|
|
12
|
+
elif isinstance(obj, Dataset):
|
|
13
|
+
return "dataset"
|
|
10
14
|
else:
|
|
11
15
|
return json.__dict__
|
|
12
16
|
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from vtlengine import AST
|
|
6
|
+
from vtlengine.AST import Comment, DPRuleset, HRuleset, Operator, TimeAggregation
|
|
7
|
+
from vtlengine.AST.ASTTemplate import ASTTemplate
|
|
8
|
+
from vtlengine.AST.Grammar.lexer import Lexer
|
|
9
|
+
from vtlengine.AST.Grammar.tokens import (
|
|
10
|
+
AGGREGATE,
|
|
11
|
+
ATTRIBUTE,
|
|
12
|
+
CAST,
|
|
13
|
+
CHARSET_MATCH,
|
|
14
|
+
CHECK_DATAPOINT,
|
|
15
|
+
CHECK_HIERARCHY,
|
|
16
|
+
DATE_ADD,
|
|
17
|
+
DATEDIFF,
|
|
18
|
+
DROP,
|
|
19
|
+
FILL_TIME_SERIES,
|
|
20
|
+
FILTER,
|
|
21
|
+
HAVING,
|
|
22
|
+
HIERARCHY,
|
|
23
|
+
IDENTIFIER,
|
|
24
|
+
INSTR,
|
|
25
|
+
INTERSECT,
|
|
26
|
+
LOG,
|
|
27
|
+
MAX,
|
|
28
|
+
MEASURE,
|
|
29
|
+
MEMBERSHIP,
|
|
30
|
+
MIN,
|
|
31
|
+
MINUS,
|
|
32
|
+
MOD,
|
|
33
|
+
NOT,
|
|
34
|
+
NVL,
|
|
35
|
+
PLUS,
|
|
36
|
+
POWER,
|
|
37
|
+
RANDOM,
|
|
38
|
+
REPLACE,
|
|
39
|
+
ROUND,
|
|
40
|
+
SETDIFF,
|
|
41
|
+
SUBSTR,
|
|
42
|
+
SYMDIFF,
|
|
43
|
+
TIMESHIFT,
|
|
44
|
+
TRUNC,
|
|
45
|
+
UNION,
|
|
46
|
+
VIRAL_ATTRIBUTE,
|
|
47
|
+
)
|
|
48
|
+
from vtlengine.DataTypes import SCALAR_TYPES_CLASS_REVERSE
|
|
49
|
+
from vtlengine.Model import Component, Dataset
|
|
50
|
+
|
|
51
|
+
nl = "\n"
|
|
52
|
+
tab = "\t"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _handle_literal(value: Union[str, int, float, bool]):
|
|
56
|
+
if isinstance(value, str):
|
|
57
|
+
if '"' in value:
|
|
58
|
+
return value
|
|
59
|
+
return f'"{value}"'
|
|
60
|
+
elif isinstance(value, bool):
|
|
61
|
+
return "true" if value else "false"
|
|
62
|
+
elif isinstance(value, float):
|
|
63
|
+
decimal = str(value).split(".")[1]
|
|
64
|
+
if len(decimal) > 4:
|
|
65
|
+
return f"{value:f}".rstrip("0")
|
|
66
|
+
else:
|
|
67
|
+
return f"{value:g}"
|
|
68
|
+
return str(value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _format_dataset_eval(dataset: Dataset) -> str:
|
|
72
|
+
def __format_component(component: Component) -> str:
|
|
73
|
+
return (
|
|
74
|
+
f"\n\t\t\t{component.role.value.lower()}"
|
|
75
|
+
f"<{SCALAR_TYPES_CLASS_REVERSE[component.data_type].lower()}> "
|
|
76
|
+
f"{component.name}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return f"{{ {', '.join([__format_component(x) for x in dataset.components.values()])} \n\t\t}}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _format_reserved_word(value: str):
|
|
83
|
+
reserved_words = {x.replace("'", ""): x for x in Lexer.literalNames}
|
|
84
|
+
if value in reserved_words:
|
|
85
|
+
return reserved_words[value]
|
|
86
|
+
elif value[0] == "_":
|
|
87
|
+
return f"'{value}'"
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class ASTString(ASTTemplate):
|
|
93
|
+
vtl_script: str = ""
|
|
94
|
+
pretty: bool = False
|
|
95
|
+
is_first_assignment: bool = False
|
|
96
|
+
is_from_agg: bool = False # Handler to write grouping at aggr level
|
|
97
|
+
|
|
98
|
+
def render(self, ast: AST.AST) -> str:
|
|
99
|
+
self.vtl_script = ""
|
|
100
|
+
result = self.visit(ast)
|
|
101
|
+
if result:
|
|
102
|
+
self.vtl_script += result
|
|
103
|
+
return self.vtl_script
|
|
104
|
+
|
|
105
|
+
def visit_Start(self, node: AST.Start) -> Any:
|
|
106
|
+
transformations = [
|
|
107
|
+
x for x in node.children if not isinstance(x, (HRuleset, DPRuleset, Operator, Comment))
|
|
108
|
+
]
|
|
109
|
+
for child in node.children:
|
|
110
|
+
if child in transformations:
|
|
111
|
+
self.is_first_assignment = True
|
|
112
|
+
self.visit(child)
|
|
113
|
+
self.vtl_script += "\n"
|
|
114
|
+
|
|
115
|
+
# ---------------------- Rulesets ----------------------
|
|
116
|
+
def visit_HRuleset(self, node: AST.HRuleset) -> None:
|
|
117
|
+
signature = f"{node.signature_type} rule {node.element.value}"
|
|
118
|
+
if self.pretty:
|
|
119
|
+
self.vtl_script += f"define hierarchical ruleset {node.name}({signature}) is{nl}"
|
|
120
|
+
for i, rule in enumerate(node.rules):
|
|
121
|
+
self.vtl_script += f"{tab}{self.visit(rule)}{nl}"
|
|
122
|
+
if rule.erCode:
|
|
123
|
+
self.vtl_script += f"{tab}errorcode {_handle_literal(rule.erCode)}{nl}"
|
|
124
|
+
if rule.erLevel:
|
|
125
|
+
self.vtl_script += f"{tab}errorlevel {rule.erLevel}"
|
|
126
|
+
if i != len(node.rules) - 1:
|
|
127
|
+
self.vtl_script += f";{nl}"
|
|
128
|
+
self.vtl_script += nl
|
|
129
|
+
self.vtl_script += f"end hierarchical ruleset;{nl}"
|
|
130
|
+
else:
|
|
131
|
+
rules_strs = []
|
|
132
|
+
for rule in node.rules:
|
|
133
|
+
rule_str = self.visit(rule)
|
|
134
|
+
if rule.erCode:
|
|
135
|
+
rule_str += f" errorcode {_handle_literal(rule.erCode)}"
|
|
136
|
+
if rule.erLevel:
|
|
137
|
+
rule_str += f" errorlevel {rule.erLevel}"
|
|
138
|
+
rules_strs.append(rule_str)
|
|
139
|
+
rules_sep = "; " if len(rules_strs) > 1 else ""
|
|
140
|
+
rules = rules_sep.join(rules_strs)
|
|
141
|
+
self.vtl_script += (
|
|
142
|
+
f"define hierarchical ruleset {node.name} ({signature}) is {rules} "
|
|
143
|
+
f"end hierarchical ruleset;"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def visit_HRule(self, node: AST.HRule) -> str:
|
|
147
|
+
vtl_script = ""
|
|
148
|
+
if node.name is not None:
|
|
149
|
+
vtl_script += f"{node.name}: "
|
|
150
|
+
vtl_script += f"{self.visit(node.rule)}"
|
|
151
|
+
return vtl_script
|
|
152
|
+
|
|
153
|
+
def visit_HRBinOp(self, node: AST.HRBinOp) -> str:
|
|
154
|
+
if node.op == "when":
|
|
155
|
+
if self.pretty:
|
|
156
|
+
return (
|
|
157
|
+
f"{tab * 3}when{nl}"
|
|
158
|
+
f"{tab * 4}{self.visit(node.left)}{nl}"
|
|
159
|
+
f"{tab * 3}then{nl}"
|
|
160
|
+
f"{tab * 4}{self.visit(node.right)}"
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
return f"{node.op} {self.visit(node.left)} then {self.visit(node.right)}"
|
|
164
|
+
return f"{self.visit(node.left)} {node.op} {self.visit(node.right)}"
|
|
165
|
+
|
|
166
|
+
def visit_HRUnOp(self, node: AST.HRUnOp) -> str:
|
|
167
|
+
return f"{node.op} {self.visit(node.operand)}"
|
|
168
|
+
|
|
169
|
+
def visit_DefIdentifier(self, node: AST.DefIdentifier) -> str:
|
|
170
|
+
return _format_reserved_word(node.value)
|
|
171
|
+
|
|
172
|
+
def visit_DPRule(self, node: AST.DPRule) -> str:
|
|
173
|
+
if self.pretty:
|
|
174
|
+
lines = []
|
|
175
|
+
if node.name is not None:
|
|
176
|
+
lines.append(f"{tab}{node.name}: ")
|
|
177
|
+
lines.append(self.visit(node.rule))
|
|
178
|
+
if node.erCode is not None:
|
|
179
|
+
lines.append(f"{tab * 3}errorcode {_handle_literal(node.erCode)}")
|
|
180
|
+
if node.erLevel is not None:
|
|
181
|
+
lines.append(f"{tab * 3}errorlevel {node.erLevel}")
|
|
182
|
+
return nl.join(lines)
|
|
183
|
+
else:
|
|
184
|
+
vtl_script = ""
|
|
185
|
+
if node.name is not None:
|
|
186
|
+
vtl_script += f"{node.name}: "
|
|
187
|
+
vtl_script += f"{self.visit(node.rule)}"
|
|
188
|
+
if node.erCode is not None:
|
|
189
|
+
vtl_script += f" errorcode {_handle_literal(node.erCode)}"
|
|
190
|
+
if node.erLevel is not None:
|
|
191
|
+
vtl_script += f" errorlevel {node.erLevel}"
|
|
192
|
+
return vtl_script
|
|
193
|
+
|
|
194
|
+
def visit_DPRIdentifier(self, node: AST.DPRIdentifier) -> str:
|
|
195
|
+
vtl_script = f"{node.value}"
|
|
196
|
+
if node.alias is not None:
|
|
197
|
+
vtl_script += f" as {node.alias}"
|
|
198
|
+
return vtl_script
|
|
199
|
+
|
|
200
|
+
def visit_DPRuleset(self, node: AST.DPRuleset) -> None:
|
|
201
|
+
rules_sep = "; " if len(node.rules) > 1 else ""
|
|
202
|
+
signature_sep = ", " if len(node.params) > 1 else ""
|
|
203
|
+
signature = (
|
|
204
|
+
f"{node.signature_type} {signature_sep.join([self.visit(x) for x in node.params])}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if self.pretty:
|
|
208
|
+
self.vtl_script += f"define datapoint ruleset {node.name}({signature}) is {nl}"
|
|
209
|
+
rules = ""
|
|
210
|
+
for i, rule in enumerate(node.rules):
|
|
211
|
+
rules += f"\t{self.visit(rule)}"
|
|
212
|
+
if i != len(node.rules) - 1:
|
|
213
|
+
rules += f";{nl * 2}"
|
|
214
|
+
else:
|
|
215
|
+
rules += f"{nl}"
|
|
216
|
+
self.vtl_script += rules
|
|
217
|
+
self.vtl_script += f"end datapoint ruleset;{nl}"
|
|
218
|
+
else:
|
|
219
|
+
rules = rules_sep.join([self.visit(x) for x in node.rules])
|
|
220
|
+
self.vtl_script += (
|
|
221
|
+
f"define datapoint ruleset {node.name} "
|
|
222
|
+
f"({signature}) is {rules} end datapoint ruleset;"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# ---------------------- User Defined Operators ----------------------
|
|
226
|
+
|
|
227
|
+
def visit_Argument(self, node: AST.Argument) -> str:
|
|
228
|
+
default = f" default {self.visit(node.default)}" if node.default is not None else ""
|
|
229
|
+
|
|
230
|
+
if isinstance(node.type_, Dataset):
|
|
231
|
+
argument_type = "dataset"
|
|
232
|
+
elif isinstance(node.type_, Component):
|
|
233
|
+
argument_type = "component"
|
|
234
|
+
else:
|
|
235
|
+
argument_type = node.type_.__name__.lower()
|
|
236
|
+
|
|
237
|
+
name = _format_reserved_word(node.name)
|
|
238
|
+
|
|
239
|
+
return f"{name} {argument_type}{default}"
|
|
240
|
+
|
|
241
|
+
def visit_Operator(self, node: AST.Operator) -> None:
|
|
242
|
+
signature_sep = ", " if len(node.parameters) > 1 else ""
|
|
243
|
+
signature = signature_sep.join([self.visit(x) for x in node.parameters])
|
|
244
|
+
if self.pretty:
|
|
245
|
+
self.vtl_script += f"define operator {node.op}({signature}){nl}"
|
|
246
|
+
self.vtl_script += f"\treturns {node.output_type.lower()} is{nl}"
|
|
247
|
+
expression = self.visit(node.expression)
|
|
248
|
+
if "(" in expression:
|
|
249
|
+
expression = expression.replace("(", f"({nl}{tab * 2}")
|
|
250
|
+
expression = expression.replace(")", f"{nl}{tab * 2})")
|
|
251
|
+
|
|
252
|
+
self.vtl_script += f"{tab * 2}{expression}{nl}"
|
|
253
|
+
self.vtl_script += f"end operator;{nl}"
|
|
254
|
+
else:
|
|
255
|
+
body = f"returns {node.output_type.lower()} is {self.visit(node.expression)}"
|
|
256
|
+
self.vtl_script += f"define operator {node.op}({signature}) {body} end operator;"
|
|
257
|
+
|
|
258
|
+
# ---------------------- Basic Operators ----------------------
|
|
259
|
+
def visit_Assignment(self, node: AST.Assignment) -> Optional[str]:
|
|
260
|
+
return_element = not copy.deepcopy(self.is_first_assignment)
|
|
261
|
+
is_first = self.is_first_assignment
|
|
262
|
+
if is_first:
|
|
263
|
+
self.is_first_assignment = False
|
|
264
|
+
if self.pretty:
|
|
265
|
+
if is_first:
|
|
266
|
+
expression = f"{self.visit(node.left)} {node.op}{nl}{tab}{self.visit(node.right)}"
|
|
267
|
+
else:
|
|
268
|
+
expression = f"{self.visit(node.left)} {node.op} {self.visit(node.right)}"
|
|
269
|
+
else:
|
|
270
|
+
expression = f"{self.visit(node.left)} {node.op} {self.visit(node.right)}"
|
|
271
|
+
if return_element:
|
|
272
|
+
return expression
|
|
273
|
+
self.vtl_script += f"{expression};"
|
|
274
|
+
|
|
275
|
+
def visit_PersistentAssignment(self, node: AST.PersistentAssignment) -> Optional[str]:
|
|
276
|
+
return self.visit_Assignment(node)
|
|
277
|
+
|
|
278
|
+
def visit_BinOp(self, node: AST.BinOp) -> str:
|
|
279
|
+
if node.op in [NVL, LOG, MOD, POWER, RANDOM, TIMESHIFT, DATEDIFF, CHARSET_MATCH]:
|
|
280
|
+
return f"{node.op}({self.visit(node.left)}, {self.visit(node.right)})"
|
|
281
|
+
elif node.op == MEMBERSHIP:
|
|
282
|
+
return f"{self.visit(node.left)}{node.op}{self.visit(node.right)}"
|
|
283
|
+
if self.pretty:
|
|
284
|
+
return f"{self.visit(node.left)} {node.op} {self.visit(node.right)}"
|
|
285
|
+
|
|
286
|
+
return f"{self.visit(node.left)} {node.op} {self.visit(node.right)}"
|
|
287
|
+
|
|
288
|
+
def visit_UnaryOp(self, node: AST.UnaryOp) -> str:
|
|
289
|
+
if node.op in [PLUS, MINUS]:
|
|
290
|
+
return f"{node.op}{self.visit(node.operand)}"
|
|
291
|
+
elif node.op in [IDENTIFIER, ATTRIBUTE, VIRAL_ATTRIBUTE, NOT]:
|
|
292
|
+
return f"{node.op} {self.visit(node.operand)}"
|
|
293
|
+
elif node.op == MEASURE:
|
|
294
|
+
return self.visit(node.operand)
|
|
295
|
+
|
|
296
|
+
return f"{node.op}({self.visit(node.operand)})"
|
|
297
|
+
|
|
298
|
+
def visit_MulOp(self, node: AST.MulOp) -> str:
|
|
299
|
+
sep = ", " if len(node.children) > 1 else ""
|
|
300
|
+
body = sep.join([self.visit(x) for x in node.children])
|
|
301
|
+
if self.pretty:
|
|
302
|
+
return f"{node.op}({body})"
|
|
303
|
+
return f"{node.op}({body})"
|
|
304
|
+
|
|
305
|
+
def visit_ParamOp(self, node: AST.ParamOp) -> str:
|
|
306
|
+
if node.op == HAVING:
|
|
307
|
+
return f"{node.op} {self.visit(node.params)}"
|
|
308
|
+
elif node.op in [SUBSTR, INSTR, REPLACE, ROUND, TRUNC, UNION, SETDIFF, SYMDIFF, INTERSECT]:
|
|
309
|
+
params_sep = ", " if len(node.params) > 1 else ""
|
|
310
|
+
return (
|
|
311
|
+
f"{node.op}({self.visit(node.children[0])}, "
|
|
312
|
+
f"{params_sep.join([self.visit(x) for x in node.params])})"
|
|
313
|
+
)
|
|
314
|
+
elif node.op in (CHECK_HIERARCHY, HIERARCHY):
|
|
315
|
+
operand = self.visit(node.children[0])
|
|
316
|
+
component_name = self.visit(node.children[1])
|
|
317
|
+
rule_name = self.visit(node.children[2])
|
|
318
|
+
param_mode_value = node.params[0].value
|
|
319
|
+
param_input_value = node.params[1].value
|
|
320
|
+
param_output_value = node.params[2].value
|
|
321
|
+
|
|
322
|
+
default_value_input = "dataset" if node.op == CHECK_HIERARCHY else "rule"
|
|
323
|
+
default_value_output = "invalid" if node.op == CHECK_HIERARCHY else "computed"
|
|
324
|
+
|
|
325
|
+
param_mode = f" {param_mode_value}" if param_mode_value != "non_null" else ""
|
|
326
|
+
param_input = (
|
|
327
|
+
f" {param_input_value}" if param_input_value != default_value_input else ""
|
|
328
|
+
)
|
|
329
|
+
param_output = (
|
|
330
|
+
f" {param_output_value}" if param_output_value != default_value_output else ""
|
|
331
|
+
)
|
|
332
|
+
if self.pretty:
|
|
333
|
+
return (
|
|
334
|
+
f"{node.op}({nl}{tab * 2}{operand},{nl}{tab * 2}{rule_name}{nl}{tab * 2}rule "
|
|
335
|
+
f"{component_name}"
|
|
336
|
+
f"{param_mode}{param_input}{param_output})"
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
return (
|
|
340
|
+
f"{node.op}({operand}, {rule_name} rule {component_name}"
|
|
341
|
+
f"{param_mode}{param_input}{param_output})"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
elif node.op == CHECK_DATAPOINT:
|
|
345
|
+
operand = self.visit(node.children[0])
|
|
346
|
+
rule_name = node.children[1]
|
|
347
|
+
output = ""
|
|
348
|
+
if len(node.params) == 1 and node.params[0] != "invalid":
|
|
349
|
+
output = f"{node.params[0]}"
|
|
350
|
+
if self.pretty:
|
|
351
|
+
return f"{node.op}({nl}{tab}{operand},{nl}{tab}{rule_name}{nl}{tab}{output}{nl})"
|
|
352
|
+
else:
|
|
353
|
+
return f"{node.op}({operand}, {rule_name}{output})"
|
|
354
|
+
elif node.op == CAST:
|
|
355
|
+
operand = self.visit(node.children[0])
|
|
356
|
+
data_type = SCALAR_TYPES_CLASS_REVERSE[node.children[1]].lower()
|
|
357
|
+
mask = ""
|
|
358
|
+
if len(node.params) == 1:
|
|
359
|
+
mask = f", {_handle_literal(self.visit(node.params[0]))}"
|
|
360
|
+
if self.pretty:
|
|
361
|
+
return f"{node.op}({operand}, {data_type}{mask})"
|
|
362
|
+
else:
|
|
363
|
+
return f"{node.op}({operand}, {data_type}{mask})"
|
|
364
|
+
elif node.op == FILL_TIME_SERIES:
|
|
365
|
+
operand = self.visit(node.children[0])
|
|
366
|
+
param = node.params[0].value if node.params else "all"
|
|
367
|
+
if self.pretty:
|
|
368
|
+
return f"{node.op}({operand},{param})"
|
|
369
|
+
else:
|
|
370
|
+
return f"{node.op}({operand}, {param})"
|
|
371
|
+
elif node.op == DATE_ADD:
|
|
372
|
+
operand = self.visit(node.children[0])
|
|
373
|
+
shift_number = self.visit(node.params[0])
|
|
374
|
+
period_indicator = self.visit(node.params[1])
|
|
375
|
+
if self.pretty:
|
|
376
|
+
return (
|
|
377
|
+
f"{node.op}({nl}{tab * 2}{operand},{nl}{tab * 2}{shift_number},"
|
|
378
|
+
f"{nl}{tab * 2}"
|
|
379
|
+
f"{period_indicator})"
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
return f"{node.op}({operand}, {shift_number}, {period_indicator})"
|
|
383
|
+
return ""
|
|
384
|
+
|
|
385
|
+
# ---------------------- Individual operators ----------------------
|
|
386
|
+
|
|
387
|
+
def _handle_grouping_having(self, node: AST) -> Tuple[str, str]:
|
|
388
|
+
if self.is_from_agg:
|
|
389
|
+
return "", ""
|
|
390
|
+
grouping = ""
|
|
391
|
+
if node.grouping is not None:
|
|
392
|
+
grouping_sep = ", " if len(node.grouping) > 1 else ""
|
|
393
|
+
grouping_values = []
|
|
394
|
+
for grouping_value in node.grouping:
|
|
395
|
+
if isinstance(grouping_value, TimeAggregation):
|
|
396
|
+
grouping_values.append(self.visit(grouping_value))
|
|
397
|
+
else:
|
|
398
|
+
grouping_values.append(_format_reserved_word(grouping_value.value))
|
|
399
|
+
grouping = f" {node.grouping_op} {grouping_sep.join(grouping_values)}"
|
|
400
|
+
having = f" {self.visit(node.having_clause)}" if node.having_clause is not None else ""
|
|
401
|
+
return grouping, having
|
|
402
|
+
|
|
403
|
+
def visit_Aggregation(self, node: AST.Aggregation) -> str:
|
|
404
|
+
grouping, having = self._handle_grouping_having(node)
|
|
405
|
+
if self.pretty and node.op not in (MAX, MIN):
|
|
406
|
+
operand = self.visit(node.operand)
|
|
407
|
+
return f"{node.op}({nl}{tab * 2}{operand}{grouping}{having}{nl}{tab * 2})"
|
|
408
|
+
return f"{node.op}({self.visit(node.operand)}{grouping}{having})"
|
|
409
|
+
|
|
410
|
+
def visit_Analytic(self, node: AST.Analytic) -> str:
|
|
411
|
+
operand = "" if node.operand is None else self.visit(node.operand)
|
|
412
|
+
partition = ""
|
|
413
|
+
if node.partition_by:
|
|
414
|
+
partition_sep = ", " if len(node.partition_by) > 1 else ""
|
|
415
|
+
partition_values = [_format_reserved_word(x) for x in node.partition_by]
|
|
416
|
+
partition = f"partition by {partition_sep.join(partition_values)}"
|
|
417
|
+
order = ""
|
|
418
|
+
if node.order_by:
|
|
419
|
+
order_sep = ", " if len(node.order_by) > 1 else ""
|
|
420
|
+
order = f" order by {order_sep.join([self.visit(x) for x in node.order_by])}"
|
|
421
|
+
window = f" {self.visit(node.window)}" if node.window is not None else ""
|
|
422
|
+
params = ""
|
|
423
|
+
if node.params:
|
|
424
|
+
params = "" if len(node.params) == 0 else f", {int(node.params[0])}"
|
|
425
|
+
if self.pretty:
|
|
426
|
+
result = (
|
|
427
|
+
f"{node.op}({nl}{tab * 3}{operand}{params} over({partition}{order} {window})"
|
|
428
|
+
f"{nl}{tab * 2})"
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
result = f"{node.op}({operand}{params} over ({partition}{order}{window}))"
|
|
432
|
+
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
def visit_Case(self, node: AST.Case) -> str:
|
|
436
|
+
if self.pretty:
|
|
437
|
+
else_str = f"{nl}{tab * 2}else{nl}{tab * 3}{self.visit(node.elseOp)}"
|
|
438
|
+
body_sep = " " if len(node.cases) > 1 else ""
|
|
439
|
+
body = body_sep.join([self.visit(x) for x in node.cases])
|
|
440
|
+
return f"case {body} {else_str}"
|
|
441
|
+
else:
|
|
442
|
+
else_str = f"else {self.visit(node.elseOp)}"
|
|
443
|
+
body_sep = " " if len(node.cases) > 1 else ""
|
|
444
|
+
body = body_sep.join([self.visit(x) for x in node.cases])
|
|
445
|
+
return f"case {body} {else_str}"
|
|
446
|
+
|
|
447
|
+
def visit_CaseObj(self, node: AST.CaseObj) -> str:
|
|
448
|
+
if self.pretty:
|
|
449
|
+
return (
|
|
450
|
+
f"{nl}{tab * 2}when{nl}{tab * 3}{self.visit(node.condition)}{nl}{tab * 2}then"
|
|
451
|
+
f"{nl}{tab * 3}{self.visit(node.thenOp)}"
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
return f"when {self.visit(node.condition)} then {self.visit(node.thenOp)}"
|
|
455
|
+
|
|
456
|
+
def visit_EvalOp(self, node: AST.EvalOp) -> str:
|
|
457
|
+
operand_sep = ", " if len(node.operands) > 1 else ""
|
|
458
|
+
if self.pretty:
|
|
459
|
+
operands = operand_sep.join([self.visit(x) for x in node.operands])
|
|
460
|
+
ext_routine = f"{nl}{tab * 2}{node.name}({operands})"
|
|
461
|
+
language = f"{nl}{tab * 2}language {_handle_literal(node.language)}{nl}"
|
|
462
|
+
output = f"{tab * 2}returns dataset {_format_dataset_eval(node.output)}"
|
|
463
|
+
return f"eval({ext_routine} {language} {output})"
|
|
464
|
+
else:
|
|
465
|
+
operands = operand_sep.join([self.visit(x) for x in node.operands])
|
|
466
|
+
ext_routine = f"{node.name}({operands})"
|
|
467
|
+
language = f"language {_handle_literal(node.language)}"
|
|
468
|
+
output = f"returns dataset {_format_dataset_eval(node.output)}"
|
|
469
|
+
return f"eval({ext_routine} {language} {output})"
|
|
470
|
+
|
|
471
|
+
def visit_If(self, node: AST.If) -> str:
|
|
472
|
+
if self.pretty:
|
|
473
|
+
else_str = (
|
|
474
|
+
f"else{nl}{tab * 5}{self.visit(node.elseOp)}" if node.elseOp is not None else ""
|
|
475
|
+
)
|
|
476
|
+
return (
|
|
477
|
+
f"{nl}{tab * 4}if {nl}{tab * 5}{self.visit(node.condition)} "
|
|
478
|
+
f"{nl}{tab * 4}then {nl}{tab * 5}{self.visit(node.thenOp)}{nl}{tab * 4}{else_str}"
|
|
479
|
+
)
|
|
480
|
+
else:
|
|
481
|
+
else_str = f"else {self.visit(node.elseOp)}" if node.elseOp is not None else ""
|
|
482
|
+
return f"if {self.visit(node.condition)} then {self.visit(node.thenOp)} {else_str}"
|
|
483
|
+
|
|
484
|
+
def visit_JoinOp(self, node: AST.JoinOp) -> str:
|
|
485
|
+
if self.pretty:
|
|
486
|
+
sep = f",{nl}{tab * 2}" if len(node.clauses) > 1 else ""
|
|
487
|
+
clauses = sep.join([self.visit(x) for x in node.clauses])
|
|
488
|
+
using = ""
|
|
489
|
+
if node.using is not None:
|
|
490
|
+
using_sep = ", " if len(node.using) > 1 else ""
|
|
491
|
+
using_values = [_format_reserved_word(x) for x in node.using]
|
|
492
|
+
using = f"using {using_sep.join(using_values)}"
|
|
493
|
+
return f"{node.op}({nl}{tab * 2}{clauses}{nl}{tab * 2}{using})"
|
|
494
|
+
else:
|
|
495
|
+
sep = ", " if len(node.clauses) > 1 else ""
|
|
496
|
+
clauses = sep.join([self.visit(x) for x in node.clauses])
|
|
497
|
+
using = ""
|
|
498
|
+
if node.using is not None:
|
|
499
|
+
using_sep = ", " if len(node.using) > 1 else ""
|
|
500
|
+
using_values = [_format_reserved_word(x) for x in node.using]
|
|
501
|
+
using = f" using {using_sep.join(using_values)}"
|
|
502
|
+
return f"{node.op}({clauses}{using})"
|
|
503
|
+
|
|
504
|
+
def visit_ParFunction(self, node: AST.ParFunction) -> str:
|
|
505
|
+
return f"({self.visit(node.operand)})"
|
|
506
|
+
|
|
507
|
+
def visit_RegularAggregation(self, node: AST.RegularAggregation) -> str:
|
|
508
|
+
child_sep = ", " if len(node.children) > 1 else ""
|
|
509
|
+
if node.op == AGGREGATE:
|
|
510
|
+
self.is_from_agg = True
|
|
511
|
+
body = child_sep.join([self.visit(x) for x in node.children])
|
|
512
|
+
self.is_from_agg = False
|
|
513
|
+
grouping, having = self._handle_grouping_having(node.children[0].right)
|
|
514
|
+
if self.pretty:
|
|
515
|
+
body = f"{nl}{tab * 3}{body}{nl}{tab * 3}{grouping}{having}{nl}{tab * 2}"
|
|
516
|
+
else:
|
|
517
|
+
body = f"{body}{grouping}{having}"
|
|
518
|
+
elif node.op == DROP and self.pretty:
|
|
519
|
+
drop_sep = f",{nl}{tab * 3}" if len(node.children) > 1 else ""
|
|
520
|
+
body = f"{drop_sep.join([self.visit(x) for x in node.children])}{nl}{tab * 2}"
|
|
521
|
+
elif node.op == FILTER and self.pretty:
|
|
522
|
+
condition = self.visit(node.children[0])
|
|
523
|
+
if " and " in condition or " or " in condition:
|
|
524
|
+
for op in (" and ", " or "):
|
|
525
|
+
condition = condition.replace(op, f"{op}{nl}{tab * 5}")
|
|
526
|
+
body = f"{nl}{tab * 4}{condition}{nl}{tab * 2}"
|
|
527
|
+
else:
|
|
528
|
+
body = child_sep.join([self.visit(x) for x in node.children])
|
|
529
|
+
if isinstance(node.dataset, AST.JoinOp):
|
|
530
|
+
dataset = self.visit(node.dataset)
|
|
531
|
+
if self.pretty:
|
|
532
|
+
return f"{dataset[:-1]} {(node.op)} {body}{nl}{tab})"
|
|
533
|
+
else:
|
|
534
|
+
return f"{dataset[:-1]} {node.op} {body})"
|
|
535
|
+
else:
|
|
536
|
+
dataset = self.visit(node.dataset)
|
|
537
|
+
if self.pretty:
|
|
538
|
+
return f"{dataset}{nl}{tab * 2}[{node.op} {body}]"
|
|
539
|
+
else:
|
|
540
|
+
return f"{dataset}[{node.op} {body}]"
|
|
541
|
+
|
|
542
|
+
def visit_RenameNode(self, node: AST.RenameNode) -> str:
|
|
543
|
+
return f"{node.old_name} to {node.new_name}"
|
|
544
|
+
|
|
545
|
+
def visit_TimeAggregation(self, node: AST.TimeAggregation) -> str:
|
|
546
|
+
operand = self.visit(node.operand)
|
|
547
|
+
period_from = "_" if node.period_from is None else _handle_literal(node.period_from)
|
|
548
|
+
period_to = _handle_literal(node.period_to)
|
|
549
|
+
if self.pretty:
|
|
550
|
+
return f"{node.op}({period_to}, {period_from}, {operand})"
|
|
551
|
+
else:
|
|
552
|
+
return f"{node.op}({period_to}, {period_from}, {operand})"
|
|
553
|
+
|
|
554
|
+
def visit_UDOCall(self, node: AST.UDOCall) -> str:
|
|
555
|
+
params_sep = ", " if len(node.params) > 1 else ""
|
|
556
|
+
params = params_sep.join([self.visit(x) for x in node.params])
|
|
557
|
+
return f"{node.op}({params})"
|
|
558
|
+
|
|
559
|
+
def visit_Validation(self, node: AST.Validation) -> str:
|
|
560
|
+
operand = self.visit(node.validation)
|
|
561
|
+
imbalance = f" imbalance {self.visit(node.imbalance)}" if node.imbalance is not None else ""
|
|
562
|
+
error_code = (
|
|
563
|
+
f" errorcode {_handle_literal(node.error_code)}" if node.error_code is not None else ""
|
|
564
|
+
)
|
|
565
|
+
error_level = f" errorlevel {node.error_level}" if node.error_level is not None else ""
|
|
566
|
+
invalid = " invalid" if node.invalid else " all"
|
|
567
|
+
return f"{node.op}({operand}{error_code}{error_level}{imbalance}{invalid})"
|
|
568
|
+
|
|
569
|
+
# ---------------------- Constants and IDs ----------------------
|
|
570
|
+
|
|
571
|
+
def visit_VarID(self, node: AST.VarID) -> str:
|
|
572
|
+
return _format_reserved_word(node.value)
|
|
573
|
+
|
|
574
|
+
def visit_Identifier(self, node: AST.Identifier) -> Any:
|
|
575
|
+
return _format_reserved_word(node.value)
|
|
576
|
+
|
|
577
|
+
def visit_Constant(self, node: AST.Constant) -> str:
|
|
578
|
+
if node.value is None:
|
|
579
|
+
return "null"
|
|
580
|
+
return _handle_literal(node.value)
|
|
581
|
+
|
|
582
|
+
def visit_ParamConstant(self, node: AST.ParamConstant) -> Any:
|
|
583
|
+
if node.value is None:
|
|
584
|
+
return "null"
|
|
585
|
+
return node.value
|
|
586
|
+
|
|
587
|
+
def visit_Collection(self, node: AST.Collection) -> str:
|
|
588
|
+
if node.kind == "ValueDomain":
|
|
589
|
+
return node.name
|
|
590
|
+
sep = ", " if len(node.children) > 1 else ""
|
|
591
|
+
return f"{{{sep.join([self.visit(x) for x in node.children])}}}"
|
|
592
|
+
|
|
593
|
+
def visit_Windowing(self, node: AST.Windowing) -> str:
|
|
594
|
+
if (
|
|
595
|
+
node.type_ == "data"
|
|
596
|
+
and node.start == -1
|
|
597
|
+
and node.start_mode == "preceding"
|
|
598
|
+
and node.stop == 0
|
|
599
|
+
and node.stop_mode == "current"
|
|
600
|
+
):
|
|
601
|
+
return ""
|
|
602
|
+
if node.start == -1:
|
|
603
|
+
start = f"unbounded {node.start_mode}"
|
|
604
|
+
elif node.start_mode == "current":
|
|
605
|
+
start = "current data point"
|
|
606
|
+
else:
|
|
607
|
+
start = f"{node.start} {node.start_mode}"
|
|
608
|
+
stop = f"{node.stop} {node.stop_mode}"
|
|
609
|
+
if node.stop_mode == "current":
|
|
610
|
+
stop = "current data point"
|
|
611
|
+
mode = "data points" if node.type_ == "data" else "range"
|
|
612
|
+
return f"{mode} between {start} and {stop}"
|
|
613
|
+
|
|
614
|
+
def visit_OrderBy(self, node: AST.OrderBy) -> str:
|
|
615
|
+
if node.order == "asc":
|
|
616
|
+
return f"{_format_reserved_word(node.component)}"
|
|
617
|
+
return f"{_format_reserved_word(node.component)} {node.order}"
|
|
618
|
+
|
|
619
|
+
def visit_Comment(self, node: AST.Comment) -> None:
|
|
620
|
+
value = copy.copy(node.value)
|
|
621
|
+
value = value[:-1] if value[-1] == "\n" else value
|
|
622
|
+
self.vtl_script += value
|