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,899 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import sqlglot
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from sqlglot.optimizer import RULES, optimize, qualify
|
|
8
|
+
from sqlglot.optimizer.merge_subqueries import merge_derived_tables
|
|
9
|
+
|
|
10
|
+
from sqlleaf import exception, util
|
|
11
|
+
from sqlleaf.models.query import Q
|
|
12
|
+
from sqlleaf.processors.transformer import udf
|
|
13
|
+
from sqlleaf.processors.transformer.expressions import (
|
|
14
|
+
_rewrite_values_statement,
|
|
15
|
+
add_parens_for_composite_field_access,
|
|
16
|
+
normalize_values,
|
|
17
|
+
rewrite_functional_notation_columns,
|
|
18
|
+
simplify_row,
|
|
19
|
+
)
|
|
20
|
+
from sqlleaf.settings import system_functions as get_system_functions
|
|
21
|
+
from sqlleaf.typing import E, SqlObjectType
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("sqlleaf")
|
|
24
|
+
|
|
25
|
+
EXCLUDE_OPTIMIZER_RULES = [
|
|
26
|
+
"eliminate_ctes", # Preserve CTEs
|
|
27
|
+
"merge_subqueries", # Preserve CTEs
|
|
28
|
+
"qualify", # We qualify when we need to
|
|
29
|
+
"quote_identifiers", # Preserve identifiers
|
|
30
|
+
"eliminate_subqueries", # Preserve subqueries
|
|
31
|
+
]
|
|
32
|
+
LOG_TRANSFORMATIONS = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseQueryTransformer:
|
|
36
|
+
"""
|
|
37
|
+
Base class holding shared transformation helpers.
|
|
38
|
+
Subclasses call these helpers from their transform() method.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, statement: E, query: Q) -> None:
|
|
42
|
+
self.statement = statement
|
|
43
|
+
self.query = query
|
|
44
|
+
|
|
45
|
+
def transform(self, statement: E) -> E:
|
|
46
|
+
"""
|
|
47
|
+
Default transformation function that subclasses override to add query-type-specific logic.
|
|
48
|
+
"""
|
|
49
|
+
return statement
|
|
50
|
+
|
|
51
|
+
def _validate_syntax(func):
|
|
52
|
+
"""
|
|
53
|
+
Ensure that the transformed query is parseable.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@functools.wraps(func)
|
|
57
|
+
def wrapper(self, *args, **kwargs) -> E:
|
|
58
|
+
statement = args[0] if args else kwargs.get("statement", self.statement)
|
|
59
|
+
query = self.query
|
|
60
|
+
|
|
61
|
+
if LOG_TRANSFORMATIONS:
|
|
62
|
+
logger.debug(f"Function: {func.__name__}")
|
|
63
|
+
|
|
64
|
+
result = func(self, *args, **kwargs)
|
|
65
|
+
|
|
66
|
+
in_statement = statement.sql(dialect=query.dialect)
|
|
67
|
+
out_statement = result.sql(dialect=query.dialect)
|
|
68
|
+
|
|
69
|
+
if result and (in_statement != out_statement):
|
|
70
|
+
logger.debug(f"Function: {func.__name__} transformed: {out_statement}")
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
return wrapper
|
|
75
|
+
|
|
76
|
+
@t.final
|
|
77
|
+
def preprocess(self, statement: E) -> E:
|
|
78
|
+
"""
|
|
79
|
+
Run a set of transformations over every statement
|
|
80
|
+
BEFORE the type-specific transformations.
|
|
81
|
+
"""
|
|
82
|
+
statement = self._expand_to_query(statement)
|
|
83
|
+
statement = self._convert_table_to_select(statement)
|
|
84
|
+
|
|
85
|
+
# Remove any FILTER clauses (not used for lineage)
|
|
86
|
+
for filter_expr in statement.find_all(exp.Filter):
|
|
87
|
+
filter_expr.replace(filter_expr.this)
|
|
88
|
+
|
|
89
|
+
# Remove WHERE clauses (not used for lineage)
|
|
90
|
+
for where_expr in statement.find_all(exp.Where):
|
|
91
|
+
where_expr.pop()
|
|
92
|
+
|
|
93
|
+
simplify_row(statement, self.query)
|
|
94
|
+
|
|
95
|
+
if isinstance(statement, exp.Insert):
|
|
96
|
+
statement = self._convert_insert_defaults_to_values(statement)
|
|
97
|
+
|
|
98
|
+
statement = normalize_values(self.query, statement)
|
|
99
|
+
return statement
|
|
100
|
+
|
|
101
|
+
@t.final
|
|
102
|
+
def postprocess(self, statement: E) -> E:
|
|
103
|
+
"""
|
|
104
|
+
Run a set of transformations over every statement
|
|
105
|
+
AFTER the type-specific transformations.
|
|
106
|
+
"""
|
|
107
|
+
# TODO: ideally this is done here, but it would overwrite the types manually added for COPY, UNLOAD etc
|
|
108
|
+
# statement = annotate_types(statement, dialect=self.query.dialect, schema=self.query.object_mapping)
|
|
109
|
+
|
|
110
|
+
validate_columns, exclusion_rules = self._get_validation_and_exclusion_rules(statement)
|
|
111
|
+
|
|
112
|
+
statement = self._apply_udf_substitutions(statement)
|
|
113
|
+
statement = self._add_aliases_to_udfs(statement)
|
|
114
|
+
statement = self._qualify_function_columns(statement)
|
|
115
|
+
statement = self._apply_qualify(statement, validate_columns)
|
|
116
|
+
statement = rewrite_functional_notation_columns(statement)
|
|
117
|
+
statement = self._add_aliases_to_pseudocolumns(statement)
|
|
118
|
+
statement = self._add_column_names_to_insert(statement)
|
|
119
|
+
statement = self._apply_optimizations(statement, exclusion_rules)
|
|
120
|
+
# statement = self._add_missing_table_alias_columns(statement)
|
|
121
|
+
|
|
122
|
+
if statement.find(exp.Values):
|
|
123
|
+
raise exception.InvalidQueryError(
|
|
124
|
+
f"VALUES() found in expression but should have been transformed: {statement.sql(self.query.dialect)}"
|
|
125
|
+
)
|
|
126
|
+
return statement
|
|
127
|
+
|
|
128
|
+
def _expand_to_query(self, statement: E) -> E:
|
|
129
|
+
"""
|
|
130
|
+
Expand Snowflake TABLE(TO_QUERY(SQL => '...')) into an inline subquery.
|
|
131
|
+
|
|
132
|
+
Transforms:
|
|
133
|
+
SELECT * FROM TABLE(TO_QUERY(SQL => 'SELECT * FROM source'))
|
|
134
|
+
Into:
|
|
135
|
+
SELECT * FROM (SELECT * FROM source)
|
|
136
|
+
"""
|
|
137
|
+
if self.query.dialect == "snowflake":
|
|
138
|
+
for table_from_rows in statement.find_all(exp.TableFromRows):
|
|
139
|
+
anon = table_from_rows.this
|
|
140
|
+
if not (isinstance(anon, exp.Anonymous) and anon.name.upper() == "TO_QUERY"):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Extract the SQL string - named arg (SQL => '...') or positional first arg
|
|
144
|
+
sql_str = None
|
|
145
|
+
for arg in anon.expressions:
|
|
146
|
+
if isinstance(arg, exp.Kwarg) and arg.this.name.upper() == "SQL":
|
|
147
|
+
sql_str = arg.expression.this # Literal.this gives the string value
|
|
148
|
+
break
|
|
149
|
+
if isinstance(arg, exp.Literal) and arg.is_string:
|
|
150
|
+
sql_str = arg.this
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
if sql_str is None:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# Parse the extracted SQL string
|
|
157
|
+
inner_query = sqlglot.parse_one(sql_str, dialect=self.query.dialect)
|
|
158
|
+
|
|
159
|
+
# Wrap as subquery and replace the TableFromRows
|
|
160
|
+
subquery = inner_query.subquery()
|
|
161
|
+
table_from_rows.replace(subquery)
|
|
162
|
+
|
|
163
|
+
return statement
|
|
164
|
+
|
|
165
|
+
@_validate_syntax
|
|
166
|
+
def _apply_udf_substitutions(self, statement: E) -> E:
|
|
167
|
+
"""
|
|
168
|
+
Replaces UDF call sites in the transformed statement with their inlined body.
|
|
169
|
+
Must run before _add_aliases_to_udfs so raw exp.Anonymous nodes are still present.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
Given a UDF defined as:
|
|
173
|
+
CREATE FUNCTION hello() RETURNS TEXT LANGUAGE SQL RETURN 'Hello';
|
|
174
|
+
|
|
175
|
+
And an INSERT statement:
|
|
176
|
+
INSERT INTO target (name) SELECT hello();
|
|
177
|
+
|
|
178
|
+
The call site `hello()` is replaced with the inlined UDF body, producing:
|
|
179
|
+
INSERT INTO target (name) SELECT (SELECT 'Hello');
|
|
180
|
+
"""
|
|
181
|
+
while True:
|
|
182
|
+
annotated = statement
|
|
183
|
+
node, matched_udf = udf.find_next_udf_call(annotated, self.query.object_mapping)
|
|
184
|
+
if not node:
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
target_node = udf.get_target_node(node)
|
|
188
|
+
replacement_exprs = udf.build_replacement_exprs(node, matched_udf)
|
|
189
|
+
if not replacement_exprs:
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
if len(replacement_exprs) > 1:
|
|
193
|
+
# Multi-statement UDF body: branch the entire query for each expression,
|
|
194
|
+
# then recursively process each branch to handle remaining UDF calls.
|
|
195
|
+
node_index = next((i for i, n in enumerate(annotated.walk()) if n is target_node), -1)
|
|
196
|
+
if node_index == -1:
|
|
197
|
+
break
|
|
198
|
+
final_results = []
|
|
199
|
+
for repl_expr in replacement_exprs:
|
|
200
|
+
new_statement = annotated.copy()
|
|
201
|
+
for i, n in enumerate(new_statement.walk()):
|
|
202
|
+
if i == node_index:
|
|
203
|
+
udf.apply_replacement(n, repl_expr, matched_udf)
|
|
204
|
+
break
|
|
205
|
+
# Recursively substitute any remaining UDF calls in this branch
|
|
206
|
+
substituted = self._apply_udf_substitutions(new_statement)
|
|
207
|
+
final_results.append(substituted)
|
|
208
|
+
|
|
209
|
+
# Return the last statement as the primary statement; for scalar UDFs the last
|
|
210
|
+
# statement is the return value. Others are discarded.
|
|
211
|
+
if final_results:
|
|
212
|
+
return final_results[-1]
|
|
213
|
+
break
|
|
214
|
+
udf.apply_replacement(target_node, replacement_exprs[0], matched_udf)
|
|
215
|
+
return statement
|
|
216
|
+
|
|
217
|
+
@_validate_syntax
|
|
218
|
+
def _convert_table_to_select(self, statement: E) -> E:
|
|
219
|
+
"""
|
|
220
|
+
Convert the statement "TABLE x" to "SELECT * FROM x"
|
|
221
|
+
"""
|
|
222
|
+
if self.query.dialect in ["mysql", "postgres"]:
|
|
223
|
+
source = statement.args.get("source", None)
|
|
224
|
+
if source:
|
|
225
|
+
table = source.pop()
|
|
226
|
+
statement.set("expression", exp.select("*").from_(table))
|
|
227
|
+
return statement
|
|
228
|
+
|
|
229
|
+
@_validate_syntax
|
|
230
|
+
def _add_aliases_to_udfs(self, statement: E) -> E:
|
|
231
|
+
"""
|
|
232
|
+
Iterate over the query looking for UDFs and add an alias to them with the same name
|
|
233
|
+
as the UDF if it doesn't already exist. This prevents sqlglot from adding its own
|
|
234
|
+
custom aliases (_0, _1, etc).
|
|
235
|
+
"""
|
|
236
|
+
# TODO: support other dialects; this logic is only for Postgres
|
|
237
|
+
query = self.query
|
|
238
|
+
for node in statement.find_all(exp.Anonymous):
|
|
239
|
+
udf_query = query.object_mapping.lookup_udf_call(node)
|
|
240
|
+
if not udf_query:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Get the name of the UDF to use as an alias
|
|
244
|
+
name = node.this if isinstance(node.this, str) else node.this.name
|
|
245
|
+
|
|
246
|
+
# The expression to be aliased
|
|
247
|
+
to_alias = node
|
|
248
|
+
if isinstance(node.parent, exp.Dot):
|
|
249
|
+
to_alias = node.parent
|
|
250
|
+
|
|
251
|
+
# If it's a Table's this (e.g. SELECT * FROM MY_UDF())
|
|
252
|
+
if isinstance(to_alias.parent, exp.Table):
|
|
253
|
+
table_node = to_alias.parent
|
|
254
|
+
if not table_node.alias:
|
|
255
|
+
table_node.set("alias", exp.TableAlias(this=exp.to_identifier(name)))
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
# If it's in a SELECT list and not already aliased
|
|
259
|
+
if isinstance(to_alias.parent, exp.Select) and to_alias in to_alias.parent.expressions:
|
|
260
|
+
# Wrap the expression in an Alias
|
|
261
|
+
to_alias.replace(exp.alias_(to_alias.copy(), name))
|
|
262
|
+
|
|
263
|
+
return statement
|
|
264
|
+
|
|
265
|
+
@_validate_syntax
|
|
266
|
+
def _add_aliases_to_pseudocolumns(self, statement: E) -> E:
|
|
267
|
+
"""
|
|
268
|
+
Given a query:
|
|
269
|
+
SELECT xmax FROM fruit.raw
|
|
270
|
+
rename it to:
|
|
271
|
+
SELECT raw.xmax FROM fruit.raw
|
|
272
|
+
or use the table's alias instead.
|
|
273
|
+
|
|
274
|
+
This requires that the tables and columns have run through qualify()
|
|
275
|
+
"""
|
|
276
|
+
for pseudo in statement.find_all(exp.Pseudocolumn):
|
|
277
|
+
if pseudo.table:
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
if pseudo.parent_select and pseudo.parent_select.args.get("from_"):
|
|
281
|
+
from_table_alias = pseudo.parent_select.args["from_"].alias_or_name
|
|
282
|
+
pseudo.set("table", exp.to_identifier(from_table_alias))
|
|
283
|
+
|
|
284
|
+
return statement
|
|
285
|
+
|
|
286
|
+
@_validate_syntax
|
|
287
|
+
def _process_inner_ctes(self, statement: E) -> E:
|
|
288
|
+
"""
|
|
289
|
+
Transform any inner CTE statements.
|
|
290
|
+
"""
|
|
291
|
+
if not isinstance(statement, (exp.Insert | exp.Merge | exp.Update | exp.Delete)):
|
|
292
|
+
return statement
|
|
293
|
+
|
|
294
|
+
for cte_expr in getattr(statement, "ctes", []):
|
|
295
|
+
if isinstance(cte_expr.this, exp.Update):
|
|
296
|
+
# Replace the inner UPDATE with an INSERT first.
|
|
297
|
+
# The inner query is different from the child query, which is its own separate copy.
|
|
298
|
+
inner_expr = self._convert_update_to_insert(cte_expr.this)
|
|
299
|
+
cte_expr.this.replace(inner_expr)
|
|
300
|
+
|
|
301
|
+
elif cte_expr.this.is_star:
|
|
302
|
+
# VALUES() has already been transformed into SELECT * FROM (VALUES())
|
|
303
|
+
from_ = cte_expr.this.args["from_"].this
|
|
304
|
+
|
|
305
|
+
values_node = None
|
|
306
|
+
if isinstance(from_, exp.Values):
|
|
307
|
+
values_node = from_
|
|
308
|
+
elif isinstance(from_, exp.Subquery) and isinstance(from_.this, exp.Values):
|
|
309
|
+
values_node = from_.this
|
|
310
|
+
|
|
311
|
+
if values_node is not None:
|
|
312
|
+
_rewrite_values_statement(self.query, expression=values_node, statement=cte_expr)
|
|
313
|
+
else:
|
|
314
|
+
normalize_values(self.query, cte_expr.this)
|
|
315
|
+
|
|
316
|
+
# Rename the columns and replace the INSERT with the SELECT
|
|
317
|
+
self._rename_returning_columns(statement=cte_expr, child_table=cte_expr.find(exp.Table))
|
|
318
|
+
|
|
319
|
+
return statement
|
|
320
|
+
|
|
321
|
+
@_validate_syntax
|
|
322
|
+
def _convert_insert_defaults_to_values(self, statement: exp.Insert) -> exp.Insert:
|
|
323
|
+
"""
|
|
324
|
+
Transform the query:
|
|
325
|
+
INSERT INTO x DEFAULT VALUES
|
|
326
|
+
into:
|
|
327
|
+
INSERT INTO x VALUES (DEFAULT, DEFAULT)
|
|
328
|
+
and then:
|
|
329
|
+
INSERT INTO x VALUES (NULL, 42)
|
|
330
|
+
according to the table's default column values.
|
|
331
|
+
|
|
332
|
+
The column names are also added from the mapping.
|
|
333
|
+
|
|
334
|
+
Promoted from InsertTransformer so it can run inside preprocess() before
|
|
335
|
+
the unified normalize_values() pass, for every exp.Insert statement.
|
|
336
|
+
"""
|
|
337
|
+
query = self.query
|
|
338
|
+
is_default_values = statement.args.get("default", False)
|
|
339
|
+
values = statement.expression
|
|
340
|
+
|
|
341
|
+
if not (isinstance(values, exp.Values) or is_default_values):
|
|
342
|
+
return statement
|
|
343
|
+
|
|
344
|
+
child_table = query.get_target_as_table()
|
|
345
|
+
table_query = query.object_mapping.lookup_table_query(table=child_table)
|
|
346
|
+
if not table_query:
|
|
347
|
+
return statement
|
|
348
|
+
|
|
349
|
+
table_columns = table_query.get_column_defs()
|
|
350
|
+
|
|
351
|
+
if is_default_values:
|
|
352
|
+
# Transform 'DEFAULT VALUES' into 'VALUES (DEFAULT,)'
|
|
353
|
+
values = exp.Values(expressions=[exp.Tuple(expressions=[exp.Var(this="DEFAULT") for _ in table_columns])])
|
|
354
|
+
statement.set("expression", values)
|
|
355
|
+
statement.set("default", False)
|
|
356
|
+
|
|
357
|
+
named_columns = util.get_selected_column_names(statement)
|
|
358
|
+
|
|
359
|
+
if not named_columns:
|
|
360
|
+
# Use the associated column names from the mapping
|
|
361
|
+
named_columns = list(table_columns)[: len(values.expressions[0].expressions)]
|
|
362
|
+
named_columns = [n.name for n in named_columns]
|
|
363
|
+
|
|
364
|
+
for value_expr in values.expressions:
|
|
365
|
+
if isinstance(value_expr, exp.Tuple):
|
|
366
|
+
for i, tuple_expr in enumerate(value_expr.expressions):
|
|
367
|
+
if isinstance(tuple_expr, exp.Var) and tuple_expr.name.upper() == "DEFAULT":
|
|
368
|
+
self._replace_default_with_value(
|
|
369
|
+
expression=tuple_expr,
|
|
370
|
+
column_name=named_columns[i],
|
|
371
|
+
table_columns=table_columns,
|
|
372
|
+
)
|
|
373
|
+
return statement
|
|
374
|
+
|
|
375
|
+
def _replace_default_with_value(
|
|
376
|
+
self,
|
|
377
|
+
expression: exp.Expr,
|
|
378
|
+
column_name: str,
|
|
379
|
+
table_columns: t.List[exp.ColumnDef],
|
|
380
|
+
) -> None:
|
|
381
|
+
"""
|
|
382
|
+
Replace a 'DEFAULT' expression with the column's default value or NULL.
|
|
383
|
+
"""
|
|
384
|
+
col_def = [col for col in table_columns if col.name == column_name]
|
|
385
|
+
if col_def:
|
|
386
|
+
col_def = col_def[0]
|
|
387
|
+
if default_constraint := util.get_column_constraint_expression(col_def):
|
|
388
|
+
expression.replace(default_constraint.this.copy())
|
|
389
|
+
else:
|
|
390
|
+
expression.replace(exp.Null())
|
|
391
|
+
|
|
392
|
+
def _optimizer_rules(self, exclude_rules: t.List[str]):
|
|
393
|
+
"""
|
|
394
|
+
Return a list of sqlglot.optimizer rules to use.
|
|
395
|
+
"""
|
|
396
|
+
return [r for r in RULES if getattr(r, "__name__", None) not in exclude_rules]
|
|
397
|
+
|
|
398
|
+
@_validate_syntax
|
|
399
|
+
def _qualify_function_columns(self, statement: E) -> E:
|
|
400
|
+
"""
|
|
401
|
+
Look for table functions used in FROM whose names are known
|
|
402
|
+
and ensure they have a TableAlias with the correct default column list.
|
|
403
|
+
|
|
404
|
+
This enables sqlglot.qualify() to expand '*' based on alias columns without
|
|
405
|
+
rewriting the select list ourselves.
|
|
406
|
+
|
|
407
|
+
For example,
|
|
408
|
+
SELECT * FROM JSON_EACH_TEXT()
|
|
409
|
+
->
|
|
410
|
+
SELECT * FROM JSON_EACH_TEXT() AS JSON_EACH_TEXT(key, value)
|
|
411
|
+
"""
|
|
412
|
+
fn_columns_map: dict[str, t.List[str]] = get_system_functions(self.query.dialect)
|
|
413
|
+
if not fn_columns_map:
|
|
414
|
+
return statement
|
|
415
|
+
|
|
416
|
+
# Iterate over SELECT statements and inspect their FROM/JOIN clauses
|
|
417
|
+
for select in statement.find_all(exp.Select):
|
|
418
|
+
from_clause = select.args.get("from_") or select.args.get("from")
|
|
419
|
+
if not from_clause:
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
# Collect tables nodes (e.g. FROM.this and any nested ones in JOIN/LATERAL)
|
|
423
|
+
tables: list[exp.Table] = []
|
|
424
|
+
root_tbl = getattr(from_clause, "this", None)
|
|
425
|
+
if isinstance(root_tbl, exp.Table):
|
|
426
|
+
tables.append(root_tbl)
|
|
427
|
+
tables.extend(list(from_clause.find_all(exp.Table)))
|
|
428
|
+
|
|
429
|
+
for tbl in tables:
|
|
430
|
+
this_expr = tbl.this
|
|
431
|
+
|
|
432
|
+
# Existing behavior for other table functions represented as Anonymous
|
|
433
|
+
if not isinstance(this_expr, exp.Anonymous):
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
function_name = str(this_expr.this).lower()
|
|
437
|
+
cols = fn_columns_map.get(function_name)
|
|
438
|
+
if not cols:
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
# Ensure there is a TableAlias with the correct column list
|
|
442
|
+
alias: exp.TableAlias | None = tbl.args.get("alias")
|
|
443
|
+
if alias is None:
|
|
444
|
+
tbl.set(
|
|
445
|
+
"alias",
|
|
446
|
+
exp.TableAlias(
|
|
447
|
+
this=exp.to_identifier(function_name),
|
|
448
|
+
columns=[exp.to_identifier(c) for c in cols],
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
elif not alias.columns:
|
|
452
|
+
alias.set("columns", [exp.to_identifier(c) for c in cols])
|
|
453
|
+
|
|
454
|
+
return statement
|
|
455
|
+
|
|
456
|
+
def _get_validation_and_exclusion_rules(self, statement: E) -> t.Tuple[bool, t.List[str]]:
|
|
457
|
+
"""
|
|
458
|
+
Determine some settings used in the main transformer functions.
|
|
459
|
+
"""
|
|
460
|
+
validate_columns = True
|
|
461
|
+
exclude_rules = EXCLUDE_OPTIMIZER_RULES[:]
|
|
462
|
+
|
|
463
|
+
# Do not validate the columns if the source is a non-table
|
|
464
|
+
if not self.query.source_info or SqlObjectType.type_has_no_column_defs(self.query.source_info.type):
|
|
465
|
+
validate_columns = False
|
|
466
|
+
|
|
467
|
+
# Do not validate columns for SELECT statements with no FROM clause (e.g. UDF body inner queries
|
|
468
|
+
# that have table-qualified columns from parameter substitution but no actual FROM tables).
|
|
469
|
+
# For example, after substitution of 'MY_UDF(people.*)' with 'MY_UDF = SELECT $1.age AS age'
|
|
470
|
+
if isinstance(statement, exp.Select) and not statement.args.get("from"):
|
|
471
|
+
validate_columns = False
|
|
472
|
+
|
|
473
|
+
if not validate_columns:
|
|
474
|
+
# Prevent overwriting known types to 'UNKNOWN' (sqlglot can't resolve non-table sources)
|
|
475
|
+
# This occurs when we've already added the types after the conversion from COPY into INSERT
|
|
476
|
+
exclude_rules += ["annotate_types"]
|
|
477
|
+
|
|
478
|
+
return validate_columns, exclude_rules
|
|
479
|
+
|
|
480
|
+
@_validate_syntax
|
|
481
|
+
def _apply_optimizations(self, statement: E, exclusion_rules: t.List[str]) -> E:
|
|
482
|
+
"""
|
|
483
|
+
Call sqlglot's optimization functions, which simplify a range of expressions.
|
|
484
|
+
"""
|
|
485
|
+
statement = optimize(
|
|
486
|
+
expression=statement,
|
|
487
|
+
dialect=self.query.dialect,
|
|
488
|
+
schema=self.query.object_mapping,
|
|
489
|
+
rules=self._optimizer_rules(exclusion_rules),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# We don't want to merge the CTEs as they provide useful info to the user
|
|
493
|
+
# so we skip merge_ctes() but call its sibling function below directly instead
|
|
494
|
+
statement = merge_derived_tables(statement)
|
|
495
|
+
|
|
496
|
+
# Add surrounding parens to some expressions (optimize() removes them)
|
|
497
|
+
statement = add_parens_for_composite_field_access(statement)
|
|
498
|
+
|
|
499
|
+
return statement
|
|
500
|
+
|
|
501
|
+
@_validate_syntax
|
|
502
|
+
def _apply_qualify(self, statement: E, validate_columns: bool) -> E:
|
|
503
|
+
"""
|
|
504
|
+
Run sqlglot's qualify() function against the statement for schema validation and
|
|
505
|
+
naming/aliasing population.
|
|
506
|
+
|
|
507
|
+
We pass infer_schema=True to source unqualified columns from the source table (if missing from `schema` param)
|
|
508
|
+
so that e.g. `INSERT INTO my.other SELECT name FROM my.table` produces `my.table.name -> my.other.name`
|
|
509
|
+
"""
|
|
510
|
+
qualify.qualify(
|
|
511
|
+
statement,
|
|
512
|
+
schema=self.query.object_mapping,
|
|
513
|
+
infer_schema=True,
|
|
514
|
+
dialect=self.query.dialect,
|
|
515
|
+
isolate_tables=False,
|
|
516
|
+
validate_qualify_columns=validate_columns,
|
|
517
|
+
quote_identifiers=False,
|
|
518
|
+
)
|
|
519
|
+
return statement
|
|
520
|
+
|
|
521
|
+
@_validate_syntax
|
|
522
|
+
def _add_missing_table_alias_columns(self, statement: E) -> E:
|
|
523
|
+
"""
|
|
524
|
+
Populate any missing column lists on exp.TableAlias nodes by copying the
|
|
525
|
+
output column names from the aliased expression (typically a Subquery).
|
|
526
|
+
|
|
527
|
+
Example:
|
|
528
|
+
FROM (SELECT 'x' AS column1, UPPER('y') AS column2) AS v
|
|
529
|
+
becomes:
|
|
530
|
+
FROM (SELECT 'x' AS column1, UPPER('y') AS column2) AS v(column1, column2)
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
def _in_from_join_lateral(node: exp.Expr | None) -> bool:
|
|
534
|
+
"""Return True if the node appears in a table position (FROM/JOIN/LATERAL).
|
|
535
|
+
|
|
536
|
+
Scalar/value subqueries should return False so no alias column list is attached there.
|
|
537
|
+
"""
|
|
538
|
+
p = node
|
|
539
|
+
# Walk up until we hit a boundary that tells us our role
|
|
540
|
+
while p is not None and not isinstance(p, (exp.Select, exp.From, exp.Join, exp.Lateral)):
|
|
541
|
+
p = p.parent
|
|
542
|
+
return isinstance(p, (exp.From, exp.Join, exp.Lateral))
|
|
543
|
+
|
|
544
|
+
def _resolve_representative_select(inner: exp.Expr | None) -> exp.Select | None:
|
|
545
|
+
"""Given an inner derived expression, return a Select to read output names from.
|
|
546
|
+
|
|
547
|
+
Handles Select, Subquery(Select/Union), and Union.
|
|
548
|
+
"""
|
|
549
|
+
if inner is None:
|
|
550
|
+
return None
|
|
551
|
+
if isinstance(inner, exp.Select):
|
|
552
|
+
return inner
|
|
553
|
+
if isinstance(inner, exp.Subquery):
|
|
554
|
+
return _resolve_representative_select(inner.this)
|
|
555
|
+
if isinstance(inner, exp.Union):
|
|
556
|
+
return inner.find(exp.Select)
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
for tbl_alias in statement.find_all(exp.TableAlias):
|
|
560
|
+
parent = tbl_alias.parent
|
|
561
|
+
|
|
562
|
+
# Identify derived-table hosts we can read columns from
|
|
563
|
+
inner: exp.Expr | None = None
|
|
564
|
+
if isinstance(parent, exp.Subquery) and _in_from_join_lateral(parent):
|
|
565
|
+
inner = parent.this
|
|
566
|
+
elif isinstance(parent, exp.Lateral) and _in_from_join_lateral(parent):
|
|
567
|
+
inner = parent.this
|
|
568
|
+
else:
|
|
569
|
+
# Not a derived table alias in FROM/JOIN/LATERAL position
|
|
570
|
+
continue
|
|
571
|
+
|
|
572
|
+
# Determine a representative SELECT to read output names from
|
|
573
|
+
sel = _resolve_representative_select(inner)
|
|
574
|
+
if not sel:
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
projected_column_names = [s.alias_or_name for s in sel.selects]
|
|
578
|
+
|
|
579
|
+
# Skip if columns already present; only append truly missing ones
|
|
580
|
+
existing_cols = tbl_alias.columns
|
|
581
|
+
existing_column_names = {str(c.name) for c in existing_cols}
|
|
582
|
+
missing = [n for n in projected_column_names if n and n not in existing_column_names]
|
|
583
|
+
if not missing:
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
logger.debug(f"Adding missing columns to table alias {tbl_alias.name}: {missing}")
|
|
587
|
+
# Append missing identifiers to the alias column list
|
|
588
|
+
new_cols = existing_cols + [exp.to_identifier(n) for n in missing]
|
|
589
|
+
tbl_alias.set("columns", new_cols)
|
|
590
|
+
|
|
591
|
+
return statement
|
|
592
|
+
|
|
593
|
+
@_validate_syntax
|
|
594
|
+
def _rename_returning_columns(self, statement: exp.CTE, child_table: exp.Table) -> exp.CTE:
|
|
595
|
+
"""
|
|
596
|
+
Given an (INSERT .. RETURNING *) statement, expand the star to the table's column names
|
|
597
|
+
and add the correct column aliases.
|
|
598
|
+
|
|
599
|
+
For example, the query:
|
|
600
|
+
INSERT INTO fruit.raw (name)
|
|
601
|
+
SELECT 'orange' AS name
|
|
602
|
+
RETURNING UPPER(name)
|
|
603
|
+
|
|
604
|
+
is rewritten to:
|
|
605
|
+
SELECT UPPER(name)
|
|
606
|
+
FROM fruit.raw
|
|
607
|
+
|
|
608
|
+
Note that:
|
|
609
|
+
MERGE RETURNING * returns all columns from source and target
|
|
610
|
+
UPDATE RETURNING * returns all columns from target
|
|
611
|
+
INSERT RETURNING * returns all columns from target
|
|
612
|
+
DELETE RETURNING * returns all columns from target
|
|
613
|
+
"""
|
|
614
|
+
returning_expr: exp.Returning = statement.this.args.get("returning", None)
|
|
615
|
+
if not returning_expr:
|
|
616
|
+
return statement
|
|
617
|
+
|
|
618
|
+
for col_expr in returning_expr.expressions:
|
|
619
|
+
if not isinstance(col_expr, (exp.Alias, exp.Column, exp.Star)):
|
|
620
|
+
message = (
|
|
621
|
+
f"Non-column expression ({col_expr}) must have an alias inside RETURNING to prevent ambiguity."
|
|
622
|
+
)
|
|
623
|
+
raise exception.SqlLeafException(message=message)
|
|
624
|
+
|
|
625
|
+
# Replace the OLD & NEW aliases with the table alias if it exists. Otherwise, remove it to be valid.
|
|
626
|
+
returning_columns = list(returning_expr.find_all(exp.Column))
|
|
627
|
+
for col in returning_columns:
|
|
628
|
+
if col.table.lower() in ["old", "new"]:
|
|
629
|
+
if child_table.alias:
|
|
630
|
+
col.set("table", exp.to_identifier(child_table.alias, quoted=False))
|
|
631
|
+
else:
|
|
632
|
+
col.args["table"].pop()
|
|
633
|
+
if isinstance(col.this, exp.Star):
|
|
634
|
+
# optimize() needs Star(), not Column(Star())
|
|
635
|
+
col.replace(col.this)
|
|
636
|
+
|
|
637
|
+
if isinstance(statement.this, exp.Merge):
|
|
638
|
+
using = statement.this.args["using"]
|
|
639
|
+
on = statement.this.args["on"]
|
|
640
|
+
new_select = exp.select(*returning_expr.expressions).from_(child_table).join(using, on=on)
|
|
641
|
+
else:
|
|
642
|
+
new_select = exp.select(*returning_expr.expressions).from_(child_table)
|
|
643
|
+
|
|
644
|
+
statement.set("this", new_select)
|
|
645
|
+
return statement
|
|
646
|
+
|
|
647
|
+
@staticmethod
|
|
648
|
+
def _extract_merge_context(statement: exp.Insert | exp.Update) -> dict | None:
|
|
649
|
+
"""
|
|
650
|
+
Extract the shared preamble needed by both INSERT and UPDATE branches of
|
|
651
|
+
_add_information_from_merge. Returns None if the statement is not nested
|
|
652
|
+
inside a MERGE expression.
|
|
653
|
+
"""
|
|
654
|
+
merge_expr = statement.find_ancestor(exp.Merge)
|
|
655
|
+
if not merge_expr:
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
ctes = merge_expr.args["with_"].expressions if "with_" in merge_expr.args else []
|
|
659
|
+
return {
|
|
660
|
+
"merge_expr": merge_expr,
|
|
661
|
+
"using": merge_expr.args["using"],
|
|
662
|
+
"on": merge_expr.args["on"],
|
|
663
|
+
"returning": merge_expr.args.get("returning"),
|
|
664
|
+
"ctes": ctes,
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
@staticmethod
|
|
668
|
+
def _extract_multitable_insert_context(statement: exp.Insert) -> dict | None:
|
|
669
|
+
"""
|
|
670
|
+
Extract the shared source from the parent MultitableInserts statement.
|
|
671
|
+
"""
|
|
672
|
+
multitable_insert = statement.find_ancestor(exp.MultitableInserts)
|
|
673
|
+
if not multitable_insert:
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
source = multitable_insert.args.get("source")
|
|
677
|
+
return {
|
|
678
|
+
"source": source,
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
@_validate_syntax
|
|
682
|
+
def _add_column_names_to_insert(self, statement: exp.Expr) -> exp.Insert:
|
|
683
|
+
"""
|
|
684
|
+
Add aliases to SELECTs that are missing them by looking at the corresponding INSERT column.
|
|
685
|
+
This prevents sqlglot from assigning its own generated names as aliases.
|
|
686
|
+
This needs to run after qualify() as it expands stars and aliases.
|
|
687
|
+
|
|
688
|
+
For example, given table:
|
|
689
|
+
CREATE TABLE my.apple (a VARCHAR, b VARCHAR);
|
|
690
|
+
the statement:
|
|
691
|
+
INSERT INTO my.apple SELECT name, age FROM my.pear
|
|
692
|
+
renames to:
|
|
693
|
+
INSERT INTO my.apple (a,b) SELECT name as a, age as b FROM my.pear
|
|
694
|
+
"""
|
|
695
|
+
query = self.query
|
|
696
|
+
if not isinstance(statement, exp.Insert) or not statement.selects:
|
|
697
|
+
return statement
|
|
698
|
+
|
|
699
|
+
# sqlglot throws a parse error on named columns for Snowflake: INSERT INTO @"my_eXt_sTaGe" (NAME, AGE) SELECT ...
|
|
700
|
+
SKIP_COLUMN_RENAME_TYPES = SqlObjectType.type_has_no_column_defs(query.source_info.type, query.target_info.type)
|
|
701
|
+
if SKIP_COLUMN_RENAME_TYPES:
|
|
702
|
+
# The aliases and column names already exist from a previous transformation,
|
|
703
|
+
# or the target is not a table (e.g. S3 file, stage, stream)
|
|
704
|
+
return statement
|
|
705
|
+
|
|
706
|
+
child_table = query.get_target_as_table()
|
|
707
|
+
table_query = query.object_mapping.get_table_or_stage(child_table)
|
|
708
|
+
if not table_query:
|
|
709
|
+
raise exception.SqlLeafException(message=f"Unknown target table: {str(exp.table_name(child_table))}")
|
|
710
|
+
|
|
711
|
+
table_columns = [c.name for c in table_query.get_column_defs(include_system=True)]
|
|
712
|
+
selects = statement.selects
|
|
713
|
+
insert_columns = self._extract_insert_columns(statement, child_table, include_system=True)
|
|
714
|
+
|
|
715
|
+
# Add the column names from the mapping to the INSERT's column names
|
|
716
|
+
insert_columns = list(insert_columns)[: len(selects)]
|
|
717
|
+
schema = exp.Schema(this=child_table, expressions=[exp.to_identifier(c) for c in insert_columns])
|
|
718
|
+
statement.set("this", schema)
|
|
719
|
+
|
|
720
|
+
unknown_columns = [col for col in insert_columns if col not in table_columns]
|
|
721
|
+
if unknown_columns:
|
|
722
|
+
raise exception.MappingError(
|
|
723
|
+
message=f"Unknown columns used in SELECT: {list(unknown_columns)}",
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
if exp.Star() in selects:
|
|
727
|
+
exception.raise_error(exception.InvalidQueryError,
|
|
728
|
+
message=f"Statement has unresolved star column: {statement.sql(dialect=query.dialect)}",
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
if len(insert_columns) != len(statement.selects):
|
|
732
|
+
message = "Mismatched column count: number of column names (%s) does not match selected columns (%s)" % (
|
|
733
|
+
len(insert_columns),
|
|
734
|
+
len(statement.selects),
|
|
735
|
+
)
|
|
736
|
+
raise exception.InvalidQueryError(message=message)
|
|
737
|
+
|
|
738
|
+
aliases = util.get_selected_column_names(statement)
|
|
739
|
+
if aliases != insert_columns:
|
|
740
|
+
message = "Mismatched column names: column names (%s) do not match column aliases (%s)" % (
|
|
741
|
+
",".join(insert_columns),
|
|
742
|
+
",".join(aliases),
|
|
743
|
+
)
|
|
744
|
+
# logger.warning(message)
|
|
745
|
+
|
|
746
|
+
# Collect every SELECT expression
|
|
747
|
+
set_operation = statement.args.get("expression")
|
|
748
|
+
if isinstance(set_operation, exp.SetOperation):
|
|
749
|
+
selects = list(set_operation.iter_expressions())
|
|
750
|
+
else:
|
|
751
|
+
selects = [statement]
|
|
752
|
+
|
|
753
|
+
for select in selects:
|
|
754
|
+
for i, ins in enumerate(insert_columns):
|
|
755
|
+
# Overwrite the aliases because sqlglot may have added incorrect ones
|
|
756
|
+
select.selects[i] = select.selects[i].as_(ins)
|
|
757
|
+
|
|
758
|
+
return statement
|
|
759
|
+
|
|
760
|
+
def _extract_insert_columns(
|
|
761
|
+
self, statement: exp.Insert, target: exp.Table | exp.Schema, include_system: bool = False
|
|
762
|
+
) -> t.List[str]:
|
|
763
|
+
"""
|
|
764
|
+
Returns a list of column names for an exp.Insert statement.
|
|
765
|
+
If they are not found, fall back to the columns for table `target`.
|
|
766
|
+
"""
|
|
767
|
+
if isinstance(statement.this, exp.Schema):
|
|
768
|
+
return [s.name for s in statement.this.expressions]
|
|
769
|
+
|
|
770
|
+
columns = []
|
|
771
|
+
if isinstance(statement.this, exp.Table):
|
|
772
|
+
columns = statement.this.alias_column_names
|
|
773
|
+
elif isinstance(statement.this, exp.Tuple):
|
|
774
|
+
columns = [s.alias_or_name for s in statement.this.expressions]
|
|
775
|
+
|
|
776
|
+
if columns:
|
|
777
|
+
return columns
|
|
778
|
+
|
|
779
|
+
# Fall back to the table's definition in the mapping
|
|
780
|
+
table_query = self.query.object_mapping.get_table_or_stage(target)
|
|
781
|
+
if not table_query:
|
|
782
|
+
return []
|
|
783
|
+
|
|
784
|
+
return [c.name for c in table_query.get_column_defs(include_system=include_system)]
|
|
785
|
+
|
|
786
|
+
@_validate_syntax
|
|
787
|
+
def _convert_update_to_insert(self, statement: exp.Update) -> exp.Insert:
|
|
788
|
+
"""
|
|
789
|
+
Taken from extract_select_from_update() at datahub/metadata-ingestion/src/datahub/sql_parsing/sqlglotlineage.py
|
|
790
|
+
|
|
791
|
+
This transforms an UPDATE statement into an INSERT statement so that it can be processed by the lineage functions.
|
|
792
|
+
Subclasses (UpdateTransformer) may override this with a more specific implementation.
|
|
793
|
+
"""
|
|
794
|
+
query = self.query
|
|
795
|
+
_UPDATE_FROM_TABLE_ARGS_TO_MOVE = {"joins", "laterals", "pivot"}
|
|
796
|
+
_UPDATE_ARGS_NOT_SUPPORTED_BY_SELECT: t.Set[str] = set(exp.Update.arg_types.keys()) - set(
|
|
797
|
+
exp.Select.arg_types.keys()
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
if where := statement.args.get("where", None):
|
|
801
|
+
# WHERE statements aren't relevant to lineage
|
|
802
|
+
where.pop()
|
|
803
|
+
|
|
804
|
+
# The "SET" expressions need to be converted.
|
|
805
|
+
# For the update command, it'll be a list of EQ expressions, but the select
|
|
806
|
+
# should contain aliased columns.
|
|
807
|
+
alias_names = []
|
|
808
|
+
new_expressions = []
|
|
809
|
+
subquery_from = None
|
|
810
|
+
for expr in statement.expressions:
|
|
811
|
+
if isinstance(expr, exp.EQ):
|
|
812
|
+
if isinstance(expr.left, exp.Paren) and isinstance(expr.right, exp.Subquery):
|
|
813
|
+
# UPDATE x SET (a) = (SELECT a FROM ...)
|
|
814
|
+
column_names = [expr.left.this]
|
|
815
|
+
subquery = expr.right.this # the inner SELECT
|
|
816
|
+
column_values = []
|
|
817
|
+
for col_expr in subquery.expressions:
|
|
818
|
+
column_values.append(col_expr.unalias())
|
|
819
|
+
subquery_from = subquery.args.get("from_")
|
|
820
|
+
if isinstance(expr.left, exp.Tuple) and isinstance(expr.right, exp.Subquery):
|
|
821
|
+
# UPDATE x SET (a,b) = (SELECT a, b FROM ...)
|
|
822
|
+
column_names = expr.left.expressions
|
|
823
|
+
subquery = expr.right.this # the inner SELECT
|
|
824
|
+
column_values = []
|
|
825
|
+
for col_expr in subquery.expressions:
|
|
826
|
+
column_values.append(col_expr.unalias())
|
|
827
|
+
subquery_from = subquery.args.get("from_")
|
|
828
|
+
|
|
829
|
+
elif isinstance(expr.left, exp.Tuple):
|
|
830
|
+
# UPDATE x SET (a,b) = (1,2)
|
|
831
|
+
column_names = expr.left.expressions
|
|
832
|
+
column_values = expr.right.expressions
|
|
833
|
+
|
|
834
|
+
elif isinstance(expr.left, exp.Column):
|
|
835
|
+
# UPDATE x SET a = 1
|
|
836
|
+
column_names = [expr.left]
|
|
837
|
+
column_values = [expr.right]
|
|
838
|
+
|
|
839
|
+
new_columns = zip(column_names, column_values)
|
|
840
|
+
for name_expr, value_expr in new_columns:
|
|
841
|
+
alias_names.append(name_expr.this)
|
|
842
|
+
new_expressions.append(
|
|
843
|
+
exp.Alias(
|
|
844
|
+
this=value_expr,
|
|
845
|
+
alias=name_expr.this,
|
|
846
|
+
)
|
|
847
|
+
)
|
|
848
|
+
else:
|
|
849
|
+
# If we don't know how to convert it, just leave it as-is. If this causes issues,
|
|
850
|
+
# they'll get caught later.
|
|
851
|
+
new_expressions.append(expr)
|
|
852
|
+
|
|
853
|
+
# Special translation for the `from` clause.
|
|
854
|
+
extra_args: dict = {}
|
|
855
|
+
from_table = statement.args.get("from_")
|
|
856
|
+
if from_table and isinstance(from_table, exp.Table):
|
|
857
|
+
from_table = from_table.this
|
|
858
|
+
# Move joins, laterals, and pivots from the Update->From->Table->field
|
|
859
|
+
# to the top-level Select->field.
|
|
860
|
+
|
|
861
|
+
for k in _UPDATE_FROM_TABLE_ARGS_TO_MOVE:
|
|
862
|
+
if k in from_table.args:
|
|
863
|
+
# Mutate the from table clause in-place.
|
|
864
|
+
extra_args[k] = from_table.args.get(k)
|
|
865
|
+
from_table.set(k, None)
|
|
866
|
+
|
|
867
|
+
# We need to add the CTEs to the insert, not as part of the select.
|
|
868
|
+
# Otherwise the query will be ordered incorrectly (i.e. INSERT .. WITH () .. SELECT)
|
|
869
|
+
with_ = statement.args.get("with_", None)
|
|
870
|
+
if with_:
|
|
871
|
+
with_.pop()
|
|
872
|
+
|
|
873
|
+
select_statement = exp.Select(**{
|
|
874
|
+
**{k: v for k, v in statement.args.items() if k not in _UPDATE_ARGS_NOT_SUPPORTED_BY_SELECT},
|
|
875
|
+
**extra_args,
|
|
876
|
+
"expressions": new_expressions,
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
into_table = util.get_table(statement)
|
|
880
|
+
if subquery_from:
|
|
881
|
+
# If the SET used a subquery, use its FROM clause
|
|
882
|
+
select_statement = select_statement.from_(subquery_from.this)
|
|
883
|
+
elif not from_table:
|
|
884
|
+
# If the table is a self-reference
|
|
885
|
+
select_statement = select_statement.from_(into_table)
|
|
886
|
+
|
|
887
|
+
# Convert the statement into an insert
|
|
888
|
+
insert_expr = exp.insert(
|
|
889
|
+
expression=select_statement,
|
|
890
|
+
columns=alias_names,
|
|
891
|
+
into=into_table,
|
|
892
|
+
returning=statement.args.get("returning", None),
|
|
893
|
+
dialect=query.dialect,
|
|
894
|
+
)
|
|
895
|
+
if with_:
|
|
896
|
+
insert_expr.set("with_", with_)
|
|
897
|
+
|
|
898
|
+
statement.replace(insert_expr)
|
|
899
|
+
return insert_expr
|