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.
Files changed (111) hide show
  1. sqlleaf/__init__.py +4 -0
  2. sqlleaf/dialects/__init__.py +0 -0
  3. sqlleaf/dialects/plpgsql/__init__.py +0 -0
  4. sqlleaf/dialects/plpgsql/p_classes.py +275 -0
  5. sqlleaf/dialects/plpgsql/p_dialect.py +148 -0
  6. sqlleaf/dialects/plpgsql/p_generator.py +538 -0
  7. sqlleaf/dialects/plpgsql/p_parser.py +1244 -0
  8. sqlleaf/dialects/plpgsql/p_tokenizer.py +41 -0
  9. sqlleaf/exception.py +67 -0
  10. sqlleaf/lineage.py +326 -0
  11. sqlleaf/mappings.py +376 -0
  12. sqlleaf/models/__init__.py +0 -0
  13. sqlleaf/models/context.py +153 -0
  14. sqlleaf/models/hook.py +9 -0
  15. sqlleaf/models/node/__init__.py +27 -0
  16. sqlleaf/models/node/base.py +148 -0
  17. sqlleaf/models/node/column.py +345 -0
  18. sqlleaf/models/node/dynamodb.py +39 -0
  19. sqlleaf/models/node/function.py +33 -0
  20. sqlleaf/models/node/interval.py +21 -0
  21. sqlleaf/models/node/json.py +57 -0
  22. sqlleaf/models/node/literal.py +17 -0
  23. sqlleaf/models/node/null.py +14 -0
  24. sqlleaf/models/node/pivot.py +34 -0
  25. sqlleaf/models/node/program.py +27 -0
  26. sqlleaf/models/node/sequence.py +18 -0
  27. sqlleaf/models/node/star.py +11 -0
  28. sqlleaf/models/node/stream.py +14 -0
  29. sqlleaf/models/node/user_defined_function.py +33 -0
  30. sqlleaf/models/node/var.py +7 -0
  31. sqlleaf/models/node/variable.py +8 -0
  32. sqlleaf/models/node/window.py +22 -0
  33. sqlleaf/models/query/__init__.py +33 -0
  34. sqlleaf/models/query/base.py +248 -0
  35. sqlleaf/models/query/call.py +111 -0
  36. sqlleaf/models/query/copy.py +144 -0
  37. sqlleaf/models/query/ctas.py +61 -0
  38. sqlleaf/models/query/database.py +48 -0
  39. sqlleaf/models/query/delete.py +26 -0
  40. sqlleaf/models/query/execute.py +66 -0
  41. sqlleaf/models/query/holder.py +49 -0
  42. sqlleaf/models/query/insert.py +69 -0
  43. sqlleaf/models/query/merge.py +29 -0
  44. sqlleaf/models/query/multitable_insert.py +42 -0
  45. sqlleaf/models/query/prepare.py +61 -0
  46. sqlleaf/models/query/procedure.py +72 -0
  47. sqlleaf/models/query/put.py +38 -0
  48. sqlleaf/models/query/replace.py +32 -0
  49. sqlleaf/models/query/schema.py +47 -0
  50. sqlleaf/models/query/select.py +27 -0
  51. sqlleaf/models/query/sequence.py +33 -0
  52. sqlleaf/models/query/set.py +74 -0
  53. sqlleaf/models/query/stage.py +78 -0
  54. sqlleaf/models/query/table.py +220 -0
  55. sqlleaf/models/query/trigger.py +42 -0
  56. sqlleaf/models/query/type.py +42 -0
  57. sqlleaf/models/query/unload.py +64 -0
  58. sqlleaf/models/query/update.py +45 -0
  59. sqlleaf/models/query/user_defined_function.py +297 -0
  60. sqlleaf/models/query/values.py +20 -0
  61. sqlleaf/models/query/view.py +41 -0
  62. sqlleaf/path.py +194 -0
  63. sqlleaf/processors/__init__.py +0 -0
  64. sqlleaf/processors/collector/__init__.py +0 -0
  65. sqlleaf/processors/collector/collector.py +827 -0
  66. sqlleaf/processors/collector/substitute.py +396 -0
  67. sqlleaf/processors/generator/__init__.py +0 -0
  68. sqlleaf/processors/generator/dialects/__init__.py +5 -0
  69. sqlleaf/processors/generator/dialects/athena.py +45 -0
  70. sqlleaf/processors/generator/dialects/base.py +572 -0
  71. sqlleaf/processors/generator/dialects/mysql.py +19 -0
  72. sqlleaf/processors/generator/dialects/postgres.py +182 -0
  73. sqlleaf/processors/generator/dialects/redshift.py +192 -0
  74. sqlleaf/processors/generator/dialects/snowflake.py +90 -0
  75. sqlleaf/processors/generator/generator.py +495 -0
  76. sqlleaf/processors/transformer/__init__.py +31 -0
  77. sqlleaf/processors/transformer/base.py +899 -0
  78. sqlleaf/processors/transformer/call.py +9 -0
  79. sqlleaf/processors/transformer/copy.py +63 -0
  80. sqlleaf/processors/transformer/ctas.py +39 -0
  81. sqlleaf/processors/transformer/delete.py +14 -0
  82. sqlleaf/processors/transformer/execute.py +9 -0
  83. sqlleaf/processors/transformer/expressions/__init__.py +11 -0
  84. sqlleaf/processors/transformer/expressions/functional_notation.py +22 -0
  85. sqlleaf/processors/transformer/expressions/row.py +451 -0
  86. sqlleaf/processors/transformer/expressions/values.py +318 -0
  87. sqlleaf/processors/transformer/insert.py +125 -0
  88. sqlleaf/processors/transformer/merge.py +14 -0
  89. sqlleaf/processors/transformer/multitable_insert.py +14 -0
  90. sqlleaf/processors/transformer/replace.py +23 -0
  91. sqlleaf/processors/transformer/transformer.py +141 -0
  92. sqlleaf/processors/transformer/udf.py +235 -0
  93. sqlleaf/processors/transformer/unload.py +30 -0
  94. sqlleaf/processors/transformer/update.py +174 -0
  95. sqlleaf/processors/transformer/values.py +10 -0
  96. sqlleaf/settings/__init__.py +19 -0
  97. sqlleaf/settings/athena.py +23 -0
  98. sqlleaf/settings/base.py +37 -0
  99. sqlleaf/settings/postgres.py +52 -0
  100. sqlleaf/system_tables/load.py +0 -0
  101. sqlleaf/typing.py +93 -0
  102. sqlleaf/util/__init__.py +34 -0
  103. sqlleaf/util/expression.py +282 -0
  104. sqlleaf/util/graph.py +116 -0
  105. sqlleaf/util/helpers.py +42 -0
  106. sqlleaf/util/iterators.py +37 -0
  107. sqlleaf-0.0.1.dist-info/METADATA +46 -0
  108. sqlleaf-0.0.1.dist-info/RECORD +111 -0
  109. sqlleaf-0.0.1.dist-info/WHEEL +5 -0
  110. sqlleaf-0.0.1.dist-info/licenses/LICENSE +21 -0
  111. 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