sqlleaf 0.0.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.
- sqlleaf/__init__.py +4 -0
- sqlleaf/dialects/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/p_classes.py +275 -0
- sqlleaf/dialects/plpgsql/p_dialect.py +148 -0
- sqlleaf/dialects/plpgsql/p_generator.py +538 -0
- sqlleaf/dialects/plpgsql/p_parser.py +1244 -0
- sqlleaf/dialects/plpgsql/p_tokenizer.py +41 -0
- sqlleaf/exception.py +67 -0
- sqlleaf/lineage.py +326 -0
- sqlleaf/mappings.py +376 -0
- sqlleaf/models/__init__.py +0 -0
- sqlleaf/models/context.py +153 -0
- sqlleaf/models/hook.py +9 -0
- sqlleaf/models/node/__init__.py +27 -0
- sqlleaf/models/node/base.py +148 -0
- sqlleaf/models/node/column.py +345 -0
- sqlleaf/models/node/dynamodb.py +39 -0
- sqlleaf/models/node/function.py +33 -0
- sqlleaf/models/node/interval.py +21 -0
- sqlleaf/models/node/json.py +57 -0
- sqlleaf/models/node/literal.py +17 -0
- sqlleaf/models/node/null.py +14 -0
- sqlleaf/models/node/pivot.py +34 -0
- sqlleaf/models/node/program.py +27 -0
- sqlleaf/models/node/sequence.py +18 -0
- sqlleaf/models/node/star.py +11 -0
- sqlleaf/models/node/stream.py +14 -0
- sqlleaf/models/node/user_defined_function.py +33 -0
- sqlleaf/models/node/var.py +7 -0
- sqlleaf/models/node/variable.py +8 -0
- sqlleaf/models/node/window.py +22 -0
- sqlleaf/models/query/__init__.py +33 -0
- sqlleaf/models/query/base.py +248 -0
- sqlleaf/models/query/call.py +111 -0
- sqlleaf/models/query/copy.py +144 -0
- sqlleaf/models/query/ctas.py +61 -0
- sqlleaf/models/query/database.py +48 -0
- sqlleaf/models/query/delete.py +26 -0
- sqlleaf/models/query/execute.py +66 -0
- sqlleaf/models/query/holder.py +49 -0
- sqlleaf/models/query/insert.py +69 -0
- sqlleaf/models/query/merge.py +29 -0
- sqlleaf/models/query/multitable_insert.py +42 -0
- sqlleaf/models/query/prepare.py +61 -0
- sqlleaf/models/query/procedure.py +72 -0
- sqlleaf/models/query/put.py +38 -0
- sqlleaf/models/query/replace.py +32 -0
- sqlleaf/models/query/schema.py +47 -0
- sqlleaf/models/query/select.py +27 -0
- sqlleaf/models/query/sequence.py +33 -0
- sqlleaf/models/query/set.py +74 -0
- sqlleaf/models/query/stage.py +78 -0
- sqlleaf/models/query/table.py +220 -0
- sqlleaf/models/query/trigger.py +42 -0
- sqlleaf/models/query/type.py +42 -0
- sqlleaf/models/query/unload.py +64 -0
- sqlleaf/models/query/update.py +45 -0
- sqlleaf/models/query/user_defined_function.py +297 -0
- sqlleaf/models/query/values.py +20 -0
- sqlleaf/models/query/view.py +41 -0
- sqlleaf/path.py +194 -0
- sqlleaf/processors/__init__.py +0 -0
- sqlleaf/processors/collector/__init__.py +0 -0
- sqlleaf/processors/collector/collector.py +827 -0
- sqlleaf/processors/collector/substitute.py +396 -0
- sqlleaf/processors/generator/__init__.py +0 -0
- sqlleaf/processors/generator/dialects/__init__.py +5 -0
- sqlleaf/processors/generator/dialects/athena.py +45 -0
- sqlleaf/processors/generator/dialects/base.py +572 -0
- sqlleaf/processors/generator/dialects/mysql.py +19 -0
- sqlleaf/processors/generator/dialects/postgres.py +182 -0
- sqlleaf/processors/generator/dialects/redshift.py +192 -0
- sqlleaf/processors/generator/dialects/snowflake.py +90 -0
- sqlleaf/processors/generator/generator.py +495 -0
- sqlleaf/processors/transformer/__init__.py +31 -0
- sqlleaf/processors/transformer/base.py +899 -0
- sqlleaf/processors/transformer/call.py +9 -0
- sqlleaf/processors/transformer/copy.py +63 -0
- sqlleaf/processors/transformer/ctas.py +39 -0
- sqlleaf/processors/transformer/delete.py +14 -0
- sqlleaf/processors/transformer/execute.py +9 -0
- sqlleaf/processors/transformer/expressions/__init__.py +11 -0
- sqlleaf/processors/transformer/expressions/functional_notation.py +22 -0
- sqlleaf/processors/transformer/expressions/row.py +451 -0
- sqlleaf/processors/transformer/expressions/values.py +318 -0
- sqlleaf/processors/transformer/insert.py +125 -0
- sqlleaf/processors/transformer/merge.py +14 -0
- sqlleaf/processors/transformer/multitable_insert.py +14 -0
- sqlleaf/processors/transformer/replace.py +23 -0
- sqlleaf/processors/transformer/transformer.py +141 -0
- sqlleaf/processors/transformer/udf.py +235 -0
- sqlleaf/processors/transformer/unload.py +30 -0
- sqlleaf/processors/transformer/update.py +174 -0
- sqlleaf/processors/transformer/values.py +10 -0
- sqlleaf/settings/__init__.py +19 -0
- sqlleaf/settings/athena.py +23 -0
- sqlleaf/settings/base.py +37 -0
- sqlleaf/settings/postgres.py +52 -0
- sqlleaf/system_tables/load.py +0 -0
- sqlleaf/typing.py +93 -0
- sqlleaf/util/__init__.py +34 -0
- sqlleaf/util/expression.py +282 -0
- sqlleaf/util/graph.py +116 -0
- sqlleaf/util/helpers.py +42 -0
- sqlleaf/util/iterators.py +37 -0
- sqlleaf-0.0.1.dist-info/METADATA +46 -0
- sqlleaf-0.0.1.dist-info/RECORD +111 -0
- sqlleaf-0.0.1.dist-info/WHEEL +5 -0
- sqlleaf-0.0.1.dist-info/licenses/LICENSE +21 -0
- sqlleaf-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
from sqlglot import exp
|
|
5
|
+
|
|
6
|
+
from sqlleaf import mappings, util, exception
|
|
7
|
+
from sqlleaf.models.query import CallQuery, CTASQuery, ExecuteQuery, FunctionParam, UserDefinedFunctionQuery
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("sqlleaf")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def find_arg(args: t.List[exp.Expr], param: FunctionParam, index: int) -> t.Optional[exp.Expr]:
|
|
13
|
+
"""
|
|
14
|
+
Finds the argument corresponding to a parameter by its name or position.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
Given query: SELECT my_func(1, y => 2)
|
|
18
|
+
and parameter 'y',
|
|
19
|
+
we get output 2.
|
|
20
|
+
"""
|
|
21
|
+
# 1. Named lookup
|
|
22
|
+
param_name = param.name.lower()
|
|
23
|
+
for arg in args:
|
|
24
|
+
if isinstance(arg, exp.Kwarg):
|
|
25
|
+
arg_key = arg.this
|
|
26
|
+
expr = arg.expression
|
|
27
|
+
elif isinstance(arg, exp.PropertyEQ):
|
|
28
|
+
arg_key = arg.left
|
|
29
|
+
expr = arg.right
|
|
30
|
+
else:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
is_variadic_call = isinstance(arg_key, exp.Variadic)
|
|
34
|
+
arg_name = (arg_key.this.name if is_variadic_call else arg_key.name).lower()
|
|
35
|
+
|
|
36
|
+
if arg_name == param_name:
|
|
37
|
+
if param.is_variadic:
|
|
38
|
+
if isinstance(expr, exp.Array) and not is_variadic_call and not isinstance(expr, exp.Variadic):
|
|
39
|
+
raise exception.MappingError(
|
|
40
|
+
f"VARIADIC keyword must be used when passing an array to variadic parameter '{param.name}'"
|
|
41
|
+
)
|
|
42
|
+
if isinstance(expr, exp.Variadic):
|
|
43
|
+
return expr.this
|
|
44
|
+
if is_variadic_call:
|
|
45
|
+
return expr
|
|
46
|
+
return expr
|
|
47
|
+
|
|
48
|
+
# 2. Positional lookup
|
|
49
|
+
if index < len(args) and not isinstance(args[index], (exp.Kwarg, exp.PropertyEQ)):
|
|
50
|
+
arg = args[index]
|
|
51
|
+
if not param.is_variadic:
|
|
52
|
+
return arg
|
|
53
|
+
|
|
54
|
+
# If the parameter is variadic and the supplied argument is an array,
|
|
55
|
+
# the keyword 'VARIADIC' must also be supplied.
|
|
56
|
+
if isinstance(arg, exp.Array):
|
|
57
|
+
raise exception.MappingError(
|
|
58
|
+
f"VARIADIC keyword must be used when passing an array to variadic parameter '{param.name}'"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if isinstance(arg, exp.Variadic):
|
|
62
|
+
return arg.this
|
|
63
|
+
|
|
64
|
+
# Convert variadic arguments into an array
|
|
65
|
+
# e.g. (1,2,3) -> ARRAY[1,2,3]
|
|
66
|
+
variadic = [a.copy() for a in args[index:] if not isinstance(a, (exp.Kwarg, exp.PropertyEQ))]
|
|
67
|
+
return exp.Array(expressions=variadic) if variadic else None
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def transform_arguments(
|
|
73
|
+
node: exp.Anonymous, query: UserDefinedFunctionQuery
|
|
74
|
+
) -> t.Tuple[t.Dict[str, exp.Expr], t.Dict[str, exp.Expr]]:
|
|
75
|
+
"""
|
|
76
|
+
Transforms every argument passed to the UDF into the correct format.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
For a UDF with parameters `(x, y)` called as `my_udf(1, y => 2)`, returns:
|
|
80
|
+
param_map = {"x": 1, "y": 2}
|
|
81
|
+
positional_map = {"1": 1, "2": 2}
|
|
82
|
+
"""
|
|
83
|
+
param_map = {}
|
|
84
|
+
positional_map = {}
|
|
85
|
+
args = node.expressions
|
|
86
|
+
|
|
87
|
+
logger.debug(f"Transforming arguments for UDF: {query.name}")
|
|
88
|
+
|
|
89
|
+
for i, param in enumerate(query.parameters):
|
|
90
|
+
arg_expr = find_arg(args, param, i) or param.default
|
|
91
|
+
|
|
92
|
+
if arg_expr:
|
|
93
|
+
# Normalize people.* to people
|
|
94
|
+
if isinstance(arg_expr, exp.Column) and arg_expr.this.name == "*":
|
|
95
|
+
arg_expr = exp.column(arg_expr.table)
|
|
96
|
+
elif isinstance(arg_expr, exp.TableColumn):
|
|
97
|
+
arg_expr = exp.column(arg_expr.this)
|
|
98
|
+
|
|
99
|
+
# If the parameter is a table type and the argument is a ROW expression without a cast,
|
|
100
|
+
# we need to add the cast to the expected type.
|
|
101
|
+
if util.is_row_function(arg_expr) and (
|
|
102
|
+
isinstance(param.type, exp.DataType) and param.type.this == exp.DataType.Type.USERDEFINED
|
|
103
|
+
):
|
|
104
|
+
arg_expr = exp.Cast(
|
|
105
|
+
this=arg_expr,
|
|
106
|
+
to=param.type.copy(),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
param_map[param.name.lower()] = arg_expr
|
|
110
|
+
positional_map[str(i + 1)] = arg_expr
|
|
111
|
+
|
|
112
|
+
return param_map, positional_map
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def substitute_parameters(
|
|
116
|
+
replacement_expr: exp.Expr,
|
|
117
|
+
query: UserDefinedFunctionQuery,
|
|
118
|
+
param_map: t.Dict[str, exp.Expr],
|
|
119
|
+
positional_map: t.Dict[str, exp.Expr],
|
|
120
|
+
) -> exp.Expr:
|
|
121
|
+
"""
|
|
122
|
+
Replaces parameter references within an expression with their corresponding arguments.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
Expression: `SELECT $1 + x`
|
|
126
|
+
Maps: param_map={'x': 10}, positional_map={'1': 5}
|
|
127
|
+
Result: `SELECT 5 + 10`
|
|
128
|
+
"""
|
|
129
|
+
# 1. Handle case where the root replacement expression itself needs substitution (e.g., expression is just 'x')
|
|
130
|
+
if isinstance(replacement_expr, exp.Column) and not replacement_expr.table:
|
|
131
|
+
col_name = replacement_expr.this.name.lower()
|
|
132
|
+
if col_name in param_map:
|
|
133
|
+
return param_map[col_name].copy()
|
|
134
|
+
|
|
135
|
+
elif isinstance(replacement_expr, exp.Parameter):
|
|
136
|
+
param_id = replacement_expr.this.name
|
|
137
|
+
if param_id in positional_map:
|
|
138
|
+
return positional_map[param_id].copy()
|
|
139
|
+
|
|
140
|
+
# 2. Walk and replace all references in the expression tree
|
|
141
|
+
for subnode in replacement_expr.walk():
|
|
142
|
+
substitute_parameter_node(subnode, query, param_map, positional_map)
|
|
143
|
+
|
|
144
|
+
return replacement_expr
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def substitute_parameter_node(
|
|
148
|
+
node: exp.Expr,
|
|
149
|
+
query: UserDefinedFunctionQuery,
|
|
150
|
+
param_map: t.Dict[str, exp.Expr],
|
|
151
|
+
positional_map: t.Dict[str, exp.Expr],
|
|
152
|
+
) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Replaces parameter references and member access for a single node.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
Before: `x + $1`
|
|
158
|
+
After: `10 + 5` (where x=10, $1=5)
|
|
159
|
+
"""
|
|
160
|
+
replacement = None
|
|
161
|
+
|
|
162
|
+
# 1. Determine if this node is a parameter reference and find its replacement
|
|
163
|
+
if isinstance(node, exp.Column) and not node.table:
|
|
164
|
+
col_name = node.this.name.lower()
|
|
165
|
+
if col_name in param_map:
|
|
166
|
+
# In Postgres, if a param name matches a column name in a query with a
|
|
167
|
+
# FROM clause, the column takes precedence.
|
|
168
|
+
parent_select = node.find_ancestor(exp.Select)
|
|
169
|
+
if parent_select:
|
|
170
|
+
for table in parent_select.find_all(exp.Table):
|
|
171
|
+
table_query = query.object_mapping.lookup_table_query(table)
|
|
172
|
+
if table_query and col_name in [c.name for c in table_query.get_column_defs()]:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
replacement = param_map[col_name]
|
|
176
|
+
elif isinstance(node, exp.Parameter):
|
|
177
|
+
param_id = node.this.name
|
|
178
|
+
if param_id in positional_map:
|
|
179
|
+
replacement = positional_map[param_id]
|
|
180
|
+
|
|
181
|
+
# 2. Apply replacement if found
|
|
182
|
+
if replacement is not None:
|
|
183
|
+
# Avoid replacing the left side of a Dot (member access), as it's handled separately
|
|
184
|
+
if not (isinstance(node.parent, exp.Dot) and node.arg_key == "this"):
|
|
185
|
+
replacement = replacement.copy()
|
|
186
|
+
|
|
187
|
+
# Wrap cast in parentheses if it's being indexed to ensure valid PostgreSQL syntax
|
|
188
|
+
# e.g. (ARRAY[]::numeric[])[i]
|
|
189
|
+
if isinstance(node.parent, exp.Bracket) and isinstance(replacement, exp.Cast):
|
|
190
|
+
replacement = exp.Paren(this=replacement)
|
|
191
|
+
|
|
192
|
+
node.replace(replacement)
|
|
193
|
+
|
|
194
|
+
# 3. Handle member access (e.g. SELECT x.name)
|
|
195
|
+
elif isinstance(node, exp.Dot):
|
|
196
|
+
replace_dot_reference(node, param_map, positional_map)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def replace_dot_reference(
|
|
200
|
+
node: exp.Dot, param_map: t.Dict[str, exp.Expr], positional_map: t.Dict[str, exp.Expr]
|
|
201
|
+
) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Replacements related to member access on parameters (e.g., $1.field -> table.field).
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
Before: `$1.name` (where $1 is a table-type argument 'people')
|
|
207
|
+
After: `people.name`
|
|
208
|
+
"""
|
|
209
|
+
# 1. Substitute the left side if it's a parameter
|
|
210
|
+
left = node.left
|
|
211
|
+
sub = (
|
|
212
|
+
param_map.get(left.this.name.lower())
|
|
213
|
+
if isinstance(left, exp.Column) and not left.table
|
|
214
|
+
else positional_map.get(left.this.name)
|
|
215
|
+
if isinstance(left, (exp.Parameter, exp.Placeholder))
|
|
216
|
+
else None
|
|
217
|
+
)
|
|
218
|
+
if not sub:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# 2. Extract table name from the substituted expression
|
|
222
|
+
table_name = (
|
|
223
|
+
sub.table or (sub.this.name if isinstance(sub.this, exp.Identifier) else None)
|
|
224
|
+
if isinstance(sub, exp.Column)
|
|
225
|
+
else sub.this.name
|
|
226
|
+
if isinstance(sub, exp.TableColumn)
|
|
227
|
+
else sub.name
|
|
228
|
+
if isinstance(sub, exp.Table)
|
|
229
|
+
else None
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if table_name:
|
|
233
|
+
node.replace(exp.Column(table=exp.Identifier(this=table_name, quoted=False), this=node.right.copy()))
|
|
234
|
+
elif isinstance(sub, exp.Cast):
|
|
235
|
+
# Wrap cast in parentheses so we can look up fields on it, e.g. (ROW(...)::type).field
|
|
236
|
+
node.set("this", exp.Paren(this=sub.copy()))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def substitute_call(query: CallQuery) -> t.List[exp.Expr]:
|
|
240
|
+
"""
|
|
241
|
+
Substitutes a CALL statement's arguments into the statmenets inside its body and returns them.
|
|
242
|
+
Circular references in procedure-to-procedure calls could cause infinite recursion.
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
CREATE PROCEDURE hello() ... SELECT 3;
|
|
246
|
+
CALL hello();
|
|
247
|
+
->
|
|
248
|
+
SELECT 3;
|
|
249
|
+
"""
|
|
250
|
+
procedure_table = exp.Table(
|
|
251
|
+
this=exp.to_identifier(query.procedure),
|
|
252
|
+
db=exp.to_identifier(query.schema) if query.schema else None,
|
|
253
|
+
)
|
|
254
|
+
matched_proc = query.object_mapping.lookup_procedure_query(procedure_table, raise_on_missing=False)
|
|
255
|
+
if not matched_proc:
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
logger.debug(f"Substituting CALL query '{query.name}' with procedure body")
|
|
259
|
+
|
|
260
|
+
# Re-use find_arg logic. We don't have an Anonymous node,
|
|
261
|
+
# just a list of args from the CallQuery.
|
|
262
|
+
param_map = {}
|
|
263
|
+
positional_map = {}
|
|
264
|
+
args = query.args
|
|
265
|
+
|
|
266
|
+
for i, param in enumerate(matched_proc.parameters):
|
|
267
|
+
arg_expr = find_arg(args, param, i) or param.default
|
|
268
|
+
|
|
269
|
+
if arg_expr:
|
|
270
|
+
param_map[param.name.lower()] = arg_expr
|
|
271
|
+
positional_map[str(i + 1)] = arg_expr
|
|
272
|
+
|
|
273
|
+
replacement_exprs = []
|
|
274
|
+
for stmt in matched_proc.inner_statements:
|
|
275
|
+
# Procedures can contain multiple statements
|
|
276
|
+
replacement_exprs.append(substitute_parameters(stmt.copy(), None, param_map, positional_map))
|
|
277
|
+
|
|
278
|
+
logger.debug(f"Substituted to {len(replacement_exprs)} statements:")
|
|
279
|
+
for r in replacement_exprs:
|
|
280
|
+
logger.debug(f" {r.sql(dialect=query.dialect)}")
|
|
281
|
+
return replacement_exprs
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def substitute_execute_with_plan(
|
|
285
|
+
execute_name: str, execute_arguments: t.List[exp.Literal], object_mapping: mappings.ObjectMapping
|
|
286
|
+
) -> t.List[exp.Expr]:
|
|
287
|
+
"""
|
|
288
|
+
Example:
|
|
289
|
+
PREPARE stmt AS SELECT 1;
|
|
290
|
+
EXECUTE stmt;
|
|
291
|
+
->
|
|
292
|
+
SELECT 1;
|
|
293
|
+
"""
|
|
294
|
+
plan_table = exp.to_table(execute_name)
|
|
295
|
+
matched_prepare = object_mapping.lookup_prepare_query(plan_table, raise_on_missing=False)
|
|
296
|
+
|
|
297
|
+
if not matched_prepare:
|
|
298
|
+
raise exception.MappingError(message=f"Could not find PREPARE statement for plan: {execute_name}")
|
|
299
|
+
|
|
300
|
+
expected = matched_prepare.parameter_count
|
|
301
|
+
actual = len(execute_arguments)
|
|
302
|
+
if expected > 0 and actual != expected:
|
|
303
|
+
raise exception.MappingError(
|
|
304
|
+
message=f"Wrong number of parameters for prepared statement (expected: {expected}, actual: {actual})"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
logger.debug(f"Substituting EXECUTE query for plan '{execute_name}'")
|
|
308
|
+
|
|
309
|
+
positional_map = {str(i + 1): arg for i, arg in enumerate(execute_arguments)}
|
|
310
|
+
result_expr = substitute_parameters(matched_prepare.statement.copy(), None, {}, positional_map)
|
|
311
|
+
|
|
312
|
+
return [result_expr]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def substitute_execute(query: ExecuteQuery) -> t.List[exp.Expr]:
|
|
316
|
+
"""
|
|
317
|
+
Substitutes an EXECUTE statement with the statement of the PREPARE it refers to.
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
PREPARE stmt AS SELECT 1;
|
|
321
|
+
EXECUTE stmt;
|
|
322
|
+
->
|
|
323
|
+
SELECT 1;
|
|
324
|
+
"""
|
|
325
|
+
execute_name = query.parameters.name
|
|
326
|
+
execute_args = query.parameters.arguments
|
|
327
|
+
|
|
328
|
+
return substitute_execute_with_plan(execute_name, execute_args, query.object_mapping)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def substitute_create_execute(query: CTASQuery) -> exp.Expr:
|
|
332
|
+
"""
|
|
333
|
+
Substitutes 'EXECUTE <plan>' in 'CREATE TABLE AS' with the actual query.
|
|
334
|
+
|
|
335
|
+
Example:
|
|
336
|
+
PREPARE stmt AS SELECT 1 AS one;
|
|
337
|
+
CREATE TABLE t AS EXECUTE stmt;
|
|
338
|
+
->
|
|
339
|
+
CREATE TABLE t AS SELECT 1 AS one;
|
|
340
|
+
"""
|
|
341
|
+
if exec_prop := query.statement.find(exp.ExecuteAsProperty):
|
|
342
|
+
if isinstance(exec_prop.this, exp.Anonymous):
|
|
343
|
+
execute_name = exec_prop.this.this
|
|
344
|
+
execute_args = exec_prop.this.expressions
|
|
345
|
+
else:
|
|
346
|
+
execute_name = exec_prop.this.name
|
|
347
|
+
execute_args = []
|
|
348
|
+
|
|
349
|
+
result_expr = substitute_execute_with_plan(execute_name, execute_args, query.object_mapping)[0]
|
|
350
|
+
|
|
351
|
+
# Replace property with the actual expression
|
|
352
|
+
expression = query.statement
|
|
353
|
+
expression.set("expression", result_expr)
|
|
354
|
+
exec_prop.pop()
|
|
355
|
+
|
|
356
|
+
return expression
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def substitute_session_variables(
|
|
360
|
+
stmt: exp.Expr,
|
|
361
|
+
session_variables: dict[str, exp.Expr],
|
|
362
|
+
) -> exp.Expr:
|
|
363
|
+
"""
|
|
364
|
+
Replaces $var Parameter nodes with their stored session variable values,
|
|
365
|
+
and resolves IDENTIFIER($var) DynamicIdentifier nodes to concrete identifiers.
|
|
366
|
+
"""
|
|
367
|
+
# 1. Walk and replace exp.Parameter nodes
|
|
368
|
+
for node in stmt.walk():
|
|
369
|
+
if isinstance(node, exp.Parameter):
|
|
370
|
+
var_name = node.this.name.upper()
|
|
371
|
+
if var_name in session_variables:
|
|
372
|
+
logger.debug(f"Substituting session variable: ${var_name}")
|
|
373
|
+
node.replace(session_variables[var_name].copy())
|
|
374
|
+
|
|
375
|
+
# 2. Resolve any remaining DynamicIdentifier nodes that wrap a resolved Literal
|
|
376
|
+
# (e.g. IDENTIFIER('my_table') or IDENTIFIER($var) after step 1)
|
|
377
|
+
# Use transform to handle replacements more robustly during traversal
|
|
378
|
+
def _transform_dynamic_identifiers(node):
|
|
379
|
+
if isinstance(node, exp.DynamicIdentifier):
|
|
380
|
+
inner = node.this
|
|
381
|
+
if isinstance(inner, exp.Literal) and inner.is_string:
|
|
382
|
+
resolved_name = inner.this
|
|
383
|
+
|
|
384
|
+
if isinstance(node.parent, exp.Table):
|
|
385
|
+
# FROM IDENTIFIER($b) → FROM my_table
|
|
386
|
+
# Here we modify the parent Table node's 'this' arg
|
|
387
|
+
node.parent.set("this", exp.to_identifier(resolved_name.upper()))
|
|
388
|
+
return node # It's already disconnected or about to be ignored
|
|
389
|
+
else:
|
|
390
|
+
# SELECT IDENTIFIER($col) → SELECT col_name
|
|
391
|
+
return exp.column(resolved_name.upper())
|
|
392
|
+
return node
|
|
393
|
+
|
|
394
|
+
stmt.transform(_transform_dynamic_identifiers, copy=False)
|
|
395
|
+
|
|
396
|
+
return stmt
|
|
File without changes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from sqlleaf.processors.generator.dialects.athena import AthenaGenerator as AthenaGenerator
|
|
2
|
+
from sqlleaf.processors.generator.dialects.mysql import MySqlGenerator as MySqlGenerator
|
|
3
|
+
from sqlleaf.processors.generator.dialects.postgres import PostgresGenerator as PostgresGenerator
|
|
4
|
+
from sqlleaf.processors.generator.dialects.redshift import RedshiftGenerator as RedshiftGenerator
|
|
5
|
+
from sqlleaf.processors.generator.dialects.snowflake import SnowflakeGenerator as SnowflakeGenerator
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
9
|
+
from sqlleaf.models.node import FileColumnNode
|
|
10
|
+
from sqlleaf.processors.generator.dialects.base import BaseGenerator, EdgeToCreate, singledispatchmethodlogger
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("sqlleaf")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AthenaGenerator(BaseGenerator):
|
|
16
|
+
dialect = "athena"
|
|
17
|
+
|
|
18
|
+
@singledispatchmethodlogger
|
|
19
|
+
def process(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
|
|
20
|
+
yield from super().process(expr, gen_ctx, pos_ctx)
|
|
21
|
+
|
|
22
|
+
@process.register
|
|
23
|
+
def process_location(
|
|
24
|
+
self, expr: exp.LocationProperty, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
25
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
26
|
+
"""
|
|
27
|
+
CREATE EXTERNAL TABLE ... LOCATION
|
|
28
|
+
"""
|
|
29
|
+
location = expr.this
|
|
30
|
+
child_node = gen_ctx.child_node
|
|
31
|
+
|
|
32
|
+
# TODO: change to source/target info
|
|
33
|
+
|
|
34
|
+
file_format_property = gen_ctx.query.statement.args["properties"].find(exp.FileFormatProperty)
|
|
35
|
+
file_format = file_format_property.this.name if file_format_property else ""
|
|
36
|
+
|
|
37
|
+
column_node = self.create_node(FileColumnNode(
|
|
38
|
+
column=child_node.name,
|
|
39
|
+
file_format=file_format,
|
|
40
|
+
file_path=location.name,
|
|
41
|
+
gen_ctx=gen_ctx,
|
|
42
|
+
pos_ctx=pos_ctx,
|
|
43
|
+
))
|
|
44
|
+
|
|
45
|
+
yield EdgeToCreate(column_node, child_node)
|