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,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