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,572 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import singledispatchmethod
|
|
7
|
+
|
|
8
|
+
from sqlglot import exp
|
|
9
|
+
from sqlglot.optimizer import Scope
|
|
10
|
+
from sqlglot.optimizer.scope import ScopeType
|
|
11
|
+
|
|
12
|
+
from sqlleaf import exception, util
|
|
13
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
14
|
+
from sqlleaf.models.hook import Hook
|
|
15
|
+
from sqlleaf.models.node import (
|
|
16
|
+
ColumnNode,
|
|
17
|
+
DynamoDbNode,
|
|
18
|
+
FileColumnNode,
|
|
19
|
+
FunctionNode,
|
|
20
|
+
IntervalNode,
|
|
21
|
+
JsonPathNode,
|
|
22
|
+
LiteralNode,
|
|
23
|
+
N,
|
|
24
|
+
NullNode,
|
|
25
|
+
ProgramNode,
|
|
26
|
+
StageColumnNode,
|
|
27
|
+
StarNode,
|
|
28
|
+
StreamNode,
|
|
29
|
+
TargetNodeType,
|
|
30
|
+
UserDefinedFunctionNode,
|
|
31
|
+
VarNode,
|
|
32
|
+
WindowNode,
|
|
33
|
+
)
|
|
34
|
+
from sqlleaf.models.query import TableQuery
|
|
35
|
+
from sqlleaf.typing import SourceExprType, SqlObjectType, TargetExprType
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger("sqlleaf")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SingleDispatchMethodLogger(singledispatchmethod):
|
|
41
|
+
"""
|
|
42
|
+
Override the functools.singledispatchmethod class to print the methods that get called.
|
|
43
|
+
Used for debugging purposes.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __get__(self, obj: t.Any, cls: t.Any = None) -> t.Any:
|
|
47
|
+
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
48
|
+
"""
|
|
49
|
+
Intercept execution and print the function calls.
|
|
50
|
+
"""
|
|
51
|
+
target_type = type(args[0])
|
|
52
|
+
actual_func = self.dispatcher.dispatch(target_type)
|
|
53
|
+
|
|
54
|
+
class_name, func_name = actual_func.__qualname__.rsplit(".", 1)
|
|
55
|
+
logger.debug(f"Dispatching to: '{func_name}' ({class_name}) for expr: {type(args[0])}")
|
|
56
|
+
|
|
57
|
+
result = actual_func(obj, *args, **kwargs)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
t.cast(t.Any, wrapper).register = self.register
|
|
61
|
+
return wrapper
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
singledispatchmethodlogger = SingleDispatchMethodLogger
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class EdgeToCreate[N]:
|
|
69
|
+
parent: N | None
|
|
70
|
+
child: N | None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BaseGenerator:
|
|
74
|
+
# A registry to store subclasses
|
|
75
|
+
_dialects = {}
|
|
76
|
+
dialect = ""
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self.hooks = {}
|
|
80
|
+
|
|
81
|
+
@singledispatchmethodlogger
|
|
82
|
+
def process(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
|
|
83
|
+
# See the functions below for correct examples.
|
|
84
|
+
raise exception.GeneratorError(message=f"Type is not yet registered with a method: {type(expr)}")
|
|
85
|
+
|
|
86
|
+
def __init_subclass__(cls, **kwargs):
|
|
87
|
+
"""Automatically registers subclasses when they are defined."""
|
|
88
|
+
super().__init_subclass__(**kwargs)
|
|
89
|
+
BaseGenerator._dialects[cls.dialect] = cls
|
|
90
|
+
|
|
91
|
+
@process.register(exp.DataType)
|
|
92
|
+
@process.register(exp.Identifier)
|
|
93
|
+
@process.register(exp.ColumnDef)
|
|
94
|
+
@process.register(exp.Table)
|
|
95
|
+
def skip(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
|
|
96
|
+
"""
|
|
97
|
+
This causes tree traversal to stop.
|
|
98
|
+
"""
|
|
99
|
+
logger.debug(f"Skipping expression: {type(expr)} {str(expr)}")
|
|
100
|
+
yield EdgeToCreate(None, None)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_dialect(cls, class_name) -> BaseGenerator:
|
|
104
|
+
"""Instantiates a class from the registry by name."""
|
|
105
|
+
target_class = cls._dialects.get(class_name)
|
|
106
|
+
if not target_class:
|
|
107
|
+
return BaseGenerator()
|
|
108
|
+
return target_class()
|
|
109
|
+
|
|
110
|
+
def add_hooks(self, hooks: list[Hook]) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Add user-defined hooks.
|
|
113
|
+
"""
|
|
114
|
+
self.hooks: dict[type, Hook] = {h.kind: h for h in hooks}
|
|
115
|
+
|
|
116
|
+
def do_grandparents(
|
|
117
|
+
self,
|
|
118
|
+
grandparents: t.List[exp.Expr],
|
|
119
|
+
parent: t.Optional[N],
|
|
120
|
+
gen_ctx: GeneratorContext,
|
|
121
|
+
pos_ctx: PositionContext,
|
|
122
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
123
|
+
"""
|
|
124
|
+
Process a list of grandparents, which are parents of parent expressions.
|
|
125
|
+
|
|
126
|
+
This is called after a parent->child edge as been processed, and now we
|
|
127
|
+
have additional expressions to now process, i.e. [grandparents]->parent->child
|
|
128
|
+
"""
|
|
129
|
+
if parent is None:
|
|
130
|
+
if gen_ctx.child_node is None:
|
|
131
|
+
# Catches programming errors when adding new visitors
|
|
132
|
+
raise exception.GeneratorError(message="A parent cannot be None when processing grandparents.")
|
|
133
|
+
|
|
134
|
+
# This may occur due to a user-provided hook function that turned the parent into None.
|
|
135
|
+
# For example, perhaps we attempted to create the path [A -> B -> C] in the graph, but the hook prevented the
|
|
136
|
+
# creation of B; thus we set A is the previous node and C as the next, resulting in the path [A -> C].
|
|
137
|
+
parent = gen_ctx.child_node
|
|
138
|
+
|
|
139
|
+
if parent.kind in ["function", "udf"]:
|
|
140
|
+
pos_ctx = pos_ctx.replace(function_depth=pos_ctx.function_depth + 1)
|
|
141
|
+
|
|
142
|
+
for grand_expr in grandparents:
|
|
143
|
+
gen_ctx = gen_ctx.replace(expr=grand_expr, child_node=parent)
|
|
144
|
+
yield from self.process(gen_ctx.expr, gen_ctx=gen_ctx, pos_ctx=pos_ctx)
|
|
145
|
+
pos_ctx = pos_ctx.replace(function_arg_index=pos_ctx.function_arg_index + 1)
|
|
146
|
+
|
|
147
|
+
def create_node(self, node: N) -> N | None:
|
|
148
|
+
# Run against the hooks
|
|
149
|
+
hook = self.hooks.get(type(node))
|
|
150
|
+
if hook:
|
|
151
|
+
logger.debug(f"Running hook '{hook.name}' on node type '{node.__class__.__name__}'")
|
|
152
|
+
result = hook.func(node)
|
|
153
|
+
return result
|
|
154
|
+
return node
|
|
155
|
+
|
|
156
|
+
@process.register(exp.AtTimeZone)
|
|
157
|
+
@process.register(exp.Func)
|
|
158
|
+
def process_function(
|
|
159
|
+
self, expr: exp.Func, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
160
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
161
|
+
parent = self.create_node(FunctionNode(gen_ctx, pos_ctx))
|
|
162
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
163
|
+
|
|
164
|
+
grandparents = util.get_function_args(expr=expr)
|
|
165
|
+
yield from self.do_grandparents(grandparents, parent, gen_ctx, pos_ctx)
|
|
166
|
+
|
|
167
|
+
@process.register
|
|
168
|
+
def process_tuple(
|
|
169
|
+
self, expr: exp.Tuple, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
170
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
171
|
+
parent = gen_ctx.child_node
|
|
172
|
+
grandparents = expr.expressions
|
|
173
|
+
yield from self.do_grandparents(grandparents, parent, gen_ctx, pos_ctx)
|
|
174
|
+
|
|
175
|
+
@process.register
|
|
176
|
+
def process_array(
|
|
177
|
+
self, expr: exp.Array, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
178
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
179
|
+
"""
|
|
180
|
+
SELECT ARRAY[1,2,3]
|
|
181
|
+
"""
|
|
182
|
+
values = [str(e) for e in expr.expressions]
|
|
183
|
+
values = "{" + ",".join(values) + "}"
|
|
184
|
+
parent = self.create_node(LiteralNode(name=values, gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
185
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
186
|
+
|
|
187
|
+
@process.register
|
|
188
|
+
def process_window(
|
|
189
|
+
self, expr: exp.Window, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
190
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
191
|
+
"""
|
|
192
|
+
SELECT ROW_NUMBER() OVER (ORDER BY name DESC) AS amount
|
|
193
|
+
"""
|
|
194
|
+
parent = self.create_node(WindowNode(gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
195
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
196
|
+
|
|
197
|
+
@process.register(exp.Literal)
|
|
198
|
+
@process.register(exp.Boolean)
|
|
199
|
+
def process_literal(
|
|
200
|
+
self, expr: exp.Literal, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
201
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
202
|
+
"""
|
|
203
|
+
select 'hello' as greeting
|
|
204
|
+
"""
|
|
205
|
+
parent = self.create_node(LiteralNode(name=expr.sql(), gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
206
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
207
|
+
|
|
208
|
+
@process.register
|
|
209
|
+
def process_star(
|
|
210
|
+
self, expr: exp.Star, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
211
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
212
|
+
"""
|
|
213
|
+
select count(*) as cnt
|
|
214
|
+
"""
|
|
215
|
+
parent = self.create_node(StarNode(gen_ctx, pos_ctx, name="*"))
|
|
216
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
217
|
+
|
|
218
|
+
@process.register
|
|
219
|
+
def process_null(
|
|
220
|
+
self, expr: exp.Null, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
221
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
222
|
+
parent = self.create_node(NullNode(gen_ctx, pos_ctx))
|
|
223
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
224
|
+
|
|
225
|
+
@process.register
|
|
226
|
+
def process_neg(
|
|
227
|
+
self, expr: exp.Neg, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
228
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
229
|
+
"""
|
|
230
|
+
SELECT -10
|
|
231
|
+
"""
|
|
232
|
+
parent = self.create_node(LiteralNode(name="-" + expr.name, gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
233
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
234
|
+
|
|
235
|
+
@process.register
|
|
236
|
+
def process_anonymous(
|
|
237
|
+
self, expr: exp.Anonymous, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
238
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
239
|
+
"""
|
|
240
|
+
Functions or user-defined functions, e.g. SELECT my.func()
|
|
241
|
+
sqlglot recognises most functions as 'Anonymous', so we assume they're functions
|
|
242
|
+
unless they're in the mapping.
|
|
243
|
+
"""
|
|
244
|
+
schema, function = util.get_udf_name(expr)
|
|
245
|
+
node_args = expr.expressions
|
|
246
|
+
# node_args = list(expr.flatten())
|
|
247
|
+
|
|
248
|
+
# A function has to be registered to be a UDF
|
|
249
|
+
udf_query = gen_ctx.query.object_mapping.lookup_udf_call(expr)
|
|
250
|
+
if udf_query:
|
|
251
|
+
parent = self.create_node(UserDefinedFunctionNode(schema=schema, gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
252
|
+
else:
|
|
253
|
+
parent = self.create_node(FunctionNode(gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
254
|
+
|
|
255
|
+
# TODO: pass the type to the above
|
|
256
|
+
|
|
257
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
258
|
+
|
|
259
|
+
grandparents = node_args
|
|
260
|
+
yield from self.do_grandparents(grandparents, parent, gen_ctx, pos_ctx)
|
|
261
|
+
|
|
262
|
+
@process.register
|
|
263
|
+
def process_within_group(
|
|
264
|
+
self, expr: exp.WithinGroup, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
265
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
266
|
+
"""
|
|
267
|
+
SELECT MODE() WITHIN GROUP (ORDER BY name DESC) AS name
|
|
268
|
+
"""
|
|
269
|
+
gen_ctx = gen_ctx.replace(expr=expr.this)
|
|
270
|
+
yield from self.process(expr.this, gen_ctx, pos_ctx)
|
|
271
|
+
|
|
272
|
+
@process.register
|
|
273
|
+
def process_select(
|
|
274
|
+
self, expr: exp.Select, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
275
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
276
|
+
"""
|
|
277
|
+
SELECT (SELECT 1) AS name
|
|
278
|
+
^-------
|
|
279
|
+
"""
|
|
280
|
+
# This is processed by the traversal into the subquery
|
|
281
|
+
yield EdgeToCreate(None, None)
|
|
282
|
+
|
|
283
|
+
@process.register
|
|
284
|
+
def process_case(
|
|
285
|
+
self, expr: exp.Case, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
286
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
287
|
+
"""
|
|
288
|
+
SELECT CASE WHEN count(*) > 1 THEN 1 ELSE 0 END AS my_var
|
|
289
|
+
"""
|
|
290
|
+
# If no default is specified, the default is NULL (via ANSI SQL) TODO: however in PL/pgsql it's an error instead
|
|
291
|
+
default = expr.args.get("default", exp.Null())
|
|
292
|
+
thens = [if_expr.args.get("true") or if_expr.args.get("false") for if_expr in expr.args.get("ifs", [])]
|
|
293
|
+
grandparents = [default] + thens
|
|
294
|
+
|
|
295
|
+
yield from self.do_grandparents(grandparents, gen_ctx.child_node, gen_ctx, pos_ctx)
|
|
296
|
+
|
|
297
|
+
@process.register
|
|
298
|
+
def process_binary(
|
|
299
|
+
self, expr: exp.Binary, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
300
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
301
|
+
"""
|
|
302
|
+
SELECT 1 + 2 AS age
|
|
303
|
+
"""
|
|
304
|
+
if isinstance(expr, (exp.Dot, exp.Bracket)):
|
|
305
|
+
# Field/Array access (a.b, a.b.c, a[0], a.b[0]) appears as a chain of exp.Dot/exp.Bracket nodes.
|
|
306
|
+
# Walk back to the left-most object so lineage points at the real source column, not the final field token.
|
|
307
|
+
logger.debug(f"Found {type(expr)} inside exp.Binary")
|
|
308
|
+
base: exp.Expr = expr
|
|
309
|
+
while isinstance(base, (exp.Dot, exp.Bracket)):
|
|
310
|
+
base = base.this
|
|
311
|
+
|
|
312
|
+
if isinstance(base, exp.Column):
|
|
313
|
+
# If the base is a Column, run it through normal Column handling.
|
|
314
|
+
logger.debug(f"Struct/Array access: processing base column {base.sql(dialect=self.dialect)}")
|
|
315
|
+
gen_ctx = gen_ctx.replace(expr=base)
|
|
316
|
+
yield from self.process(base, gen_ctx, pos_ctx)
|
|
317
|
+
else:
|
|
318
|
+
# If the base isn't a Column (e.g., a schema-qualified routine),
|
|
319
|
+
# process only the right-hand side so that UDF/qualified-name dispatch still works
|
|
320
|
+
gen_ctx = gen_ctx.replace(expr=expr.right if isinstance(expr, exp.Binary) else expr.this)
|
|
321
|
+
yield from self.process(gen_ctx.expr, gen_ctx, pos_ctx)
|
|
322
|
+
else:
|
|
323
|
+
parent = self.create_node(FunctionNode(gen_ctx, pos_ctx))
|
|
324
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
325
|
+
|
|
326
|
+
grandparents = [expr.left, expr.right]
|
|
327
|
+
yield from self.do_grandparents(grandparents, parent, gen_ctx, pos_ctx)
|
|
328
|
+
|
|
329
|
+
@process.register
|
|
330
|
+
def process_var(
|
|
331
|
+
self, expr: exp.Var, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
332
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
333
|
+
"""
|
|
334
|
+
Var "QUARTER" in: "SELECT EXTRACT(QUARTER FROM <DATE>)"
|
|
335
|
+
"""
|
|
336
|
+
parent = self.create_node(VarNode(gen_ctx=gen_ctx, pos_ctx=pos_ctx, name=gen_ctx.expr.name))
|
|
337
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
338
|
+
|
|
339
|
+
@process.register
|
|
340
|
+
def process_column(
|
|
341
|
+
self, expr: exp.Column, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
342
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
343
|
+
source_table = None
|
|
344
|
+
scope = gen_ctx.scope
|
|
345
|
+
|
|
346
|
+
if scope and isinstance(scope, Scope):
|
|
347
|
+
# Lateral queries are processed differently
|
|
348
|
+
if scope.scope_type == ScopeType.UDTF and isinstance(scope.expression, exp.Lateral):
|
|
349
|
+
source_table = dict(scope.lateral_sources).get(expr.table)
|
|
350
|
+
|
|
351
|
+
elif scope.scope_type == ScopeType.SUBQUERY and isinstance(expr, exp.Column):
|
|
352
|
+
source_table = dict(scope.references).get(expr.table)
|
|
353
|
+
|
|
354
|
+
if not source_table:
|
|
355
|
+
# Check the scope's parents recursively for the actual source.
|
|
356
|
+
# This occurs in a subquery, e.g. SELECT ((SELECT r.name)) FROM fruit.raw r
|
|
357
|
+
parent = scope.parent
|
|
358
|
+
while parent:
|
|
359
|
+
source_table = dict(parent.references).get(expr.table)
|
|
360
|
+
if source_table or parent.scope_type != ScopeType.SUBQUERY:
|
|
361
|
+
break
|
|
362
|
+
parent = parent.parent
|
|
363
|
+
else:
|
|
364
|
+
source_table = dict(scope.references).get(expr.table)
|
|
365
|
+
|
|
366
|
+
if source_table:
|
|
367
|
+
if not isinstance(
|
|
368
|
+
source_table,
|
|
369
|
+
(exp.Table, exp.Subquery, exp.Select, exp.Union, exp.Lateral, exp.Unnest),
|
|
370
|
+
):
|
|
371
|
+
raise exception.GeneratorError(message=f"Unexpected source type: {type(source_table)}")
|
|
372
|
+
|
|
373
|
+
parent = ColumnNode(
|
|
374
|
+
catalog=expr.catalog,
|
|
375
|
+
schema=expr.db,
|
|
376
|
+
table=expr.table,
|
|
377
|
+
column=expr.name,
|
|
378
|
+
gen_ctx=gen_ctx,
|
|
379
|
+
pos_ctx=pos_ctx,
|
|
380
|
+
source=source_table,
|
|
381
|
+
)
|
|
382
|
+
# Preserve the object as we need to access the object's properties below
|
|
383
|
+
if maybe_parent := self.create_node(parent):
|
|
384
|
+
yield EdgeToCreate(maybe_parent, gen_ctx.child_node)
|
|
385
|
+
|
|
386
|
+
if isinstance(parent.source_scope, exp.Table):
|
|
387
|
+
# Traverse into the table (esp. needed by "ROWS FROM")
|
|
388
|
+
ex = parent.source_scope
|
|
389
|
+
gen_ctx = gen_ctx.replace(expr=ex, child_node=parent)
|
|
390
|
+
yield from self.process(ex, gen_ctx, pos_ctx)
|
|
391
|
+
|
|
392
|
+
@process.register(exp.JSONExtractScalar)
|
|
393
|
+
@process.register(exp.JSONExtract)
|
|
394
|
+
@process.register(exp.JSONBExtract)
|
|
395
|
+
def process_json(
|
|
396
|
+
self, expr: exp.JSONExtract, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
397
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
398
|
+
parent = self.create_node(JsonPathNode(gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
399
|
+
|
|
400
|
+
# Get the bottom expression to extract the JSON paths
|
|
401
|
+
source = expr.this
|
|
402
|
+
while isinstance(source, (exp.JSONExtract, exp.JSONExtractScalar)):
|
|
403
|
+
source = source.this
|
|
404
|
+
|
|
405
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
406
|
+
|
|
407
|
+
gen_ctx = gen_ctx.replace(expr=source, child_node=parent)
|
|
408
|
+
yield from self.process(source, gen_ctx, pos_ctx)
|
|
409
|
+
|
|
410
|
+
@process.register
|
|
411
|
+
def process_interval(
|
|
412
|
+
self, expr: exp.Interval, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
413
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
414
|
+
parent = self.create_node(IntervalNode(gen_ctx=gen_ctx, pos_ctx=pos_ctx))
|
|
415
|
+
yield EdgeToCreate(parent, gen_ctx.child_node)
|
|
416
|
+
|
|
417
|
+
@process.register
|
|
418
|
+
def process_bracket(
|
|
419
|
+
self, expr: exp.Bracket, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
420
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
421
|
+
"""
|
|
422
|
+
Array/JSON subscripting: my_arr[0] or data['user']
|
|
423
|
+
"""
|
|
424
|
+
# Array access often behaves like a binary expression for lineage purposes (points to a base column)
|
|
425
|
+
return self.process_binary(expr, gen_ctx, pos_ctx)
|
|
426
|
+
|
|
427
|
+
@process.register
|
|
428
|
+
def process_subquery(
|
|
429
|
+
self, expr: exp.Subquery, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
430
|
+
) -> t.Iterator[EdgeToCreate]:
|
|
431
|
+
"""
|
|
432
|
+
SELECT 1 + (SELECT 2)
|
|
433
|
+
|
|
434
|
+
Given the above,
|
|
435
|
+
1 is part of SELECT 1 ...
|
|
436
|
+
2 is part of (SELECT 2 ...)
|
|
437
|
+
(SELECT 2 ...) is part of SELECT 1 ...
|
|
438
|
+
"""
|
|
439
|
+
if len(expr.selects) > 1 or isinstance(expr.this, exp.Union):
|
|
440
|
+
raise exception.GeneratorError("A subquery must return only one column")
|
|
441
|
+
|
|
442
|
+
# Update the scope to be the subquery itself, as it is a subscope
|
|
443
|
+
scope = t.cast(Scope, gen_ctx.scope)
|
|
444
|
+
logger.debug(
|
|
445
|
+
f"process_subquery: scope.subquery_scopes={len(scope.subquery_scopes)}, expr.this={expr.this.sql()[:40]}"
|
|
446
|
+
)
|
|
447
|
+
subquery_scope = [s for s in scope.subquery_scopes if s.expression == expr.this][0]
|
|
448
|
+
|
|
449
|
+
height, width = gen_ctx.scope_positions.get_position_for_expr(expr.this)
|
|
450
|
+
child_ctx = pos_ctx.replace(query_depth=height, query_width=width)
|
|
451
|
+
p_ctx = gen_ctx.replace(expr=expr.selects[0], scope=subquery_scope)
|
|
452
|
+
yield from self.process(p_ctx.expr, gen_ctx=p_ctx, pos_ctx=child_ctx)
|
|
453
|
+
|
|
454
|
+
def create_node_from_type(
|
|
455
|
+
self,
|
|
456
|
+
object_type: SqlObjectType,
|
|
457
|
+
expression: TargetExprType | SourceExprType,
|
|
458
|
+
column_name: str,
|
|
459
|
+
gen_ctx: GeneratorContext,
|
|
460
|
+
pos_ctx: PositionContext,
|
|
461
|
+
) -> TargetNodeType | None:
|
|
462
|
+
"""
|
|
463
|
+
Create a node for a given object type.
|
|
464
|
+
"""
|
|
465
|
+
match object_type:
|
|
466
|
+
case SqlObjectType.FILE:
|
|
467
|
+
file_format = gen_ctx.query.get_original_self().parameters.file_format
|
|
468
|
+
new_node = FileColumnNode(
|
|
469
|
+
column=column_name,
|
|
470
|
+
file_format=file_format,
|
|
471
|
+
file_path=expression.name,
|
|
472
|
+
gen_ctx=gen_ctx,
|
|
473
|
+
pos_ctx=pos_ctx,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
case SqlObjectType.STAGE:
|
|
477
|
+
stage_expression = expression.this if isinstance(expression, exp.Table) else expression
|
|
478
|
+
stage_query = gen_ctx.query.object_mapping.get_table_or_stage(table=expression, raise_on_missing=False)
|
|
479
|
+
new_node = StageColumnNode(
|
|
480
|
+
column=column_name,
|
|
481
|
+
stage=stage_expression,
|
|
482
|
+
gen_ctx=gen_ctx,
|
|
483
|
+
pos_ctx=pos_ctx,
|
|
484
|
+
path=stage_query.path if stage_query else "",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
case SqlObjectType.TABLE:
|
|
488
|
+
new_node = ColumnNode(
|
|
489
|
+
catalog=expression.catalog if isinstance(expression, exp.Table) else "",
|
|
490
|
+
schema=expression.db if isinstance(expression, exp.Table) else "",
|
|
491
|
+
table=expression.name,
|
|
492
|
+
column=column_name,
|
|
493
|
+
gen_ctx=gen_ctx,
|
|
494
|
+
pos_ctx=pos_ctx,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
case SqlObjectType.STREAM:
|
|
498
|
+
new_node = StreamNode(
|
|
499
|
+
name=expression.name,
|
|
500
|
+
gen_ctx=gen_ctx,
|
|
501
|
+
pos_ctx=pos_ctx,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
case SqlObjectType.PROGRAM:
|
|
505
|
+
new_node = ProgramNode(
|
|
506
|
+
gen_ctx=gen_ctx,
|
|
507
|
+
pos_ctx=pos_ctx,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
case SqlObjectType.DYNAMODB:
|
|
511
|
+
new_node = DynamoDbNode(
|
|
512
|
+
gen_ctx=gen_ctx,
|
|
513
|
+
pos_ctx=pos_ctx,
|
|
514
|
+
column=column_name,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
case _:
|
|
518
|
+
raise exception.GeneratorError(f"Unhandled case for type: {object_type}")
|
|
519
|
+
|
|
520
|
+
return self.create_node(new_node)
|
|
521
|
+
|
|
522
|
+
def iter_child_nodes(
|
|
523
|
+
self, gen_ctx: GeneratorContext, pos_ctx: PositionContext
|
|
524
|
+
) -> t.Generator[t.Tuple[TargetNodeType | None, ColumnNode | None]]:
|
|
525
|
+
"""
|
|
526
|
+
Iterate over every column of a table that was either selected in a query or has a default expression.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
# Both COPY and UNLOAD can have SELECTs as their sources, which have arbitrary
|
|
530
|
+
# columns that vary in length due to their sourcing from any table.
|
|
531
|
+
query = gen_ctx.query
|
|
532
|
+
target_type = query.target_info.type
|
|
533
|
+
expr = query.target_info.expression
|
|
534
|
+
target_columns = query.get_columns_from_target()
|
|
535
|
+
|
|
536
|
+
select_idx = 0
|
|
537
|
+
|
|
538
|
+
# Iterate over every column and yield it if it is referenced in the query.
|
|
539
|
+
for col_def in target_columns:
|
|
540
|
+
selected_node = None
|
|
541
|
+
default_node = None
|
|
542
|
+
gen_ctx = gen_ctx.replace(expr=col_def)
|
|
543
|
+
pos_ctx = pos_ctx.replace(select_index=select_idx)
|
|
544
|
+
|
|
545
|
+
# logger.debug(f"Iter nodes - found node: {target_type}")
|
|
546
|
+
child_node = self.create_node_from_type(
|
|
547
|
+
object_type=target_type,
|
|
548
|
+
expression=expr,
|
|
549
|
+
column_name=col_def.name,
|
|
550
|
+
gen_ctx=gen_ctx,
|
|
551
|
+
pos_ctx=pos_ctx,
|
|
552
|
+
)
|
|
553
|
+
process_defaults = target_type == SqlObjectType.TABLE
|
|
554
|
+
|
|
555
|
+
if col_def.name in util.get_selected_column_names(query.statement) or isinstance(query, TableQuery):
|
|
556
|
+
# Check if the column is selected.
|
|
557
|
+
# A 'CREATE TABLE' has no SELECT, so include all columns if this case.
|
|
558
|
+
selected_node = child_node
|
|
559
|
+
|
|
560
|
+
if (
|
|
561
|
+
process_defaults
|
|
562
|
+
and isinstance(child_node, ColumnNode)
|
|
563
|
+
and util.get_column_constraint_expression(child_node.expr)
|
|
564
|
+
):
|
|
565
|
+
default_node = child_node
|
|
566
|
+
# TODO: unset all index positions, set 'default=true' as position
|
|
567
|
+
|
|
568
|
+
if selected_node or default_node:
|
|
569
|
+
yield selected_node, default_node
|
|
570
|
+
|
|
571
|
+
if selected_node:
|
|
572
|
+
select_idx += 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
9
|
+
from sqlleaf.processors.generator.dialects.base import BaseGenerator, EdgeToCreate, singledispatchmethodlogger
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("sqlleaf")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MySqlGenerator(BaseGenerator):
|
|
15
|
+
dialect = "mysql"
|
|
16
|
+
|
|
17
|
+
@singledispatchmethodlogger
|
|
18
|
+
def process(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
|
|
19
|
+
yield from super().process(expr, gen_ctx, pos_ctx)
|