sqlleaf 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlleaf/__init__.py +4 -0
- sqlleaf/dialects/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/p_classes.py +275 -0
- sqlleaf/dialects/plpgsql/p_dialect.py +148 -0
- sqlleaf/dialects/plpgsql/p_generator.py +538 -0
- sqlleaf/dialects/plpgsql/p_parser.py +1244 -0
- sqlleaf/dialects/plpgsql/p_tokenizer.py +41 -0
- sqlleaf/exception.py +67 -0
- sqlleaf/lineage.py +326 -0
- sqlleaf/mappings.py +376 -0
- sqlleaf/models/__init__.py +0 -0
- sqlleaf/models/context.py +153 -0
- sqlleaf/models/hook.py +9 -0
- sqlleaf/models/node/__init__.py +27 -0
- sqlleaf/models/node/base.py +148 -0
- sqlleaf/models/node/column.py +345 -0
- sqlleaf/models/node/dynamodb.py +39 -0
- sqlleaf/models/node/function.py +33 -0
- sqlleaf/models/node/interval.py +21 -0
- sqlleaf/models/node/json.py +57 -0
- sqlleaf/models/node/literal.py +17 -0
- sqlleaf/models/node/null.py +14 -0
- sqlleaf/models/node/pivot.py +34 -0
- sqlleaf/models/node/program.py +27 -0
- sqlleaf/models/node/sequence.py +18 -0
- sqlleaf/models/node/star.py +11 -0
- sqlleaf/models/node/stream.py +14 -0
- sqlleaf/models/node/user_defined_function.py +33 -0
- sqlleaf/models/node/var.py +7 -0
- sqlleaf/models/node/variable.py +8 -0
- sqlleaf/models/node/window.py +22 -0
- sqlleaf/models/query/__init__.py +33 -0
- sqlleaf/models/query/base.py +248 -0
- sqlleaf/models/query/call.py +111 -0
- sqlleaf/models/query/copy.py +144 -0
- sqlleaf/models/query/ctas.py +61 -0
- sqlleaf/models/query/database.py +48 -0
- sqlleaf/models/query/delete.py +26 -0
- sqlleaf/models/query/execute.py +66 -0
- sqlleaf/models/query/holder.py +49 -0
- sqlleaf/models/query/insert.py +69 -0
- sqlleaf/models/query/merge.py +29 -0
- sqlleaf/models/query/multitable_insert.py +42 -0
- sqlleaf/models/query/prepare.py +61 -0
- sqlleaf/models/query/procedure.py +72 -0
- sqlleaf/models/query/put.py +38 -0
- sqlleaf/models/query/replace.py +32 -0
- sqlleaf/models/query/schema.py +47 -0
- sqlleaf/models/query/select.py +27 -0
- sqlleaf/models/query/sequence.py +33 -0
- sqlleaf/models/query/set.py +74 -0
- sqlleaf/models/query/stage.py +78 -0
- sqlleaf/models/query/table.py +220 -0
- sqlleaf/models/query/trigger.py +42 -0
- sqlleaf/models/query/type.py +42 -0
- sqlleaf/models/query/unload.py +64 -0
- sqlleaf/models/query/update.py +45 -0
- sqlleaf/models/query/user_defined_function.py +297 -0
- sqlleaf/models/query/values.py +20 -0
- sqlleaf/models/query/view.py +41 -0
- sqlleaf/path.py +194 -0
- sqlleaf/processors/__init__.py +0 -0
- sqlleaf/processors/collector/__init__.py +0 -0
- sqlleaf/processors/collector/collector.py +827 -0
- sqlleaf/processors/collector/substitute.py +396 -0
- sqlleaf/processors/generator/__init__.py +0 -0
- sqlleaf/processors/generator/dialects/__init__.py +5 -0
- sqlleaf/processors/generator/dialects/athena.py +45 -0
- sqlleaf/processors/generator/dialects/base.py +572 -0
- sqlleaf/processors/generator/dialects/mysql.py +19 -0
- sqlleaf/processors/generator/dialects/postgres.py +182 -0
- sqlleaf/processors/generator/dialects/redshift.py +192 -0
- sqlleaf/processors/generator/dialects/snowflake.py +90 -0
- sqlleaf/processors/generator/generator.py +495 -0
- sqlleaf/processors/transformer/__init__.py +31 -0
- sqlleaf/processors/transformer/base.py +899 -0
- sqlleaf/processors/transformer/call.py +9 -0
- sqlleaf/processors/transformer/copy.py +63 -0
- sqlleaf/processors/transformer/ctas.py +39 -0
- sqlleaf/processors/transformer/delete.py +14 -0
- sqlleaf/processors/transformer/execute.py +9 -0
- sqlleaf/processors/transformer/expressions/__init__.py +11 -0
- sqlleaf/processors/transformer/expressions/functional_notation.py +22 -0
- sqlleaf/processors/transformer/expressions/row.py +451 -0
- sqlleaf/processors/transformer/expressions/values.py +318 -0
- sqlleaf/processors/transformer/insert.py +125 -0
- sqlleaf/processors/transformer/merge.py +14 -0
- sqlleaf/processors/transformer/multitable_insert.py +14 -0
- sqlleaf/processors/transformer/replace.py +23 -0
- sqlleaf/processors/transformer/transformer.py +141 -0
- sqlleaf/processors/transformer/udf.py +235 -0
- sqlleaf/processors/transformer/unload.py +30 -0
- sqlleaf/processors/transformer/update.py +174 -0
- sqlleaf/processors/transformer/values.py +10 -0
- sqlleaf/settings/__init__.py +19 -0
- sqlleaf/settings/athena.py +23 -0
- sqlleaf/settings/base.py +37 -0
- sqlleaf/settings/postgres.py +52 -0
- sqlleaf/system_tables/load.py +0 -0
- sqlleaf/typing.py +93 -0
- sqlleaf/util/__init__.py +34 -0
- sqlleaf/util/expression.py +282 -0
- sqlleaf/util/graph.py +116 -0
- sqlleaf/util/helpers.py +42 -0
- sqlleaf/util/iterators.py +37 -0
- sqlleaf-0.0.1.dist-info/METADATA +46 -0
- sqlleaf-0.0.1.dist-info/RECORD +111 -0
- sqlleaf-0.0.1.dist-info/WHEEL +5 -0
- sqlleaf-0.0.1.dist-info/licenses/LICENSE +21 -0
- sqlleaf-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
]
|