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,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
from sqlleaf import exception, util
|
|
9
|
+
from sqlleaf.models.query import Q
|
|
10
|
+
from sqlleaf.typing import E
|
|
11
|
+
from sqlleaf.util import default_column_index_iterator
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("sqlleaf")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_values(query: Q, expr: exp.Expr) -> exp.Expr:
|
|
17
|
+
"""
|
|
18
|
+
Convert a VALUES() statement into a SELECT .. UNION statement.
|
|
19
|
+
This handles two cases:
|
|
20
|
+
- When it is a top-level query (e.g. INSERT .. VALUES, CTAS .. VALUES)
|
|
21
|
+
- When it is inside another query, e.g. nested in an expression
|
|
22
|
+
"""
|
|
23
|
+
if isinstance(expr, exp.Values):
|
|
24
|
+
return _rewrite_values_statement(query, expression=expr, statement=expr)
|
|
25
|
+
|
|
26
|
+
unresolved_ids: t.Set[int] = set()
|
|
27
|
+
|
|
28
|
+
# Walk the subtree and rewrite all Values occurrences
|
|
29
|
+
prev_value = None
|
|
30
|
+
while True:
|
|
31
|
+
values = _pick_next_values_node(expr, unresolved_ids)
|
|
32
|
+
if values and prev_value == values:
|
|
33
|
+
raise exception.InvalidQueryError("Infinite loop detected while searching for the next VALUES() expression.")
|
|
34
|
+
|
|
35
|
+
if values is None:
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
# Determine if there is a CTE ancestor within the current statement scope
|
|
39
|
+
cte = _cte_ancestor_in_scope(expr, values)
|
|
40
|
+
if cte is not None:
|
|
41
|
+
if cte.this is values:
|
|
42
|
+
_rewrite_values_statement(query, values, cte)
|
|
43
|
+
else:
|
|
44
|
+
unresolved_ids.add(id(values))
|
|
45
|
+
else:
|
|
46
|
+
# VALUES is directly the expression of an INSERT
|
|
47
|
+
parent = values.parent
|
|
48
|
+
if isinstance(parent, exp.Insert) and parent.expression is values:
|
|
49
|
+
# Inline of _handle_values_in_insert
|
|
50
|
+
converted = _rewrite_values_statement(query, values, parent)
|
|
51
|
+
if isinstance(converted, exp.Insert) and (parent is expr):
|
|
52
|
+
expr = converted
|
|
53
|
+
|
|
54
|
+
# VALUES is directly the expression of a CREATE ... AS
|
|
55
|
+
elif isinstance(parent, exp.Create) and parent.expression is values:
|
|
56
|
+
_rewrite_values_statement(query, values, parent)
|
|
57
|
+
|
|
58
|
+
# VALUES is one of the sides of a UNION
|
|
59
|
+
elif isinstance(parent, exp.SetOperation):
|
|
60
|
+
_rewrite_values_statement(query, values, parent)
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
# VALUES in a table position (wrapped by Subquery or direct FROM on SELECT/UPDATE)
|
|
64
|
+
from_ancestor = values.find_ancestor(exp.From)
|
|
65
|
+
if (
|
|
66
|
+
values.find_ancestor(exp.Subquery) is not None
|
|
67
|
+
or values.parent_select
|
|
68
|
+
or (from_ancestor is not None and from_ancestor.this is values)
|
|
69
|
+
):
|
|
70
|
+
_rewrite_values_in_table_position(values, query.dialect)
|
|
71
|
+
|
|
72
|
+
prev_value = values
|
|
73
|
+
return expr
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _pick_next_values_node(statement: exp.Expr, unresolved: set[int]) -> exp.Values | None:
|
|
77
|
+
"""
|
|
78
|
+
Return the next exp.Values node in the statement not present in `unresolved`.
|
|
79
|
+
"""
|
|
80
|
+
for candidate in statement.find_all(exp.Values):
|
|
81
|
+
if id(candidate) not in unresolved:
|
|
82
|
+
return candidate
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _cte_ancestor_in_scope(statement: exp.Expr, values: exp.Values) -> exp.CTE | None:
|
|
87
|
+
"""
|
|
88
|
+
Return the enclosing CTE only if it belongs to the current statement's subtree.
|
|
89
|
+
This is to prevent us from using a CTE that does not enclose us.
|
|
90
|
+
"""
|
|
91
|
+
cte = values.find_ancestor(exp.CTE)
|
|
92
|
+
if cte is None:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
ctes_in_scope = {id(c) for c in statement.find_all(exp.CTE)}
|
|
96
|
+
if id(cte) not in ctes_in_scope:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
return cte
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _rewrite_values_in_table_position(values: exp.Values, dialect: str) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Rewrite VALUES used in FROM/JOIN/LATERAL positions.
|
|
105
|
+
|
|
106
|
+
Supports both:
|
|
107
|
+
- Subquery(Values), which updates the inner expression and preserves the outer alias/columns.
|
|
108
|
+
- Direct 'From(Values)', which converts and transfers aliases.
|
|
109
|
+
"""
|
|
110
|
+
converted = _values_to_select_expr(
|
|
111
|
+
values=values,
|
|
112
|
+
dialect=dialect,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# SELECT (VALUES ...)
|
|
116
|
+
outer_subquery = values.find_ancestor(exp.Subquery)
|
|
117
|
+
if outer_subquery is not None and outer_subquery.this is values:
|
|
118
|
+
if isinstance(converted, exp.Subquery):
|
|
119
|
+
converted = converted.this # unwrap nested Subquery
|
|
120
|
+
outer_subquery.set("this", converted)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# SELECT/UPDATE/DELETE FROM (VALUES ...)
|
|
124
|
+
# Case A: VALUES appears in a SELECT's FROM
|
|
125
|
+
parent_select = values.parent_select
|
|
126
|
+
from_ = parent_select and parent_select.args.get("from_")
|
|
127
|
+
if from_ and from_.this is values:
|
|
128
|
+
# Ensure we have a Subquery in FROM and preserve the alias/column names
|
|
129
|
+
original_alias = values.args.get("alias")
|
|
130
|
+
if original_alias and not converted.args.get("alias"):
|
|
131
|
+
converted.set("alias", original_alias)
|
|
132
|
+
from_.set("this", converted)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Case B: VALUES appears directly under a FROM of non-SELECT (e.g., UPDATE ... FROM (VALUES ...))
|
|
136
|
+
from_ancestor = values.find_ancestor(exp.From)
|
|
137
|
+
if from_ancestor is not None and from_ancestor.this is values:
|
|
138
|
+
original_alias = values.args.get("alias")
|
|
139
|
+
if not isinstance(converted, exp.Subquery):
|
|
140
|
+
converted = converted.subquery()
|
|
141
|
+
if original_alias and not converted.args.get("alias"):
|
|
142
|
+
converted.set("alias", original_alias)
|
|
143
|
+
from_ancestor.set("this", converted)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _resolve_values_column_names(values: exp.Values, parent: exp.Expr, query: Q) -> list[str]:
|
|
147
|
+
"""
|
|
148
|
+
Resolve column names for the expressions inside VALUES() based on its surrounding expressions.
|
|
149
|
+
"""
|
|
150
|
+
columns = []
|
|
151
|
+
if isinstance(parent, exp.CTE):
|
|
152
|
+
columns = parent.alias_column_names
|
|
153
|
+
elif not isinstance(parent, exp.Values):
|
|
154
|
+
columns = [e.name for e in parent.this.expressions]
|
|
155
|
+
|
|
156
|
+
if not columns:
|
|
157
|
+
# Fall back to the mapping
|
|
158
|
+
# TODO: remove after downstream bug found
|
|
159
|
+
child_table = query.target_info.expression
|
|
160
|
+
if isinstance(child_table, exp.Table):
|
|
161
|
+
cols = query.object_mapping.find_columns_for_table(child_table)
|
|
162
|
+
values_lists: t.List[exp.Tuple] = values.expressions
|
|
163
|
+
columns = list(cols)[: len(values_lists[0].expressions)]
|
|
164
|
+
|
|
165
|
+
# Returning empty will populate dialect defaults
|
|
166
|
+
return list(columns)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _substitute_default_column_references(query: Q, values: exp.Values, target_table: exp.Table | None) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Replace the column names used inside `INSERT ... VALUES (...)` statements
|
|
172
|
+
with the referenced column's DEFAULT expression (or NULL), per MySQL's semantics for
|
|
173
|
+
VALUES(column_name).
|
|
174
|
+
|
|
175
|
+
This also applies when the column reference is nested inside another expression,
|
|
176
|
+
e.g. VALUES(UPPER(name), age * 2).
|
|
177
|
+
|
|
178
|
+
Example [given name's default is 5]:
|
|
179
|
+
INSERT INTO (name) VALUES (name)
|
|
180
|
+
->
|
|
181
|
+
INSERT INTO (name) SELECT 5 AS name
|
|
182
|
+
"""
|
|
183
|
+
if target_table is None:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
table_query = query.object_mapping.lookup_table_query(table=target_table, raise_on_missing=False)
|
|
187
|
+
if not table_query:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
table_columns = table_query.get_column_defs()
|
|
191
|
+
table_column_names = {col.name for col in table_columns}
|
|
192
|
+
|
|
193
|
+
for value_expr in values.expressions:
|
|
194
|
+
if not isinstance(value_expr, exp.Tuple):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Replace each column with its default/generated expression
|
|
198
|
+
for tuple_expr in value_expr.expressions:
|
|
199
|
+
for column_ref in list(tuple_expr.find_all(exp.Column)):
|
|
200
|
+
if column_ref.name in table_column_names:
|
|
201
|
+
col_def = next(c for c in table_columns if c.name == column_ref.name)
|
|
202
|
+
if default_constraint := util.get_column_constraint_expression(col_def):
|
|
203
|
+
column_ref.replace(default_constraint.this.copy())
|
|
204
|
+
else:
|
|
205
|
+
column_ref.replace(exp.Null())
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _rewrite_empty_values_or_values_with_column_names(
|
|
209
|
+
query: Q, expression: exp.Values, statement: E, columns: t.List[str]
|
|
210
|
+
) -> t.List[str]:
|
|
211
|
+
"""
|
|
212
|
+
MySQL-only handling for INSERT ... VALUES lists:
|
|
213
|
+
- Expand empty VALUES() to refer to all target columns so DEFAULTs can be applied.
|
|
214
|
+
- Replace target column-name references in VALUES(...) with that column's DEFAULT (or NULL).
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
CREATE TABLE t(a INT DEFAULT 5, b INT);
|
|
218
|
+
INSERT INTO t () VALUES(); -> SELECT 5 AS a, NULL AS b
|
|
219
|
+
INSERT INTO t (a,b) VALUES(a, UPPER(b)) -> SELECT 5 AS a, UPPER(NULL) AS b
|
|
220
|
+
"""
|
|
221
|
+
if isinstance(statement, exp.Insert) and query.dialect == "mysql":
|
|
222
|
+
into_table = statement.find(exp.Table)
|
|
223
|
+
|
|
224
|
+
# Expand an empty tuple to reference all target columns so substitution can occur
|
|
225
|
+
if (
|
|
226
|
+
expression.expressions
|
|
227
|
+
and isinstance(expression.expressions[0], exp.Tuple)
|
|
228
|
+
and len(expression.expressions[0].expressions) == 0
|
|
229
|
+
):
|
|
230
|
+
table_query = (
|
|
231
|
+
query.object_mapping.lookup_table_query(table=into_table, raise_on_missing=False)
|
|
232
|
+
if into_table is not None
|
|
233
|
+
else None
|
|
234
|
+
)
|
|
235
|
+
if table_query:
|
|
236
|
+
# Replace the empty tuple values with references to each column name
|
|
237
|
+
columns = [c.name for c in table_query.get_column_defs()]
|
|
238
|
+
expression.expressions[0].set("expressions", [exp.column(c) for c in columns])
|
|
239
|
+
|
|
240
|
+
_substitute_default_column_references(query, expression, into_table)
|
|
241
|
+
|
|
242
|
+
return columns
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _rewrite_values_statement(query: Q, expression: exp.Values, statement: E) -> E:
|
|
246
|
+
"""
|
|
247
|
+
Convert a `VALUES(...)` statement into a `SELECT ... UNION ALL SELECT ...` statement
|
|
248
|
+
and rewrite the parent statement in-place.
|
|
249
|
+
"""
|
|
250
|
+
if not isinstance(expression, exp.Values):
|
|
251
|
+
return statement
|
|
252
|
+
|
|
253
|
+
columns = _resolve_values_column_names(expression, statement, query)
|
|
254
|
+
columns = _rewrite_empty_values_or_values_with_column_names(query, expression, statement, columns)
|
|
255
|
+
|
|
256
|
+
# Build the 'SELECT ... UNION ALL SELECT ...'
|
|
257
|
+
new_statement = _values_to_select_expr(
|
|
258
|
+
values=expression,
|
|
259
|
+
dialect=query.dialect,
|
|
260
|
+
column_names=columns,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Rewrite the parent statement with the existing column list
|
|
264
|
+
if isinstance(statement, exp.Insert):
|
|
265
|
+
into_table = statement.find(exp.Table)
|
|
266
|
+
insert_columns = statement.this.expressions
|
|
267
|
+
|
|
268
|
+
insert_expr = exp.insert(
|
|
269
|
+
expression=new_statement,
|
|
270
|
+
columns=insert_columns,
|
|
271
|
+
into=into_table or statement.this,
|
|
272
|
+
returning=statement.args.get("returning"),
|
|
273
|
+
)
|
|
274
|
+
insert_expr.set("conflict", statement.args.get("conflict"))
|
|
275
|
+
statement.replace(insert_expr)
|
|
276
|
+
statement = insert_expr
|
|
277
|
+
elif isinstance(statement, exp.Create):
|
|
278
|
+
expression.replace(new_statement)
|
|
279
|
+
elif isinstance(statement, exp.CTE):
|
|
280
|
+
if isinstance(new_statement, exp.Subquery):
|
|
281
|
+
new_statement = new_statement.this
|
|
282
|
+
expression.replace(new_statement)
|
|
283
|
+
elif isinstance(statement, exp.Values):
|
|
284
|
+
statement = new_statement
|
|
285
|
+
elif isinstance(statement, exp.SetOperation):
|
|
286
|
+
expression.replace(new_statement)
|
|
287
|
+
else:
|
|
288
|
+
raise exception.InvalidQueryError(message=f"Unknown statement type: {statement.__class__}")
|
|
289
|
+
|
|
290
|
+
return statement
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _values_to_select_expr(
|
|
294
|
+
values: exp.Values,
|
|
295
|
+
dialect: str,
|
|
296
|
+
column_names: t.Optional[t.List[str]] = None,
|
|
297
|
+
) -> exp.Expr:
|
|
298
|
+
"""
|
|
299
|
+
Convert an exp.Values into an exp.Select or exp.Union.
|
|
300
|
+
"""
|
|
301
|
+
values_lists: t.List[exp.Tuple] = values.expressions
|
|
302
|
+
if not column_names:
|
|
303
|
+
column_names = list(default_column_index_iterator(dialect, values_lists[0].expressions))
|
|
304
|
+
|
|
305
|
+
selects = []
|
|
306
|
+
for val_list in values_lists:
|
|
307
|
+
row_vals = val_list.expressions
|
|
308
|
+
cols = [exp.alias_(val, str(col)) for col, val in zip(column_names, row_vals)]
|
|
309
|
+
selects.append(cols)
|
|
310
|
+
|
|
311
|
+
if len(selects) > 1:
|
|
312
|
+
new_selects = [exp.select(*select) for select in selects]
|
|
313
|
+
return_expr = exp.union(*new_selects, distinct=False)
|
|
314
|
+
else:
|
|
315
|
+
return_expr = exp.select(*selects[0])
|
|
316
|
+
|
|
317
|
+
# Wrap the query in a subquery if it's not a top-level statement
|
|
318
|
+
return return_expr.subquery() if values.parent_select else return_expr
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from sqlglot.optimizer.annotate_types import annotate_types
|
|
5
|
+
|
|
6
|
+
from sqlleaf.processors.transformer.base import BaseQueryTransformer
|
|
7
|
+
from sqlleaf.typing import E
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InsertTransformer(BaseQueryTransformer):
|
|
11
|
+
"""Transformer for INSERT statements."""
|
|
12
|
+
|
|
13
|
+
def transform(self, statement: exp.Insert) -> exp.Insert:
|
|
14
|
+
statement = self._add_information_from_merge(statement)
|
|
15
|
+
statement = self._add_information_from_multitable_insert(statement)
|
|
16
|
+
statement = self._process_inner_ctes(statement)
|
|
17
|
+
statement = self._remove_on_conflict(statement)
|
|
18
|
+
|
|
19
|
+
return statement
|
|
20
|
+
|
|
21
|
+
def postprocess(self, statement: E) -> E:
|
|
22
|
+
# TODO: this is a hack to get the types annotated for UDF overloading to work for Inserts.
|
|
23
|
+
# Revisit proper type annotation so that every query (not just Inserts) has its types populated to enable UDF resolution.
|
|
24
|
+
statement = annotate_types(statement, dialect=self.query.dialect, schema=self.query.object_mapping)
|
|
25
|
+
return super().postprocess(statement)
|
|
26
|
+
|
|
27
|
+
def _remove_on_conflict(self, statement: exp.Insert) -> exp.Insert:
|
|
28
|
+
"""
|
|
29
|
+
Remove the 'ON CONFLICT ...' clause from the INSERT statement.
|
|
30
|
+
"""
|
|
31
|
+
if conflict := statement.args.get("conflict"):
|
|
32
|
+
conflict.pop()
|
|
33
|
+
return statement
|
|
34
|
+
|
|
35
|
+
@BaseQueryTransformer._validate_syntax
|
|
36
|
+
def _add_information_from_merge(self, statement: exp.Insert) -> exp.Insert:
|
|
37
|
+
"""
|
|
38
|
+
INSERT branch of _add_information_from_merge.
|
|
39
|
+
|
|
40
|
+
Transform a nested INSERT (from a MERGE WHEN NOT MATCHED branch) into a
|
|
41
|
+
fully qualified INSERT … SELECT so it can be processed independently.
|
|
42
|
+
"""
|
|
43
|
+
ctx = self._extract_merge_context(statement)
|
|
44
|
+
if ctx is None:
|
|
45
|
+
return statement
|
|
46
|
+
|
|
47
|
+
using = ctx["using"]
|
|
48
|
+
returning = ctx["returning"]
|
|
49
|
+
target = self.query.target_info.expression
|
|
50
|
+
|
|
51
|
+
# Add the missing information to the INSERT statement
|
|
52
|
+
insert_columns = self._extract_insert_columns(statement, target, include_system=False)
|
|
53
|
+
values_lists = self._extract_value_lists(statement.expression)
|
|
54
|
+
|
|
55
|
+
if not values_lists:
|
|
56
|
+
return statement
|
|
57
|
+
|
|
58
|
+
# Build a new SELECT
|
|
59
|
+
values = values_lists[0]
|
|
60
|
+
aliases = [exp.alias_(val, str(col)) for col, val in zip(insert_columns, values)]
|
|
61
|
+
new_select = exp.select(*aliases).from_(using)
|
|
62
|
+
|
|
63
|
+
# 3. Build the standalone INSERT statement
|
|
64
|
+
insert_expr = exp.insert(
|
|
65
|
+
expression=new_select,
|
|
66
|
+
columns=insert_columns,
|
|
67
|
+
into=target,
|
|
68
|
+
dialect=self.query.dialect,
|
|
69
|
+
returning=returning,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Add the CTEs
|
|
73
|
+
for cte in ctx["ctes"]:
|
|
74
|
+
insert_expr = insert_expr.with_(alias=cte.alias_or_name, as_=cte.this)
|
|
75
|
+
|
|
76
|
+
statement.replace(insert_expr)
|
|
77
|
+
return insert_expr
|
|
78
|
+
|
|
79
|
+
@BaseQueryTransformer._validate_syntax
|
|
80
|
+
def _add_information_from_multitable_insert(self, statement: exp.Insert) -> exp.Insert:
|
|
81
|
+
"""
|
|
82
|
+
Reconstruct a standalone INSERT .. SELECT from a MultitableInsert branch.
|
|
83
|
+
"""
|
|
84
|
+
ctx = self._extract_multitable_insert_context(statement)
|
|
85
|
+
if ctx is None:
|
|
86
|
+
return statement
|
|
87
|
+
|
|
88
|
+
source = ctx["source"]
|
|
89
|
+
target = self.query.target_info.expression
|
|
90
|
+
|
|
91
|
+
insert_columns = self._extract_insert_columns(statement, target, include_system=False)
|
|
92
|
+
values_lists = self._extract_value_lists(statement.expression)
|
|
93
|
+
|
|
94
|
+
if not values_lists:
|
|
95
|
+
return statement
|
|
96
|
+
|
|
97
|
+
selects = []
|
|
98
|
+
for values in values_lists:
|
|
99
|
+
aliases = [exp.alias_(val, str(col)) for col, val in zip(insert_columns, values)]
|
|
100
|
+
new_select = exp.select(*aliases).from_(source.subquery())
|
|
101
|
+
selects.append(new_select)
|
|
102
|
+
|
|
103
|
+
new_expression = exp.union(*selects, distinct=False) if len(selects) > 1 else selects[0]
|
|
104
|
+
|
|
105
|
+
insert_expr = exp.insert(
|
|
106
|
+
expression=new_expression,
|
|
107
|
+
columns=insert_columns,
|
|
108
|
+
into=target,
|
|
109
|
+
dialect=self.query.dialect,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
statement.replace(insert_expr)
|
|
113
|
+
return insert_expr
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _extract_value_lists(expression: exp.Expression) -> t.List[t.List[exp.Expression]]:
|
|
117
|
+
"""
|
|
118
|
+
Handles exp.Tuple, exp.Select, and exp.Union.
|
|
119
|
+
"""
|
|
120
|
+
values_lists = []
|
|
121
|
+
if isinstance(expression, exp.Tuple):
|
|
122
|
+
values_lists = [expression.expressions]
|
|
123
|
+
elif isinstance(expression, exp.Select):
|
|
124
|
+
values_lists = [[s.unalias() for s in expression.expressions]]
|
|
125
|
+
return values_lists
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MergeTransformer — handles MERGE statement transformations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf.processors.transformer.base import BaseQueryTransformer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MergeTransformer(BaseQueryTransformer):
|
|
11
|
+
"""Transformer for MERGE statements."""
|
|
12
|
+
|
|
13
|
+
def transform(self, statement: exp.Merge) -> exp.Merge:
|
|
14
|
+
return self._process_inner_ctes(statement)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultitableInsertTransformer — handles MultitableInserts statement transformations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf.processors.transformer.base import BaseQueryTransformer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MultitableInsertTransformer(BaseQueryTransformer):
|
|
11
|
+
"""Transformer for MultitableInserts statements."""
|
|
12
|
+
|
|
13
|
+
def transform(self, statement: exp.MultitableInserts) -> exp.MultitableInserts:
|
|
14
|
+
return statement
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import sqlglot
|
|
2
|
+
from sqlglot import exp
|
|
3
|
+
|
|
4
|
+
from sqlleaf.processors.transformer.expressions import normalize_values
|
|
5
|
+
from sqlleaf.processors.transformer.insert import InsertTransformer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReplaceTransformer(InsertTransformer):
|
|
9
|
+
"""Transformer for REPLACE statements."""
|
|
10
|
+
|
|
11
|
+
def transform(self, statement: exp.Insert) -> exp.Insert:
|
|
12
|
+
# Transform REPLACE to INSERT
|
|
13
|
+
expression = statement.args.get("expression")
|
|
14
|
+
new_sql = f"INSERT {expression.this}" if expression else "INSERT"
|
|
15
|
+
statement = sqlglot.parse_one(new_sql, dialect=self.query.dialect)
|
|
16
|
+
|
|
17
|
+
# This newly-parsed statement never went through preprocess(), so it hasn't
|
|
18
|
+
# had DEFAULT VALUES expansion or VALUES->SELECT normalization applied yet.
|
|
19
|
+
if isinstance(statement, exp.Insert):
|
|
20
|
+
statement = self._convert_insert_defaults_to_values(statement)
|
|
21
|
+
statement = normalize_values(self.query, statement)
|
|
22
|
+
|
|
23
|
+
return super().transform(statement)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import util
|
|
6
|
+
from sqlleaf.models.query import (
|
|
7
|
+
CallQuery,
|
|
8
|
+
CopyQuery,
|
|
9
|
+
CTASQuery,
|
|
10
|
+
DeleteQuery,
|
|
11
|
+
ExecuteQuery,
|
|
12
|
+
InsertQuery,
|
|
13
|
+
MergeQuery,
|
|
14
|
+
MultitableInsertQuery,
|
|
15
|
+
Q,
|
|
16
|
+
QueryHolder,
|
|
17
|
+
ReplaceQuery,
|
|
18
|
+
TableQuery,
|
|
19
|
+
UnloadQuery,
|
|
20
|
+
UpdateQuery,
|
|
21
|
+
ValuesQuery,
|
|
22
|
+
)
|
|
23
|
+
from sqlleaf.processors.transformer import (
|
|
24
|
+
BaseQueryTransformer,
|
|
25
|
+
CallTransformer,
|
|
26
|
+
CopyTransformer,
|
|
27
|
+
CTASTransformer,
|
|
28
|
+
DeleteTransformer,
|
|
29
|
+
ExecuteTransformer,
|
|
30
|
+
InsertTransformer,
|
|
31
|
+
MergeTransformer,
|
|
32
|
+
MultitableInsertTransformer,
|
|
33
|
+
ReplaceTransformer,
|
|
34
|
+
UnloadTransformer,
|
|
35
|
+
UpdateTransformer,
|
|
36
|
+
ValuesTransformer,
|
|
37
|
+
)
|
|
38
|
+
from sqlleaf.typing import E
|
|
39
|
+
|
|
40
|
+
_TRANSFORMER_MAP: dict[type, type[BaseQueryTransformer]] = {
|
|
41
|
+
CallQuery: CallTransformer,
|
|
42
|
+
CTASQuery: CTASTransformer,
|
|
43
|
+
CopyQuery: CopyTransformer,
|
|
44
|
+
DeleteQuery: DeleteTransformer,
|
|
45
|
+
ExecuteQuery: ExecuteTransformer,
|
|
46
|
+
InsertQuery: InsertTransformer,
|
|
47
|
+
MergeQuery: MergeTransformer,
|
|
48
|
+
MultitableInsertQuery: MultitableInsertTransformer,
|
|
49
|
+
ReplaceQuery: ReplaceTransformer,
|
|
50
|
+
TableQuery: BaseQueryTransformer, # pass-through
|
|
51
|
+
UnloadQuery: UnloadTransformer,
|
|
52
|
+
UpdateQuery: UpdateTransformer,
|
|
53
|
+
ValuesQuery: ValuesTransformer,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger("sqlleaf")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def transform_query(query_holder: QueryHolder) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Transform an SQL query into a single, normalized form that we can easily generate lineage from.
|
|
62
|
+
|
|
63
|
+
This runs a series of transformations specific to each query type. Queries register their own
|
|
64
|
+
transformation functions to ensure that the query is fully qualified, with all columns added and
|
|
65
|
+
functions substituted so that they can be extracted into a graph by the generator.
|
|
66
|
+
"""
|
|
67
|
+
transformed_query = _transform_query_instance(query=query_holder.original)
|
|
68
|
+
query_holder.set_transformed_query(query=transformed_query)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _transform_query_instance(query: Q) -> Q:
|
|
72
|
+
"""
|
|
73
|
+
Helper to transform a Query instance.
|
|
74
|
+
"""
|
|
75
|
+
statement_to_transform = util.copy_expression(query.statement)
|
|
76
|
+
transformed_statement = _transform_statement(statement=statement_to_transform, query=query)
|
|
77
|
+
|
|
78
|
+
return _build_transformed_query(
|
|
79
|
+
original_query=query,
|
|
80
|
+
transformed_statement=transformed_statement,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_transformed_query(
|
|
85
|
+
original_query: Q,
|
|
86
|
+
transformed_statement: exp.Expr,
|
|
87
|
+
) -> Q:
|
|
88
|
+
"""
|
|
89
|
+
Create a new Query instance whose statement is the transformed expression.
|
|
90
|
+
The Query subclass is selected based on the statement type.
|
|
91
|
+
"""
|
|
92
|
+
if isinstance(transformed_statement, exp.Insert):
|
|
93
|
+
new_query = InsertQuery(
|
|
94
|
+
expr=transformed_statement,
|
|
95
|
+
dialect=original_query.dialect,
|
|
96
|
+
object_mapping=original_query.object_mapping,
|
|
97
|
+
statement_index=original_query.statement_index,
|
|
98
|
+
skip_annotate=True,
|
|
99
|
+
)
|
|
100
|
+
# TODO: everything below this in this function this should not occur
|
|
101
|
+
# - requires big refactor
|
|
102
|
+
# CopyQuery special case: preserve source_info/target_info so that
|
|
103
|
+
# _apply_optimizations can still read the STREAM/FILE/STAGE type.
|
|
104
|
+
if isinstance(original_query, (CopyQuery, UnloadQuery)):
|
|
105
|
+
new_query.source_info = original_query.source_info
|
|
106
|
+
new_query.target_info = original_query.target_info
|
|
107
|
+
else:
|
|
108
|
+
# For statements not converted to INSERT, keep the same Query subclass
|
|
109
|
+
# but with the new statement.
|
|
110
|
+
new_query = original_query.__class__.__new__(original_query.__class__)
|
|
111
|
+
new_query.__dict__.update(original_query.__dict__)
|
|
112
|
+
new_query.statement = transformed_statement
|
|
113
|
+
|
|
114
|
+
# Propagate shared metadata
|
|
115
|
+
new_query.column_defs = original_query.column_defs
|
|
116
|
+
new_query.parent_query = original_query.parent_query
|
|
117
|
+
# Store a reference to the original query so that type-based checks in the
|
|
118
|
+
# generator (e.g. isinstance(query, UpdateQuery)) can inspect the original class.
|
|
119
|
+
new_query.original_query = original_query
|
|
120
|
+
return new_query
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _transform_statement(statement: E, query: Q) -> exp.Expr:
|
|
124
|
+
"""
|
|
125
|
+
Perform a series of transformations against an SQL statement.
|
|
126
|
+
Dispatches to the appropriate transformer class based on the query type.
|
|
127
|
+
"""
|
|
128
|
+
logger.debug("---- Transformer ---")
|
|
129
|
+
logger.debug(f"Query: {statement.sql(dialect=query.dialect)}")
|
|
130
|
+
logger.debug(f"Transforming: {query.__class__.__name__} - {statement.__class__}")
|
|
131
|
+
|
|
132
|
+
transformer_cls = _TRANSFORMER_MAP.get(type(query), BaseQueryTransformer)
|
|
133
|
+
transformer = transformer_cls(statement, query)
|
|
134
|
+
stmt = transformer.preprocess(statement)
|
|
135
|
+
logger.debug(f"After pre-process: {type(stmt)} - {stmt.sql(dialect=query.dialect)}")
|
|
136
|
+
stmt = transformer.transform(stmt)
|
|
137
|
+
logger.debug(f"After process: {type(stmt)} - {stmt.sql(dialect=query.dialect)}")
|
|
138
|
+
stmt = transformer.postprocess(stmt)
|
|
139
|
+
logger.debug(f"After post-process: {type(stmt)} - {stmt.sql(dialect=query.dialect)}")
|
|
140
|
+
logger.debug("---- Transformer End ---")
|
|
141
|
+
return stmt
|