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