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,495 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+ from dataclasses import dataclass
6
+
7
+ import networkx as nx
8
+ from sqlglot import exp
9
+ from sqlglot.optimizer import Scope, build_scope
10
+
11
+ if t.TYPE_CHECKING:
12
+ pass
13
+
14
+ from sqlleaf import exception, mappings, util
15
+ from sqlleaf.models.context import GeneratorContext, PositionContext
16
+ from sqlleaf.models.hook import Hook
17
+ from sqlleaf.models.node import (
18
+ ColumnNode,
19
+ EdgeAttributes,
20
+ N,
21
+ TargetNodeType,
22
+ )
23
+ from sqlleaf.models.query import (
24
+ PutQuery,
25
+ Q,
26
+ QueryHolder,
27
+ TableQuery,
28
+ UpdateQuery,
29
+ )
30
+ from sqlleaf.processors.generator.dialects.base import BaseGenerator
31
+ from sqlleaf.typing import TableOrScopeType, TableType
32
+ from sqlleaf.util.expression import get_column_index, get_expression_for_column
33
+
34
+ logger = logging.getLogger("sqlleaf")
35
+
36
+
37
+ def generate_lineage_for_query(query_holder: QueryHolder, graph: nx.MultiDiGraph, hooks: list[Hook]) -> nx.MultiDiGraph:
38
+ """
39
+ Calculate the lineage for an SQL query.
40
+
41
+ We collect all the columns from the query's target table, and then iterate
42
+ over sqlglot's abstract syntax tree (AST) to determine the set of nodes
43
+ and transformations used along the path to reach the table's columns.
44
+
45
+ Everything is extracted: columns, literals, functions, etc.
46
+ """
47
+ query = query_holder.transformed
48
+
49
+ logger.debug("---- Generator ----")
50
+ logger.debug(f"Generating for: {type(query)}")
51
+ statement = query.statement
52
+ logger.debug(f"Getting lineage for query: {statement.sql(dialect=query.dialect)}")
53
+ logger.debug(repr(statement))
54
+
55
+ target_object = query.target_info.expression
56
+ pos_ctx = PositionContext(statement_index=query.get_statement_index())
57
+ gen_ctx = GeneratorContext(
58
+ graph=graph,
59
+ query=query,
60
+ expr=statement,
61
+ child_node=target_object,
62
+ scope=None,
63
+ )
64
+ generator = BaseGenerator.from_dialect(query.dialect)
65
+ generator.add_hooks(hooks)
66
+
67
+ if check_for_put(generator, gen_ctx, pos_ctx):
68
+ return graph
69
+
70
+ if check_for_trigger(target_object, query.object_mapping):
71
+ return graph
72
+
73
+ if check_for_external_table(generator, gen_ctx, pos_ctx):
74
+ return graph
75
+
76
+ generate_lineage_for_columns(generator, gen_ctx, pos_ctx)
77
+
78
+ logger.debug("---- Generator End ----")
79
+ return graph
80
+
81
+
82
+ def generate_lineage_for_columns(
83
+ generator: BaseGenerator,
84
+ gen_ctx: GeneratorContext,
85
+ pos_ctx: PositionContext,
86
+ ):
87
+ """
88
+ Generate the lineage for a set of columns from a given table.
89
+ """
90
+ scope = get_scope(statement=gen_ctx.expr)
91
+ gen_ctx.scope_positions.calculate(scope)
92
+
93
+ # Process the selected columns
94
+ for selected_node, default_node in generator.iter_child_nodes(gen_ctx, pos_ctx):
95
+ child_node = selected_node or default_node
96
+ if not child_node:
97
+ break
98
+
99
+ logger.info(f"Calculating lineage downstream of {child_node.friendly_name}")
100
+
101
+ # A column may have both lineage and a default expression; process both.
102
+ # TODO: make this a CLI flag for whether to include these exprs in lineage
103
+ if default_node:
104
+ constraint_expr = util.get_column_constraint_expression(default_node.expr)
105
+ if constraint_expr and constraint_expr.this:
106
+ constraint_ctx = gen_ctx.replace(
107
+ expr=constraint_expr.this,
108
+ new_data_type=child_node.data_type,
109
+ child_node=child_node,
110
+ )
111
+ # Walk only the expression
112
+ walk_expressions_and_build_graph(generator=generator, gen_ctx=constraint_ctx, pos_ctx=pos_ctx)
113
+
114
+ if selected_node:
115
+ child_pos_ctx = child_node.ctx
116
+ walk_query_and_build_graph(generator, child_node, scope, gen_ctx, child_pos_ctx)
117
+
118
+
119
+ def walk_query_and_build_graph(
120
+ generator: BaseGenerator,
121
+ child_node: TargetNodeType,
122
+ scope: Scope,
123
+ gen_ctx: GeneratorContext,
124
+ pos_ctx: PositionContext,
125
+ ) -> None:
126
+ """
127
+ Walk over each query (and its subqueries) to collect the expressions for each column.
128
+ For any expression subtrees found, invoke an 'expression walker' to process them.
129
+ """
130
+ gen_ctx = gen_ctx.replace(scope=scope, child_node=child_node)
131
+
132
+ for scope_traversal in walk_query_scope(
133
+ column=t.cast(t.Union[exp.Column, int], child_node.expr),
134
+ scope=scope,
135
+ ):
136
+ logger.debug(f"Processing node expr: {scope_traversal.expression}, Id: {id(scope_traversal)}")
137
+ logger.debug(f"Child node: {child_node.full_name}")
138
+
139
+ height, width = gen_ctx.scope_positions.get_position_for_expr(scope_traversal.scope.expression)
140
+ child_ctx = pos_ctx.replace(query_depth=height, query_width=width, select_index=scope_traversal.select_index)
141
+ gen_ctx = gen_ctx.replace(
142
+ expr=scope_traversal.expression,
143
+ scope=scope_traversal.scope,
144
+ child_node=child_node,
145
+ )
146
+
147
+ nodes = walk_expressions_and_build_graph(generator, gen_ctx, child_ctx)
148
+ if nodes:
149
+ logger.debug(f"Produced nodes: {[n.full_name for n in nodes]}")
150
+
151
+ for n in nodes:
152
+ if isinstance(n, ColumnNode) and isinstance(n.source_scope, Scope):
153
+ # There are additional expressions to traverse (e.g. inside a CTE)
154
+ walk_query_and_build_graph(generator, n, n.source_scope, gen_ctx, pos_ctx)
155
+
156
+
157
+ def walk_query_scope(column: exp.Column | int, scope: Scope) -> t.Generator[ScopeTraversal]:
158
+ """
159
+ Walk over each query scope (i.e. nested or standalone SELECT statement) and return the expression linked to the column.
160
+ """
161
+ # Subqueries, unions, etc are the first layers
162
+ if isinstance(scope.expression, exp.Subquery):
163
+ sources = scope.subquery_scopes
164
+ if not sources:
165
+ # Occurs during "INSERT .. (WITH cte .. ) SELECT FROM cte"
166
+ sources = [scope.sources[""]]
167
+ for source in sources:
168
+ logger.debug("Yielding from first subquery scope")
169
+ yield from walk_query_scope(
170
+ column=column,
171
+ scope=source,
172
+ )
173
+ if sources:
174
+ return
175
+ elif isinstance(scope.expression, exp.SetOperation):
176
+ # UNION, EXCEPT, etc
177
+ index = get_column_index(column, scope.expression)
178
+
179
+ sources = scope.union_scopes
180
+ for source in sources:
181
+ logger.debug("Yielding from union scope")
182
+ yield from walk_query_scope(
183
+ column=index,
184
+ scope=source,
185
+ )
186
+ if sources:
187
+ return
188
+ elif isinstance(scope.expression, exp.Lateral):
189
+ # LATERAL ( SELECT )
190
+ sources = [src for src in scope.sources.values() if isinstance(src, Scope)]
191
+ for source in sources:
192
+ if isinstance(source, Scope):
193
+ logger.debug("Yielding from lateral scope")
194
+ yield from walk_query_scope(
195
+ column=column,
196
+ scope=source,
197
+ )
198
+ if sources:
199
+ return
200
+
201
+ # Get the associated expression for the column name
202
+ select, select_index = get_expression_for_column(column, scope.expression)
203
+ st = ScopeTraversal(
204
+ expression=select.unalias(),
205
+ scope=scope,
206
+ select_index=select_index,
207
+ )
208
+ logger.debug(
209
+ "Yielding standard expression: '%s', Idx: %s, Type: %s, Expr: %s, Id: %s",
210
+ column,
211
+ select_index,
212
+ type(select.unalias()),
213
+ select.sql(),
214
+ id(st),
215
+ )
216
+ yield st
217
+
218
+
219
+ def walk_expressions_and_build_graph(
220
+ generator: BaseGenerator,
221
+ gen_ctx: GeneratorContext,
222
+ pos_ctx: PositionContext,
223
+ ) -> t.List[N]:
224
+ """
225
+ Collect the leaves of an expression so that we can get the full set of data sources and function arguments
226
+ for a particular column.
227
+
228
+ For example, given the query:
229
+ INSERT INTO x (name)
230
+ SELECT UPPER(CONCAT('p', 'q')) AS name
231
+ We construct the graph by moving 'upwards' from the target (child) to source (parent):
232
+ - Start with child 'x.name'. Its parent is 'UPPER', so we create a FunctionNode.
233
+ - Next, the parent of UPPER is CONCAT, which is also x.name's grandparent. This too becomes a FunctionNode.
234
+ - Finally, the parents of CONCAT are 'p' and 'q'. These become LiteralNodes.
235
+ """
236
+ nodes_created = []
237
+
238
+ for edge in generator.process(gen_ctx.expr, gen_ctx, pos_ctx):
239
+ parent_node, child_node = edge.parent, edge.child
240
+ if parent_node:
241
+ node_exists = gen_ctx.graph.has_node(parent_node.full_name)
242
+ if not node_exists:
243
+ nodes_created.append(parent_node)
244
+
245
+ # Include any inherited columns
246
+ inherited_columns_of_parent = find_inherited_columns_for_parent(
247
+ column_node=parent_node, generator=generator, gen_ctx=gen_ctx, pos_ctx=pos_ctx
248
+ )
249
+ inherited_columns_of_child = find_inherited_columns_for_child(
250
+ column_node=child_node, generator=generator, gen_ctx=gen_ctx, pos_ctx=pos_ctx
251
+ )
252
+
253
+ for parent in [parent_node] + inherited_columns_of_parent:
254
+ for child in [child_node] + inherited_columns_of_child:
255
+ add_nodes_with_edge_to_graph(
256
+ parent,
257
+ child,
258
+ gen_ctx.graph,
259
+ gen_ctx.query,
260
+ )
261
+ return nodes_created
262
+
263
+
264
+ def find_inherited_columns_for_parent(
265
+ column_node: N, generator: BaseGenerator, gen_ctx: GeneratorContext, pos_ctx: PositionContext
266
+ ) -> t.List[ColumnNode]:
267
+ """
268
+ Find the inherited columns for a particular column, but only for the form 'SELECT FROM ONLY <table>'
269
+ TODO fix comments etc
270
+ """
271
+ if not isinstance(column_node, ColumnNode) or column_node.parent_kind == TableType.CTE:
272
+ return []
273
+
274
+ # Find the column's exp.Table in the expression and check if it has 'ONLY' set
275
+ if not column_node.expr.parent_select:
276
+ return []
277
+
278
+ inherited_columns = []
279
+ for table in column_node.expr.parent_select.find_all(exp.Table):
280
+ if table.catalog == column_node.catalog and table.db == column_node.schema and table.name == column_node.table:
281
+ parent_table = table
282
+ if parent_table.args.get("only", False):
283
+ inherited_columns = []
284
+ else:
285
+ inherited_columns = find_inherited_columns(
286
+ column_node=column_node, generator=generator, gen_ctx=gen_ctx, pos_ctx=pos_ctx
287
+ )
288
+ logger.debug(f"Including inherited columns as sources: {[c.friendly_name for c in inherited_columns]}")
289
+
290
+ return inherited_columns
291
+
292
+
293
+ def find_inherited_columns_for_child(
294
+ column_node: N | None, generator: BaseGenerator, gen_ctx: GeneratorContext, pos_ctx: PositionContext
295
+ ) -> t.List[ColumnNode]:
296
+ """
297
+ Find the inherited columns for a particular column, but only for the form 'MERGE|UPDATE ONLY <table>'
298
+ """
299
+ inherited_columns = []
300
+ if not isinstance(column_node, ColumnNode) or column_node.parent_kind == TableType.CTE:
301
+ return inherited_columns
302
+
303
+ original_query = gen_ctx.query.get_original_self()
304
+ if isinstance(original_query, UpdateQuery) and not getattr(original_query, "only", False):
305
+ inherited_columns = find_inherited_columns(
306
+ column_node=column_node, generator=generator, gen_ctx=gen_ctx, pos_ctx=pos_ctx
307
+ )
308
+ logger.debug(f"Including inherited columns as targets: {[c.friendly_name for c in inherited_columns]}")
309
+
310
+ return inherited_columns
311
+
312
+
313
+ def find_inherited_columns(
314
+ column_node: ColumnNode, generator: BaseGenerator, gen_ctx: GeneratorContext, pos_ctx: PositionContext
315
+ ) -> t.List[ColumnNode]:
316
+ """
317
+ Find all inherited columns from a table that are similar to some column.
318
+
319
+ For example, if we have
320
+ CREATE TABLE a (name VARCHAR);
321
+ CREATE TABLE b (age VARCHAR) INHERITS (a);
322
+ then whenever we process column `a.name`, we also need to include `b.name`.
323
+ """
324
+ inherited_column_nodes = []
325
+ table = column_node.as_table()
326
+ table_query = gen_ctx.query.object_mapping.lookup_table_query(table=table)
327
+
328
+ # Collect any columns from inherited tables with the same name
329
+ for inh_table in getattr(table_query, "inherited_by", []):
330
+ col_def = [c for c in inh_table.get_column_defs() if c.name == column_node.name][0]
331
+ col = util.column_def_to_column(column_def=col_def, parent_table=inh_table.get_target_as_table())
332
+ col_ctx = gen_ctx.replace(expr=col, scope=None) # Remove the node so that the column isn't renamed
333
+ for edge in generator.process_column(col, col_ctx, pos_ctx):
334
+ inh_node_attrs = edge.parent
335
+ inherited_column_nodes.append(inh_node_attrs)
336
+
337
+ return inherited_column_nodes
338
+
339
+
340
+ def add_nodes_with_edge_to_graph(
341
+ parent_node: N | None,
342
+ child_node: N | None,
343
+ graph: nx.MultiDiGraph,
344
+ query: Q,
345
+ ):
346
+ """
347
+ Add two node and an edge between them to the graph.
348
+ """
349
+ p_attrs, p_created = add_node_if_not_exists(parent_node, graph)
350
+ c_attrs, c_created = add_node_if_not_exists(child_node, graph)
351
+
352
+ if p_attrs and c_attrs:
353
+ p_full_name = p_attrs.full_name
354
+ c_full_name = c_attrs.full_name
355
+
356
+ edges = graph.get_edge_data(p_full_name, c_full_name) or {}
357
+ if edges:
358
+ pass
359
+ edge_attrs = EdgeAttributes(
360
+ parent=p_attrs,
361
+ child=c_attrs,
362
+ query=query,
363
+ path_idx=len(edges),
364
+ )
365
+ graph.add_edge(p_full_name, c_full_name, attrs=edge_attrs)
366
+ logger.debug(f"Added edge between {p_full_name} [{id(p_attrs)}] -> {c_full_name} [{id(c_attrs)}]")
367
+
368
+
369
+ def add_node_if_not_exists(node_attrs: N | None, graph: nx.MultiDiGraph) -> tuple[N | None, bool]:
370
+ """
371
+ Add a node to the graph if it doesn't already exist.
372
+
373
+ We need to re-use the existing node attributes so that the edge attribute models don't refer to
374
+ different-but-same-named node attributes.
375
+ """
376
+ created = False
377
+
378
+ if not node_attrs:
379
+ return None, created
380
+
381
+ node_name = node_attrs.full_name
382
+
383
+ if graph.has_node(node_name):
384
+ logger.debug(f"Re-using Node: {node_attrs.__class__.__name__}, Name: {node_attrs.full_name}")
385
+ return t.cast(N, graph.nodes[node_name]["attrs"]), created
386
+
387
+ graph.add_node(node_name, attrs=node_attrs)
388
+ logger.debug(f"Created Node: {node_attrs.__class__.__name__}, Name: {node_attrs.full_name}")
389
+ created = True
390
+
391
+ return node_attrs, created
392
+
393
+
394
+ def get_scope(statement: exp.Expr) -> Scope:
395
+ """
396
+ Build the scope for a statement.
397
+ """
398
+ statement_lineage = statement.copy()
399
+ scope = build_scope(statement_lineage)
400
+ if not scope:
401
+ raise exception.SqlGlotException("Cannot build scope. Expression must be a SELECT")
402
+ return scope
403
+
404
+
405
+ @dataclass(frozen=True)
406
+ class ScopeTraversal:
407
+ expression: exp.Expr
408
+ scope: TableOrScopeType
409
+ select_index: int
410
+
411
+
412
+ # def set_cte_properties(path: t.List[ScopeTraversal]) -> None:
413
+ # """
414
+ # Check for properties related to recursive CTEs.
415
+ #
416
+ # Make the first node recursive if anything in its path is also recursive.
417
+ # Otherwise, we set it to be the anchor, as its children are the anchor part
418
+ # of the expression.
419
+ # """
420
+ # root_node: ScopeTraversal = path[0]
421
+ # if root_node.is_parent_a_recursive_cte:
422
+ # for n in path[1:]:
423
+ # if is_node_inside_a_recursive_cte(n):
424
+ # if n.is_parent_a_recursive_cte:
425
+ # root_node.recursive_cte_member_kind = "recursive"
426
+ # n.recursive_cte_member_kind = "anchor"
427
+ # else:
428
+ # root_node.recursive_cte_member_kind = "anchor"
429
+ # break
430
+
431
+
432
+ # def is_node_inside_a_recursive_cte(expr: exp.Expr) -> bool:
433
+ # """
434
+ # Check if we're inside a recursive CTE
435
+ # """
436
+ # if parent_cte := expr.find_ancestor(exp.CTE):
437
+ # if parent_cte.parent.recursive:
438
+ # return True
439
+ # return False
440
+
441
+
442
+ def check_for_trigger(
443
+ table: exp.Table | exp.Literal | exp.Identifier | exp.Schema, object_mapping: mappings.ObjectMapping
444
+ ) -> bool:
445
+ """
446
+ Check if a trigger overrides the query's behaviour.
447
+ """
448
+ if not isinstance(table, exp.Table):
449
+ return False
450
+
451
+ if trigger := object_mapping.lookup_trigger_query(table=table, raise_on_missing=False):
452
+ if getattr(trigger, "timing", None) == "INSTEAD OF":
453
+ logger.debug(
454
+ "Skipping lineage for all columns of table '%s' since trigger '%s' overrides it."
455
+ % (exp.table_name(table), getattr(trigger, "name", ""))
456
+ )
457
+ # TODO: Use the trigger's function as the lineage
458
+ # func = trigger.execute
459
+ return True
460
+ return False
461
+
462
+
463
+ def check_for_put(generator: BaseGenerator, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> bool:
464
+ """
465
+ Check if this is a PUT query.
466
+ """
467
+ query = gen_ctx.query
468
+ graph = gen_ctx.graph
469
+
470
+ if query.dialect == "snowflake" and isinstance(query, PutQuery):
471
+ expr = query.statement
472
+ # Short-circuit this function; it's not an insert
473
+ for edge in generator.process(expr, gen_ctx, pos_ctx):
474
+ file_node, stage_node = edge.parent, edge.child
475
+ add_nodes_with_edge_to_graph(file_node, stage_node, graph, query)
476
+ return True
477
+ return False
478
+
479
+
480
+ def check_for_external_table(generator: BaseGenerator, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> bool:
481
+ """
482
+ Check if this is a CREATE EXTERNAL TABLE query.
483
+ """
484
+ query = gen_ctx.query
485
+
486
+ if query.dialect in ["athena", "redshift"] and isinstance(query, TableQuery) and query.property == "external":
487
+ location_expr = query.location
488
+
489
+ for child_node, _ in generator.iter_child_nodes(gen_ctx, pos_ctx):
490
+ if child_node:
491
+ gen_ctx = gen_ctx.replace(expr=location_expr, child_node=child_node)
492
+ pos_ctx = pos_ctx.replace(select_index=child_node.ctx.select_index)
493
+ walk_expressions_and_build_graph(generator=generator, gen_ctx=gen_ctx, pos_ctx=pos_ctx)
494
+ return True
495
+ return False
@@ -0,0 +1,31 @@
1
+ from sqlleaf.processors.transformer import udf
2
+ from sqlleaf.processors.transformer.base import BaseQueryTransformer
3
+ from sqlleaf.processors.transformer.call import CallTransformer
4
+ from sqlleaf.processors.transformer.copy import CopyTransformer
5
+ from sqlleaf.processors.transformer.ctas import CTASTransformer
6
+ from sqlleaf.processors.transformer.delete import DeleteTransformer
7
+ from sqlleaf.processors.transformer.execute import ExecuteTransformer
8
+ from sqlleaf.processors.transformer.insert import InsertTransformer
9
+ from sqlleaf.processors.transformer.merge import MergeTransformer
10
+ from sqlleaf.processors.transformer.multitable_insert import MultitableInsertTransformer
11
+ from sqlleaf.processors.transformer.replace import ReplaceTransformer
12
+ from sqlleaf.processors.transformer.unload import UnloadTransformer
13
+ from sqlleaf.processors.transformer.update import UpdateTransformer
14
+ from sqlleaf.processors.transformer.values import ValuesTransformer
15
+
16
+ __all__ = [
17
+ "udf",
18
+ "BaseQueryTransformer",
19
+ "CallTransformer",
20
+ "CTASTransformer",
21
+ "CopyTransformer",
22
+ "DeleteTransformer",
23
+ "ExecuteTransformer",
24
+ "InsertTransformer",
25
+ "MergeTransformer",
26
+ "MultitableInsertTransformer",
27
+ "ReplaceTransformer",
28
+ "UnloadTransformer",
29
+ "UpdateTransformer",
30
+ "ValuesTransformer",
31
+ ]