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,827 @@
1
+ import logging
2
+ import typing as t
3
+ from collections import Counter
4
+ from dataclasses import dataclass, field
5
+
6
+ import sqlglot
7
+ from sqlglot import TokenType, exp
8
+ from sqlglot.expressions import ColumnDef
9
+ from sqlleaf.processors.transformer import udf
10
+ from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
11
+
12
+ from sqlleaf import exception, mappings, settings, util, typing
13
+ from sqlleaf.models.query import (
14
+ CallQuery,
15
+ CopyQuery,
16
+ CTASQuery,
17
+ DatabaseQuery,
18
+ DeleteQuery,
19
+ ExecuteQuery,
20
+ InsertQuery,
21
+ MergeQuery,
22
+ MultitableInsertQuery,
23
+ PrepareQuery,
24
+ ProcedureQuery,
25
+ PutQuery,
26
+ Q,
27
+ QueryHolder,
28
+ ReplaceQuery,
29
+ SchemaQuery,
30
+ SelectQuery,
31
+ SequenceQuery,
32
+ SetQuery,
33
+ StageQuery,
34
+ TableQuery,
35
+ TriggerQuery,
36
+ TypeQuery,
37
+ UnloadQuery,
38
+ UpdateQuery,
39
+ UserDefinedFunctionQuery,
40
+ ValuesQuery,
41
+ ViewQuery,
42
+ )
43
+ from sqlleaf.processors.collector import substitute
44
+ from sqlleaf.processors.transformer.expressions.row import simplify_row_in_values
45
+
46
+ logger = logging.getLogger("sqlleaf")
47
+
48
+ """
49
+ Parses text for SQL statements and collects them into Query models.
50
+ """
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class CollectQueryResult:
55
+ queries: t.List[QueryHolder] = field(default_factory=list) # Successfully collected queries
56
+ unknown: t.Dict[str, int] = field(default_factory=dict) # Unsupported by sqlleaf (no handler)
57
+ unsupported: t.List[t.Tuple[int, exp.Expr]] = field(default_factory=list) # Unsupported by sqlglot (no grammar)
58
+ object_mapping: mappings.ObjectMapping = None
59
+
60
+
61
+ def _is_prepare_supported(stmt: exp.Command) -> bool:
62
+ """
63
+ Check if a PREPARE statement for Postgres is supported.
64
+ Syntax: PREPARE name AS statement
65
+ The variant where parameters are provided is not yet supported.
66
+ """
67
+ expression_name = stmt.expression.name
68
+ tokens = sqlglot.tokenize(expression_name, dialect="postgres")
69
+
70
+ if len(tokens) < 3:
71
+ raise exception.InvalidQueryError(f"Invalid syntax for PREPARE expression: {stmt.sql(dialect='postgres')}")
72
+
73
+ # We cannot process arguments yet
74
+ if tokens[1].token_type != TokenType.ALIAS or tokens[1].text.upper() != "AS":
75
+ if tokens[1].token_type == TokenType.L_PAREN:
76
+ logger.warning("PREPARE with arguments is not currently supported.")
77
+ return False
78
+
79
+ # Ensure there's an 'AS' token
80
+ if not any(t.token_type == TokenType.ALIAS and t.text.upper() == "AS" for t in tokens):
81
+ raise exception.InvalidQueryError(f"Could not find 'AS' in PREPARE expression: {expression_name}")
82
+
83
+ return True
84
+
85
+
86
+ def _is_execute_supported(stmt: exp.Command) -> bool:
87
+ """
88
+ Check if an EXECUTE statement for Postgres is supported.
89
+ Syntax: EXECUTE name [ ( parameter [, ...] ) ]
90
+
91
+ Only the bare-name form (exactly one token, no argument parentheses) is supported.
92
+ If the expression contains more than one token it means arguments were supplied
93
+ (e.g. EXECUTE my_plan(arg1, arg2)), which is not yet handled.
94
+ """
95
+ expression_name = stmt.expression.name
96
+ tokens = sqlglot.tokenize(expression_name, dialect="postgres")
97
+
98
+ if not tokens or len(tokens) > 1:
99
+ raise exception.InvalidQueryError(f"Invalid syntax for EXECUTE expression: {stmt.sql(dialect='postgres')}")
100
+
101
+ return True
102
+
103
+
104
+ def _is_replace_supported(stmt: exp.Command, dialect: str) -> bool:
105
+ """
106
+ Check if a REPLACE statement is supported.
107
+ Transform to INSERT INTO to verify syntax.
108
+ """
109
+ expression = stmt.args.get("expression")
110
+ new_sql = f"INSERT {expression.this}" if expression else "INSERT"
111
+ try:
112
+ sqlglot.parse_one(new_sql, dialect=dialect)
113
+ return True
114
+ except Exception as e:
115
+ logger.warning(f"Invalid REPLACE statement syntax: {e}")
116
+ return False
117
+
118
+
119
+ def _classify_command(stmt: exp.Command, dialect: str) -> tuple[str, bool]:
120
+ """
121
+ Classify an exp.Command node into a (kind, is_supported) pair.
122
+ kind='' means the command is not recognised and should be treated as unsupported.
123
+ """
124
+ if dialect in ["athena", "redshift"] and stmt.name == "UNLOAD":
125
+ return "unload", True
126
+ if dialect == "postgres" and stmt.name == "PREPARE":
127
+ return "prepare", _is_prepare_supported(stmt)
128
+ if dialect == "postgres" and stmt.name == "EXECUTE":
129
+ return "execute", _is_execute_supported(stmt)
130
+ if dialect == "mysql" and stmt.name == "REPLACE":
131
+ return "replace", _is_replace_supported(stmt, dialect)
132
+ if stmt.this.upper() == "CALL":
133
+ return "call", True
134
+ return "", False
135
+
136
+
137
+ def collect_queries(text: str, dialect: str, object_mapping: mappings.ObjectMapping | None = None) -> CollectQueryResult:
138
+ """
139
+ Parse a series of SQL statements provided as text.
140
+ This includes tables, views, procedures, functions, sequences, etc.
141
+
142
+ Each query may contain multiple child queries. For example, a stored procedure often
143
+ has multiple individual queries. Each of these individual queries may also have
144
+ subqueries. For example, a MERGE query often has INSERTs or UPDATEs in its WHEN clauses.
145
+
146
+ The statements must be provided in the order in which they depend on each other.
147
+ If B depends on A, A must be created before B.
148
+ """
149
+ queries = {}
150
+ unknown = {}
151
+ unsupported = []
152
+ counts: Counter[str] = Counter()
153
+
154
+ if not object_mapping:
155
+ object_mapping = mappings.ObjectMapping(dialect=dialect)
156
+
157
+ # Parse the statements
158
+ parsed = sqlglot.parse(text, dialect=dialect)
159
+ parsed = _split_combined_statements(parsed)
160
+
161
+ for index, stmt in enumerate(parsed):
162
+ if not stmt:
163
+ continue
164
+
165
+ kind = ""
166
+ if isinstance(stmt, exp.Command):
167
+ kind, supported = _classify_command(stmt, dialect)
168
+ if not kind:
169
+ logger.warning(f"Unsupported statement: {stmt.sql(dialect=dialect)}")
170
+ unsupported.append((index, stmt))
171
+ continue
172
+ if not supported:
173
+ unsupported.append((index, stmt))
174
+ continue
175
+
176
+ # Remove duplicate queries
177
+ sql_text = stmt.sql(dialect=dialect)
178
+ _id = util.short_sha256_hash(sql_text)
179
+ if _id in queries:
180
+ logger.debug(f"Skipping duplicate query: {sql_text}")
181
+ continue
182
+
183
+ if not kind:
184
+ stmt, kind = _determine_query_kind(stmt, dialect)
185
+
186
+ if kind not in _QUERY_PROCESSORS:
187
+ unknown[kind] = unknown[kind] + 1 if kind in unknown else 1
188
+ continue
189
+
190
+ # Convert the statement to uppercase if the dialect supports it
191
+ stmt = normalize_identifiers(stmt, dialect=dialect, store_original_column_identifiers=True)
192
+ _unnest_values_inside_select(stmt, dialect=dialect)
193
+
194
+ # Substitute any Snowflake session variables ($var) in non-SET statements
195
+ if kind != "set" and object_mapping.session_variables:
196
+ stmt = substitute.substitute_session_variables(stmt, object_mapping.session_variables)
197
+
198
+ query: t.Optional[Q] = _QUERY_PROCESSORS[kind](
199
+ statement=stmt, dialect=dialect, object_mapping=object_mapping, statement_index=index
200
+ )
201
+ if query:
202
+ holder = QueryHolder(original=query)
203
+ _collect_query_children(query, holder, dialect, object_mapping)
204
+ queries[_id] = holder
205
+ counts[kind] += 1
206
+
207
+ logger.debug("Found statements: %s", dict(+counts))
208
+ if unknown:
209
+ logger.warning("Unknown statements: %s", dict(unknown.items()))
210
+ if unsupported:
211
+ logger.warning("Unsupported statements: %s", len(unsupported))
212
+
213
+ return CollectQueryResult(queries=list(queries.values()), unknown=unknown, unsupported=unsupported, object_mapping=object_mapping)
214
+
215
+
216
+ def _determine_query_kind(statement: exp.Expr, dialect: str) -> t.Tuple[exp.Expr, str]:
217
+ """
218
+ Determine a query's "kind" from the expression, which maps to how it will be processed.
219
+ """
220
+ if statement.key == "create" and isinstance(statement, exp.Create):
221
+ if statement.kind == "TABLE":
222
+ if isinstance(statement.expression, (exp.Select, exp.Values)) or statement.find(exp.ExecuteAsProperty):
223
+ kind = "ctas"
224
+ else:
225
+ kind = "table"
226
+ else:
227
+ kind = (statement.kind or "").lower()
228
+ elif statement.key == "select" and "into" in statement.args:
229
+ # sqlglot rewrites 'SELECT INTO' to 'CREATE TABLE AS' during parse()
230
+ # but it's not shown until we produce it with sql(), so we re-parse it
231
+ if dialect in ["redshift", "postgres"]:
232
+ statement = sqlglot.parse_one(statement.sql(dialect=""), dialect=dialect)
233
+ kind = "ctas"
234
+ else:
235
+ message = f"Expression 'SELECT INTO' has not been implemented yet for dialect: {dialect}"
236
+ raise exception.UnsupportedFeatureError(message=message)
237
+ else:
238
+ kind = statement.key.lower()
239
+
240
+ return statement, kind
241
+
242
+
243
+ def _collect_writable_cte_queries(
244
+ parent_query: Q, parent_holder: QueryHolder, dialect: str, object_mapping: mappings.ObjectMapping
245
+ ):
246
+ """
247
+ Collect any writable (DML) CTEs and attach them as downstream holders to the parent query.
248
+
249
+ If this query is of the form:
250
+ WITH cte AS (
251
+ INSERT ... RETURNING ...
252
+ )
253
+ INSERT INTO ...
254
+
255
+ then the outer and inner queries form a parent-child relationship.
256
+ The inner query is left as-is and copied, while the outer query transformer its
257
+ inner query's SELECT columns with the RETURNING columns. This is so that
258
+ the lineage functions collect the right columns during expression traversal.
259
+ The two queries are processed independently later.
260
+ """
261
+ for i, cte in enumerate(parent_query.get_ctes()):
262
+ cte_expr = cte.this
263
+
264
+ if isinstance(cte_expr, exp.Select):
265
+ continue
266
+
267
+ query = _process_unnamed(cte_expr, dialect, object_mapping, i)
268
+ if not query:
269
+ logger.warning(f"Skipping unsupported query type in CTE: {type(cte_expr)}")
270
+ continue
271
+
272
+ # Detach the query in the AST so that certain transformations work later
273
+ downstream_holder = QueryHolder(original=query)
274
+ parent_holder.add_downstream_holder(downstream_holder)
275
+ _collect_query_children(query, downstream_holder, dialect, object_mapping)
276
+
277
+
278
+ def _collect_call_substitutions(
279
+ query: Q,
280
+ holder: QueryHolder,
281
+ dialect: str,
282
+ object_mapping: mappings.ObjectMapping,
283
+ ) -> None:
284
+ """
285
+ Performs argument substitution for CallQuery, ExecuteQuery, CTASQuery (EXECUTE AS),
286
+ and any query containing UDF call sites.
287
+ The resulting inner statements are classified as Queries and attached as downstream holders.
288
+ """
289
+ subst_statements: t.List[exp.Expr] = []
290
+
291
+ if isinstance(query, CallQuery):
292
+ subst_statements = substitute.substitute_call(query=query)
293
+ elif isinstance(query, ExecuteQuery):
294
+ subst_statements = substitute.substitute_execute(query=query)
295
+ elif isinstance(query, CTASQuery) and query.source_info.type == typing.SqlObjectType.PREPARED_STATEMENT:
296
+ subst_statements = [substitute.substitute_create_execute(query=query)]
297
+ else:
298
+ expression = util.copy_expression(query.statement)
299
+ while True:
300
+ node, matched_udf = udf.find_next_udf_call(expression, query.object_mapping)
301
+ if not node:
302
+ break
303
+
304
+ inner_exprs = udf.substitute_udf(node, matched_udf)
305
+ subst_statements.extend(inner_exprs)
306
+ # Replace the processed call site with a placeholder so find_next_udf_call()
307
+ # no longer matches it on the next iteration, allowing the loop to advance
308
+ # to the next UDF call. The statement is a working copy and is not used
309
+ # after this function; only the extracted subst_statements matter.
310
+ node.replace(exp.Null())
311
+
312
+ parent_index = query.get_statement_index()
313
+ for i, stmt in enumerate(subst_statements):
314
+ composite_index = f"{parent_index}:{i}"
315
+ substituted_query = _process_unnamed(
316
+ statement=stmt,
317
+ dialect=dialect,
318
+ object_mapping=object_mapping,
319
+ statement_index=composite_index,
320
+ )
321
+ if substituted_query:
322
+ downstream_holder = QueryHolder(original=substituted_query)
323
+ holder.add_downstream_holder(downstream_holder)
324
+ # Recurse: the substituted child may itself contain calls/UDFs
325
+ _collect_query_children(substituted_query, downstream_holder, dialect, object_mapping)
326
+
327
+
328
+ def _collect_query_children(query: Q, parent_holder: QueryHolder, dialect: str, object_mapping: mappings.ObjectMapping):
329
+ """
330
+ Collect any nested child queries for a given query and attach them to the holder.
331
+ """
332
+ if isinstance(query, InsertQuery):
333
+ _collect_insert_children(query, parent_holder, object_mapping)
334
+ elif isinstance(query, MergeQuery):
335
+ _collect_merge_children(query, parent_holder, object_mapping)
336
+ elif isinstance(query, MultitableInsertQuery):
337
+ _collect_multitable_insert_children(query, parent_holder, object_mapping)
338
+ elif isinstance(query, UserDefinedFunctionQuery):
339
+ _collect_udf_children(query, parent_holder, dialect, object_mapping)
340
+
341
+ # Always check for writable CTEs regardless of query type above
342
+ if not isinstance(query, (CopyQuery, PutQuery)):
343
+ _collect_writable_cte_queries(query, parent_holder, dialect, object_mapping)
344
+
345
+ # Perform call-site substitution immediately at collection time
346
+ _collect_call_substitutions(query, parent_holder, dialect, object_mapping)
347
+
348
+
349
+ def _collect_udf_children(
350
+ query: UserDefinedFunctionQuery,
351
+ parent_holder: QueryHolder,
352
+ dialect: str,
353
+ object_mapping: mappings.ObjectMapping,
354
+ ):
355
+ """
356
+ Extract the function body from the UDF and parse it into individual statements.
357
+ Circular references in UDF/procedure definitions could cause infinite recursion.
358
+ """
359
+ body_expr = query.statement.args.get("expression")
360
+ if not body_expr:
361
+ return
362
+
363
+ for i, stmt in enumerate(util.iter_inner_statements(body_expr, dialect)):
364
+ child_query = _process_unnamed(stmt, dialect, object_mapping, i)
365
+
366
+ # If it's a simple select and _process_unnamed skipped it, create a SelectQuery
367
+ # TODO: VALUES and SELECT should be collected
368
+ if not child_query and isinstance(stmt, exp.Select):
369
+ child_query = SelectQuery(expr=stmt, dialect=dialect, object_mapping=object_mapping, statement_index=i)
370
+
371
+ if child_query:
372
+ downstream_holder = QueryHolder(original=child_query)
373
+ parent_holder.add_downstream_holder(downstream_holder)
374
+ _collect_query_children(child_query, downstream_holder, dialect, object_mapping)
375
+
376
+
377
+ def _collect_insert_children(query: InsertQuery, parent_holder: QueryHolder, object_mapping: mappings.ObjectMapping):
378
+ """
379
+ Collect any additional queries inside an INSERT. For Postgres, this is 'INSERT .. ON CONFLICT DO UPDATE'.
380
+ """
381
+ if not isinstance(query.statement, exp.Insert):
382
+ return
383
+
384
+ on_conflict = query.statement.args.get("conflict")
385
+
386
+ if not isinstance(on_conflict, exp.OnConflict) or on_conflict.args["action"].name == "DO NOTHING":
387
+ return
388
+
389
+ update_query = UpdateQuery(
390
+ expr=on_conflict,
391
+ dialect=query.dialect,
392
+ object_mapping=object_mapping,
393
+ statement_index=0,
394
+ table=query.get_target_as_table(),
395
+ )
396
+ downstream_holder = QueryHolder(original=update_query)
397
+ parent_holder.add_downstream_holder(downstream_holder)
398
+
399
+
400
+ def _collect_merge_children(
401
+ parent_query: MergeQuery, parent_holder: QueryHolder, object_mapping: mappings.ObjectMapping
402
+ ):
403
+ """
404
+ Transform any nested statements (INSERT or UPDATE) into fully qualified queries.
405
+
406
+ This is to allow the statements to be processed independently of the parent MERGE query.
407
+
408
+ For example, the merge query:
409
+
410
+ MERGE INTO fruit.processed AS t
411
+ USING fruit.raw AS s
412
+ ON t.kind = s.kind
413
+ WHEN MATCHED THEN
414
+ UPDATE SET name = s.name
415
+ WHEN NOT MATCHED THEN
416
+ INSERT (label) VALUES (s.kind);
417
+
418
+ has 2 nested queries that get transformed into:
419
+
420
+ UPDATE fruit.processed AS t
421
+ SET name = s.name
422
+ FROM fruit.raw AS t
423
+ WHERE t.kind = s.kind
424
+
425
+ INSERT INTO fruit.processed t
426
+ SELECT s.kind as label
427
+ FROM fruit.raw s;
428
+ """
429
+ merge = parent_query
430
+ parent_expr = parent_query.statement
431
+ whens = [when.args["then"] for when in parent_expr.args["whens"].expressions]
432
+
433
+ for i, when in enumerate(whens):
434
+ # Ensure the full expression tree is kept
435
+ when_expr = util.copy_expression(when)
436
+
437
+ if isinstance(when_expr, exp.Update):
438
+ update_query = UpdateQuery(
439
+ expr=when_expr,
440
+ dialect=parent_query.dialect,
441
+ object_mapping=object_mapping,
442
+ statement_index=i,
443
+ table=merge.get_target_as_table(),
444
+ )
445
+ downstream_holder = QueryHolder(original=update_query)
446
+ parent_holder.add_downstream_holder(downstream_holder)
447
+
448
+ elif isinstance(when_expr, exp.Insert):
449
+ insert_query = InsertQuery(
450
+ expr=when_expr,
451
+ dialect=parent_query.dialect,
452
+ object_mapping=object_mapping,
453
+ statement_index=i,
454
+ table=merge.get_target_as_table(),
455
+ )
456
+ insert_query.target_info = merge.target_info
457
+ downstream_holder = QueryHolder(original=insert_query)
458
+ parent_holder.add_downstream_holder(downstream_holder)
459
+
460
+
461
+ def _collect_multitable_insert_children(
462
+ parent_query: MultitableInsertQuery, parent_holder: QueryHolder, object_mapping: mappings.ObjectMapping
463
+ ):
464
+ """
465
+ Extract the ConditionalInsert (exp.Insert) branches from the Multitable statement.
466
+ """
467
+ statement = parent_query.statement
468
+ for i, branch in enumerate(statement.expressions):
469
+ if not isinstance(branch, exp.ConditionalInsert):
470
+ continue
471
+
472
+ insert_expr = branch.this
473
+ if not isinstance(insert_expr, exp.Insert):
474
+ continue
475
+
476
+ copied_expr = util.copy_expression(insert_expr)
477
+
478
+ if not copied_expr.expression and not copied_expr.args.get("source"):
479
+ # If the branch doesn't have its own source, use the parent's shared source.
480
+ # This happens in Snowflake multi-table inserts without explicit VALUES.
481
+ copied_expr.set("expression", parent_query.source_info.expression)
482
+
483
+ child_query = InsertQuery(
484
+ expr=copied_expr,
485
+ dialect=parent_query.dialect,
486
+ object_mapping=object_mapping,
487
+ statement_index=i,
488
+ )
489
+ downstream_holder = QueryHolder(original=child_query)
490
+ parent_holder.add_downstream_holder(downstream_holder)
491
+
492
+
493
+ _UNNAMED_TYPE_MAP: dict[type, type] = {
494
+ exp.Insert: InsertQuery,
495
+ exp.Update: UpdateQuery,
496
+ exp.Merge: MergeQuery,
497
+ exp.MultitableInserts: MultitableInsertQuery,
498
+ exp.Delete: DeleteQuery,
499
+ exp.Select: SelectQuery,
500
+ exp.Copy: CopyQuery,
501
+ exp.Values: ValuesQuery,
502
+ exp.Put: PutQuery,
503
+ exp.Set: SetQuery,
504
+ }
505
+
506
+
507
+ def _process_unnamed(
508
+ statement: exp.Expr, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int | str
509
+ ) -> Q | None:
510
+ """
511
+ Process an unnamed statement - one not inside a 'CREATE <name>' statement.
512
+ """
513
+ query_class = _UNNAMED_TYPE_MAP.get(type(statement))
514
+ if query_class is not None:
515
+ query = query_class(
516
+ expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
517
+ )
518
+ if isinstance(query, DeleteQuery) and not statement.find(exp.Insert, exp.Update, exp.Merge):
519
+ logger.warning(
520
+ "Skipping statement: A DELETE query must have a data-modifying statement, "
521
+ "such as an INSERT, to contain lineage."
522
+ )
523
+ return query
524
+
525
+ if isinstance(statement, exp.Command) and statement.this.upper() == "CALL":
526
+ return CallQuery(
527
+ expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
528
+ )
529
+ if isinstance(statement, exp.Command) and statement.this.upper() == "EXECUTE":
530
+ return ExecuteQuery(
531
+ expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
532
+ )
533
+ if isinstance(statement, exp.Create):
534
+ if statement.kind == "TABLE":
535
+ if isinstance(statement.expression, (exp.Select, exp.Values)) or statement.find(exp.ExecuteAsProperty):
536
+ return _process_views_and_ctas(statement, dialect, object_mapping, statement_index)
537
+ else:
538
+ return _process_tables(statement, dialect, object_mapping, statement_index)
539
+ elif statement.kind == "VIEW":
540
+ return _process_views_and_ctas(statement, dialect, object_mapping, statement_index)
541
+
542
+ return None
543
+
544
+
545
+ def _process_tables(
546
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int | str
547
+ ) -> Q | None:
548
+ """
549
+ Process a 'CREATE TABLE' statement.
550
+ """
551
+ query = None
552
+
553
+ if statement.kind == "TABLE":
554
+ # CREATE TABLE ...
555
+ query = TableQuery(
556
+ expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
557
+ )
558
+ query.set_column_defs()
559
+ object_mapping.add_table_query(
560
+ query=query,
561
+ column_mapping=query.get_column_names_with_types(include_system=True),
562
+ )
563
+ elif statement.kind == "SEQUENCE":
564
+ query = SequenceQuery(
565
+ expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
566
+ )
567
+ object_mapping.add_sequence_query(query=query)
568
+
569
+ return query
570
+
571
+
572
+ def _process_views_and_ctas(
573
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int | str
574
+ ) -> Q:
575
+ """
576
+ Convert a series of `CREATE VIEW/TABLE AS ...` SQL DDL statements into sqlglot's MappingSchema
577
+ to extract the table and column details.
578
+ """
579
+ util.qualify_and_annotate(statement, dialect, object_mapping, remove_added_aliases=True)
580
+ col_defs = _determine_column_defs(statement, dialect, object_mapping)
581
+
582
+ if statement.kind == "VIEW":
583
+ # CREATE VIEW ...
584
+ query = ViewQuery(
585
+ expr=statement,
586
+ dialect=dialect,
587
+ object_mapping=object_mapping,
588
+ columns=col_defs,
589
+ statement_index=statement_index,
590
+ )
591
+ elif statement.kind == "TABLE":
592
+ # CREATE TABLE AS ...
593
+ query = CTASQuery(
594
+ expr=statement,
595
+ dialect=dialect,
596
+ object_mapping=object_mapping,
597
+ columns=col_defs,
598
+ statement_index=statement_index,
599
+ )
600
+ query.system_column_defs = settings.system_columns(dialect)
601
+ else:
602
+ raise exception.UnsupportedFeatureError(message=f"Unhandled situation for query: {statement.kind}")
603
+
604
+ object_mapping.add_table_query(
605
+ query=query,
606
+ column_mapping=query.get_column_names_with_types(include_system=True),
607
+ )
608
+ return query
609
+
610
+
611
+ def _unnest_values_inside_select(statement: exp.Create, dialect: str):
612
+ """
613
+ Replace SELECT * FROM (VALUES ()) with VALUES ().
614
+ This prevents sqlglot from assigning its own aliases.
615
+ """
616
+ # TODO: this should be done in a transform, checked against self.statement
617
+ for values_expr in statement.find_all(exp.Values):
618
+ simplify_row_in_values(values_expr, dialect)
619
+ parent = values_expr.parent_select
620
+ while isinstance(parent, exp.Select) and parent.is_star and parent.parent_select:
621
+ parent = parent.parent_select
622
+
623
+ if parent and parent.parent:
624
+ # sqlglot converts "SELECT UNION VALUES" into "SELECT UNION SELECT * FROM (VALUES ()) AS _values"
625
+ # We undo that transformation (risky, as actual query may have that alias)
626
+ if alias := values_expr.args["alias"]:
627
+ if alias.name != "_values":
628
+ return statement
629
+ alias.pop()
630
+ parent.replace(values_expr)
631
+
632
+ return statement
633
+
634
+
635
+ def _determine_column_defs(
636
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping
637
+ ) -> t.List[ColumnDef]:
638
+ """
639
+ Look up the columns for 'y' in 'INSERT INTO x TABLE y'
640
+ """
641
+ if isinstance(statement.expression, exp.Values):
642
+ # CREATE TABLE AS VALUES
643
+ columns = [stmt.name for stmt in statement.this.expressions]
644
+ types = [val.type for val in statement.expression.expressions[0].expressions]
645
+
646
+ if not columns:
647
+ columns = util.default_column_index_iterator(dialect, types)
648
+
649
+ col_defs = [exp.ColumnDef(this=col_name, kind=col_type) for col_name, col_type in zip(columns, types)]
650
+ elif exec_prop := statement.find(exp.ExecuteAsProperty):
651
+ # CREATE TABLE AS EXECUTE
652
+ plan_name = exec_prop.this.name
653
+ plan_table = exp.to_table(plan_name)
654
+ matched_prepare = object_mapping.lookup_prepare_query(plan_table, raise_on_missing=True)
655
+
656
+ source = matched_prepare.statement
657
+ if isinstance(source, exp.Select):
658
+ col_defs = [
659
+ exp.ColumnDef(this=exp.to_identifier(col.alias_or_name), kind=col.unalias().type)
660
+ for col in source.expressions
661
+ ]
662
+ else:
663
+ col_defs = []
664
+ else:
665
+ col_defs = [exp.ColumnDef(this=exp.to_identifier(s.alias), kind=s.type) for s in statement.selects]
666
+
667
+ return col_defs
668
+
669
+
670
+ def _process_functions(
671
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
672
+ ) -> Q:
673
+ """
674
+ Process a "CREATE FUNCTION" statement.
675
+ """
676
+ query = UserDefinedFunctionQuery(
677
+ expr=statement,
678
+ dialect=dialect,
679
+ object_mapping=object_mapping,
680
+ statement_index=statement_index,
681
+ )
682
+ object_mapping.add_udf_query(query, column_mapping=query.get_column_names_with_types(include_system=True))
683
+
684
+ return query
685
+
686
+
687
+ def _process_triggers(
688
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
689
+ ) -> Q:
690
+ """
691
+ Process a "CREATE TRIGGER" statement.
692
+ """
693
+ query = TriggerQuery(statement, dialect, object_mapping, statement_index)
694
+ object_mapping.add_trigger_query(query)
695
+ return query
696
+
697
+
698
+ def _process_stored_procedures(
699
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
700
+ ) -> Q:
701
+ """
702
+ Process a "CREATE PROCEDURE" statement.
703
+ """
704
+ query = ProcedureQuery(
705
+ expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
706
+ )
707
+ object_mapping.add_procedure_query(query)
708
+ return query
709
+
710
+
711
+ def _process_stage(
712
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
713
+ ) -> Q:
714
+ query = StageQuery(statement, dialect, object_mapping=object_mapping, statement_index=statement_index)
715
+ object_mapping.add_stage_query(query)
716
+ return query
717
+
718
+
719
+ def _process_unload(
720
+ statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
721
+ ) -> Q:
722
+ query = UnloadQuery(statement, dialect, object_mapping, statement_index)
723
+ return query
724
+
725
+
726
+ def _process_replace(
727
+ statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
728
+ ) -> Q:
729
+ query = ReplaceQuery(statement, dialect, object_mapping, statement_index)
730
+ return query
731
+
732
+
733
+ def _process_prepare(
734
+ statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
735
+ ) -> Q:
736
+ query = PrepareQuery(statement, dialect, object_mapping, statement_index)
737
+ object_mapping.add_prepare_query(query)
738
+ return query
739
+
740
+
741
+ def _process_execute(
742
+ statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
743
+ ) -> Q:
744
+ query = ExecuteQuery(statement, dialect, object_mapping, statement_index)
745
+ return query
746
+
747
+
748
+ def _process_type(
749
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
750
+ ) -> Q:
751
+ query = TypeQuery(statement, dialect, object_mapping, statement_index)
752
+ object_mapping.add_type_query(query)
753
+ return query
754
+
755
+
756
+ def _process_schema(
757
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
758
+ ) -> Q:
759
+ query = SchemaQuery(statement, dialect, object_mapping, statement_index)
760
+ object_mapping.add_schema_query(query)
761
+ return query
762
+
763
+
764
+ def _process_database(
765
+ statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
766
+ ) -> Q:
767
+ query = DatabaseQuery(statement, dialect, object_mapping, statement_index)
768
+ object_mapping.add_database_query(query)
769
+ return query
770
+
771
+
772
+ _QUERY_PROCESSORS: dict[str, t.Callable] = {
773
+ "table": _process_tables,
774
+ "ctas": _process_views_and_ctas,
775
+ "view": _process_views_and_ctas,
776
+ "sequence": _process_tables,
777
+ "procedure": _process_stored_procedures,
778
+ "multitableinserts": _process_unnamed,
779
+ "function": _process_functions,
780
+ "database": _process_database,
781
+ "trigger": _process_triggers,
782
+ "select": _process_unnamed,
783
+ "insert": _process_unnamed,
784
+ "update": _process_unnamed,
785
+ "merge": _process_unnamed,
786
+ "delete": _process_unnamed,
787
+ "schema": _process_schema,
788
+ "unload": _process_unload,
789
+ "replace": _process_replace,
790
+ "prepare": _process_prepare,
791
+ "execute": _process_execute,
792
+ "stage": _process_stage,
793
+ "call": _process_unnamed,
794
+ "copy": _process_unnamed,
795
+ "put": _process_unnamed,
796
+ "set": _process_unnamed,
797
+ "values": _process_unnamed,
798
+ "type": _process_type,
799
+ }
800
+
801
+
802
+ def _split_combined_statements(parsed: t.List[exp.Expr]) -> t.List[exp.Expr]:
803
+ """
804
+ Workaround for sqlglot incorrectly bundling statements following a block-based CREATE.
805
+ For example, with the statement "CREATE PROCEDURE .. BEGIN .. END; CALL();" the CALL()
806
+ is included with the procedure definition.
807
+ """
808
+ new_parsed = []
809
+ for stmt in parsed:
810
+ if isinstance(stmt, exp.Create) and isinstance(stmt.args.get("expression"), exp.Block):
811
+ block = stmt.args["expression"]
812
+ end_index = -1
813
+ for i, expr in enumerate(block.expressions):
814
+ if isinstance(expr, exp.EndStatement):
815
+ end_index = i
816
+ break
817
+
818
+ # Separate the statements
819
+ if end_index != -1 and end_index < len(block.expressions) - 1:
820
+ extra = block.expressions[end_index + 1 :]
821
+ block.set("expressions", block.expressions[: end_index + 1])
822
+ new_parsed.append(stmt)
823
+ new_parsed.extend(extra)
824
+ continue
825
+
826
+ new_parsed.append(stmt)
827
+ return new_parsed