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,182 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+ from dataclasses import replace
6
+
7
+ from sqlglot import exp
8
+
9
+ from sqlleaf import exception, util
10
+ from sqlleaf.models.context import GeneratorContext, PositionContext
11
+ from sqlleaf.models.node import (
12
+ ColumnNode,
13
+ SequenceNode,
14
+ )
15
+ from sqlleaf.processors.generator.dialects.base import BaseGenerator, EdgeToCreate, singledispatchmethodlogger
16
+ from sqlleaf.typing import SourceInfo, SqlObjectType
17
+
18
+ logger = logging.getLogger("sqlleaf")
19
+
20
+
21
+ class PostgresGenerator(BaseGenerator):
22
+ dialect = "postgres"
23
+
24
+ @singledispatchmethodlogger
25
+ def process(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
26
+ yield from super().process(expr, gen_ctx, pos_ctx)
27
+
28
+ @process.register
29
+ def process_table(
30
+ self, expr: exp.Table, gen_ctx: GeneratorContext, pos_ctx: PositionContext
31
+ ) -> t.Iterator[EdgeToCreate]:
32
+ """
33
+ Process a table or a table function.
34
+ This is a bit awkward as we have the sequence: Table -> ColumnDef -> Table
35
+ for table functions.
36
+ """
37
+ if "rows_from" in expr.args:
38
+ yield from self._process_rows_from(expr, gen_ctx, pos_ctx)
39
+ elif isinstance(expr.this, exp.Anonymous):
40
+ yield from super().process(expr.this, gen_ctx.replace(expr=expr.this), pos_ctx.replace(select_index=0))
41
+ else:
42
+ yield from super().process(expr, gen_ctx, pos_ctx)
43
+
44
+ def _process_rows_from(
45
+ self, expr: exp.TableColumn, gen_ctx: GeneratorContext, pos_ctx: PositionContext
46
+ ) -> t.Iterator[EdgeToCreate]:
47
+ """
48
+ SELECT * FROM ROWS FROM
49
+ (
50
+ unnest(ARRAY['x', 'y']),
51
+ json_to_recordset('[{"a":40,"b":"foo"}]')
52
+ AS y(a INTEGER, b TEXT),
53
+ generate_series(1, 3)
54
+ ) AS x (name, age, kind, amount)
55
+ """
56
+ downstream_exprs = []
57
+ for table_function in expr.args["rows_from"]:
58
+ # Determine the immediate children of the expression.
59
+ # These are either table functions or aliases to table functions (ColumnDefs)
60
+ cols = list(table_function.find_all(exp.ColumnDef))
61
+ downstream_exprs.extend(cols if cols else [table_function])
62
+
63
+ # Get the expression associated with the column name
64
+ child_column_name = gen_ctx.get_child_node().expr.name
65
+ for i, col in enumerate(expr.alias_column_names):
66
+ if col == child_column_name:
67
+ # Returns ColumnDef | Function | Table
68
+ down_expr = downstream_exprs[i]
69
+ if isinstance(down_expr, exp.Table) and down_expr.arg_key == "rows_from":
70
+ # A table function inside a 'ROWS FROM'
71
+ down_expr = down_expr.this
72
+
73
+ gen_ctx = gen_ctx.replace(expr=down_expr)
74
+ yield from self.process(down_expr, gen_ctx, pos_ctx.replace(select_index=i))
75
+ return
76
+
77
+ @process.register
78
+ def process_lateral(
79
+ self, expr: exp.Lateral, gen_ctx: GeneratorContext, pos_ctx: PositionContext
80
+ ) -> t.Iterator[EdgeToCreate]:
81
+ """
82
+ Example query: "SELECT .. LATERAL function() AS t(name)"
83
+ This only handles cases where a function follows LATERAL. If a subquery follows instead,
84
+ its expressions are walked inside walk_query_scope().
85
+ """
86
+ gen_ctx = gen_ctx.replace(expr=expr.this)
87
+ yield from self.process(expr.this, gen_ctx, pos_ctx)
88
+
89
+ @process.register
90
+ def process_anonymous(
91
+ self, expr: exp.Anonymous, gen_ctx: GeneratorContext, pos_ctx: PositionContext
92
+ ) -> t.Iterator[EdgeToCreate]:
93
+ """
94
+ Either user-defined functions or sequence functions.
95
+
96
+ SELECT my.func() or SELECT nextval('my_sequence')
97
+ """
98
+ schema, function = util.get_udf_name(expr)
99
+
100
+ # Process a sequence
101
+ if not schema and function in [
102
+ "nextval",
103
+ "currval",
104
+ "setval",
105
+ # 'lastval()' is not supported since it requires tracking state
106
+ ]:
107
+ yield from self._process_sequence(expr, gen_ctx, pos_ctx)
108
+ else:
109
+ yield from super().process(expr, gen_ctx, pos_ctx)
110
+
111
+ def _process_sequence(
112
+ self, expr: exp.Anonymous, gen_ctx: GeneratorContext, pos_ctx: PositionContext
113
+ ) -> t.Iterator[EdgeToCreate]:
114
+ """
115
+ Either user-defined functions or sequence functions.
116
+
117
+ SELECT my.func() or SELECT nextval('my_sequence')
118
+ """
119
+ seq_name_expr: exp.Literal = expr.args["expressions"][0]
120
+ schema, function = util.get_udf_name(expr)
121
+ full_name = ".".join([schema, function])
122
+
123
+ # Ensure the sequence exists
124
+ seq_table = exp.table_(table=seq_name_expr.name, db=schema)
125
+ seq_query = gen_ctx.query.object_mapping.lookup_sequence_query(table=seq_table)
126
+ if not seq_query:
127
+ raise exception.MappingError(f"Sequence '{full_name}' not found.")
128
+
129
+ subkind = seq_query.property if seq_query else ""
130
+ gen_ctx = gen_ctx.replace(new_data_type=exp.DataType.build("INT"))
131
+ parent = SequenceNode(name=seq_name_expr.name, gen_ctx=gen_ctx, pos_ctx=pos_ctx, subkind=subkind)
132
+ yield EdgeToCreate(parent, gen_ctx.child_node)
133
+
134
+ @process.register
135
+ def process_column_def(
136
+ self, expr: exp.ColumnDef, gen_ctx: GeneratorContext, pos_ctx: PositionContext
137
+ ) -> t.Iterator[EdgeToCreate]:
138
+ gen_ctx = gen_ctx.replace(new_data_type=expr.kind)
139
+
140
+ if isinstance(expr.parent, exp.TableAlias):
141
+ # A table alias inside a 'ROWS FROM' cannot have a name
142
+ parent = ColumnNode(
143
+ catalog="",
144
+ schema="",
145
+ table="",
146
+ column=expr.name,
147
+ gen_ctx=gen_ctx,
148
+ pos_ctx=pos_ctx,
149
+ )
150
+ yield EdgeToCreate(parent, gen_ctx.child_node)
151
+
152
+ # Process the inner table function of the table alias
153
+ table_function: exp.Table = t.cast(exp.Table, expr.parent.parent)
154
+ yield from self.do_grandparents([table_function.this], parent, gen_ctx, pos_ctx.replace(select_index=0))
155
+
156
+ @process.register
157
+ def process_column(
158
+ self, expr: exp.Column, gen_ctx: GeneratorContext, pos_ctx: PositionContext
159
+ ) -> t.Iterator[EdgeToCreate]:
160
+ """
161
+ COPY x FROM/TO y
162
+ """
163
+ # TODO: create a new Node called column[kind=table subkind=function] for table functions
164
+ # (JSON_EACH_TEXT, UNNEST, UDFs that return sets)
165
+ source_info: SourceInfo = gen_ctx.query.source_info
166
+
167
+ if SqlObjectType.type_has_no_column_defs(source_info.type):
168
+ if source_info.type == SqlObjectType.FILE:
169
+ gen_ctx = gen_ctx.replace(
170
+ expr=source_info.expression, new_data_type=gen_ctx.get_child_node().get_data_type()
171
+ )
172
+
173
+ node = self.create_node_from_type(
174
+ object_type=source_info.type,
175
+ expression=source_info.expression,
176
+ column_name=gen_ctx.get_child_node().name,
177
+ gen_ctx=gen_ctx,
178
+ pos_ctx=pos_ctx,
179
+ )
180
+ yield EdgeToCreate(node, gen_ctx.child_node)
181
+ else:
182
+ yield from super().process(expr, gen_ctx, pos_ctx)
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+ from dataclasses import replace
6
+
7
+ from sqlglot import exp
8
+ from sqlglot.optimizer import Scope
9
+
10
+ from sqlleaf import exception
11
+ from sqlleaf.models.context import GeneratorContext, PositionContext
12
+ from sqlleaf.models.node import (
13
+ FileColumnNode,
14
+ PivotNode,
15
+ UnpivotNode,
16
+ )
17
+ from sqlleaf.processors.generator.dialects.base import BaseGenerator, EdgeToCreate, singledispatchmethodlogger
18
+ from sqlleaf.typing import SourceInfo, SqlObjectType
19
+
20
+ logger = logging.getLogger("sqlleaf")
21
+
22
+
23
+ class RedshiftGenerator(BaseGenerator):
24
+ dialect = "redshift"
25
+
26
+ @singledispatchmethodlogger
27
+ def process(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
28
+ yield from super().process(expr, gen_ctx, pos_ctx)
29
+
30
+ @process.register
31
+ def process_unpivot(
32
+ self, expr: exp.Pivot, gen_ctx: GeneratorContext, pos_ctx: PositionContext
33
+ ) -> t.Iterator[EdgeToCreate]:
34
+ """
35
+ SELECT * FROM ... UNPIVOT ( ... )
36
+ """
37
+ # Given expression:
38
+ # UNPIVOT ( <expression> FOR <field> IN (<column> AS <value>) )
39
+ # We have lineage:
40
+ # <column> -> UNPIVOT -> <expression>
41
+ # <value> -> UNPIVOT -> <field>
42
+ scope = t.cast(Scope, gen_ctx.scope)
43
+ selected_column = scope.columns[pos_ctx.select_index]
44
+ pivot_expression = expr.expressions[0]
45
+ pivot_field = expr.fields[0]
46
+
47
+ if selected_column.name == pivot_expression.name:
48
+ arg = "this"
49
+ elif selected_column.name == pivot_field.this.name:
50
+ arg = "alias"
51
+ else:
52
+ message = f"Could not find column '{selected_column.name}' in UNPIVOT expression"
53
+ raise exception.SqlLeafException(message=message)
54
+
55
+ # Create an unpivot node for each downstream column/expression
56
+ for pivot_alias in pivot_field.expressions:
57
+ pivot_value = pivot_alias.args[arg]
58
+
59
+ unpivot_node = self.create_node(UnpivotNode(
60
+ gen_ctx=gen_ctx,
61
+ pos_ctx=pos_ctx,
62
+ ))
63
+ if unpivot_node:
64
+ source = pivot_value.name if arg == "this" else "" # Only columns are sources for now
65
+ unpivot_node.set(source=source, target=selected_column.name)
66
+ yield EdgeToCreate(unpivot_node, gen_ctx.child_node)
67
+
68
+ yield from self.do_grandparents([pivot_value], unpivot_node, gen_ctx, pos_ctx)
69
+
70
+ @process.register
71
+ def process_pivot(
72
+ self, expr: exp.Pivot, gen_ctx: GeneratorContext, pos_ctx: PositionContext
73
+ ) -> t.Iterator[EdgeToCreate]:
74
+ """
75
+ SELECT * FROM (SELECT ...) PIVOT ( ... )
76
+ """
77
+ # Find the associated expression for the column, and process it
78
+ scope = t.cast(Scope, gen_ctx.scope)
79
+ selected_column = scope.columns[pos_ctx.select_index]
80
+ pivot_column_mapping = _get_pivot_mapping(expr)
81
+
82
+ # The associated column and expression
83
+ column_and_expr = pivot_column_mapping[selected_column.name]
84
+ pivot_expr = column_and_expr["expression"]
85
+
86
+ pivot_node = self.create_node(PivotNode(
87
+ gen_ctx=gen_ctx,
88
+ pos_ctx=pos_ctx,
89
+ ))
90
+ if pivot_node:
91
+ pivot_node.set(source=pivot_expr.alias_or_name, target=selected_column.alias_or_name)
92
+ yield EdgeToCreate(pivot_node, gen_ctx.child_node)
93
+
94
+ grandparents = [pivot_expr]
95
+ yield from self.do_grandparents(grandparents, pivot_node, gen_ctx, pos_ctx)
96
+
97
+ @process.register
98
+ def process_column(
99
+ self, expr: exp.Column, gen_ctx: GeneratorContext, pos_ctx: PositionContext
100
+ ) -> t.Iterator[EdgeToCreate]:
101
+ scope = t.cast(Scope, gen_ctx.scope)
102
+ pivot = _get_pivot_expr(scope)
103
+ if (
104
+ pivot
105
+ and pivot.alias_or_name == expr.table
106
+ and not isinstance(gen_ctx.child_node, UnpivotNode) # Prevent infinite recursion
107
+ ):
108
+ gen_ctx = gen_ctx.replace(expr=pivot)
109
+ if pivot.unpivot:
110
+ yield from self.process_unpivot(pivot, gen_ctx, pos_ctx)
111
+ else:
112
+ yield from self.process_pivot(pivot, gen_ctx, pos_ctx)
113
+ else:
114
+ yield from self._process_column_other(expr, gen_ctx, pos_ctx)
115
+
116
+ def _process_column_other(
117
+ self, expr: exp.Column, gen_ctx: GeneratorContext, pos_ctx: PositionContext
118
+ ) -> t.Iterator[EdgeToCreate]:
119
+ source_info: SourceInfo = gen_ctx.query.source_info
120
+
121
+ if SqlObjectType.type_has_no_column_defs(source_info.type):
122
+ # A filename from COPY/UNLOAD
123
+ gen_ctx = gen_ctx.replace(
124
+ expr=source_info.expression, new_data_type=gen_ctx.get_child_node().get_data_type()
125
+ )
126
+
127
+ node = self.create_node_from_type(
128
+ object_type=source_info.type,
129
+ expression=source_info.expression,
130
+ column_name=gen_ctx.get_child_node().name,
131
+ gen_ctx=gen_ctx,
132
+ pos_ctx=pos_ctx,
133
+ )
134
+ yield EdgeToCreate(node, gen_ctx.child_node)
135
+ else:
136
+ yield from super().process(expr, gen_ctx, pos_ctx)
137
+
138
+ @process.register
139
+ def process_location(
140
+ self, expr: exp.LocationProperty, gen_ctx: GeneratorContext, pos_ctx: PositionContext
141
+ ) -> t.Iterator[EdgeToCreate]:
142
+ """
143
+ CREATE EXTERNAL TABLE ... LOCATION
144
+ """
145
+ location = expr.this
146
+ child_node = gen_ctx.child_node
147
+
148
+ # TODO: change to source/target info
149
+
150
+ file_format_property = gen_ctx.query.statement.args["properties"].find(exp.FileFormatProperty)
151
+ file_format = file_format_property.this.name if file_format_property else ""
152
+
153
+ column_node = self.create_node(FileColumnNode(
154
+ column=child_node.name,
155
+ file_format=file_format,
156
+ file_path=location.name,
157
+ gen_ctx=gen_ctx,
158
+ pos_ctx=pos_ctx,
159
+ ))
160
+
161
+ yield EdgeToCreate(column_node, child_node)
162
+
163
+
164
+ def _get_pivot_expr(scope: Scope) -> exp.Pivot | None:
165
+ pivots = scope.pivots if scope else []
166
+ pivot = pivots[0] if len(pivots) == 1 else None
167
+ return pivot
168
+
169
+
170
+ def _get_pivot_mapping(expr: exp.Pivot) -> dict:
171
+ """
172
+ Get information related to PIVOT statements.
173
+ """
174
+ pivot_column_mapping = {}
175
+ # For each aggregation function, the pivot creates a new column for each field in category
176
+ # combined with the aggfunc. So the columns parsed have this order: cat_a_value_sum, cat_a,
177
+ # b_value_sum, b. Because of this step wise manner the aggfunc 'sum(value) as value_sum'
178
+ # belongs to the column indices 0, 2, and the aggfunc 'max(price)' without an alias belongs
179
+ # to the column indices 1, 3. Here, only the columns used in the aggregations are of interest
180
+ # in the lineage, so lookup the pivot column name by index and map that with the columns used
181
+ # in the aggregation.
182
+ #
183
+ # Example: PIVOT (SUM(value) AS value_sum, MAX(price)) FOR category IN ('a' AS cat_a, 'b')
184
+ pivot_columns = expr.args["columns"]
185
+ pivot_aggs_count = len(expr.expressions)
186
+
187
+ for i, agg in enumerate(expr.expressions):
188
+ agg_cols = list(agg.find_all(exp.Column))
189
+ for col_index in range(i, len(pivot_columns), pivot_aggs_count):
190
+ pivot_column_mapping[pivot_columns[col_index].name] = {"column": agg_cols, "expression": agg}
191
+
192
+ return pivot_column_mapping
@@ -0,0 +1,90 @@
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.models.node import (
10
+ FileColumnNode,
11
+ StageColumnNode,
12
+ )
13
+ from sqlleaf.processors.generator.dialects.base import BaseGenerator, EdgeToCreate, singledispatchmethodlogger
14
+ from sqlleaf.typing import SqlObjectType
15
+
16
+ logger = logging.getLogger("sqlleaf")
17
+
18
+
19
+ class SnowflakeGenerator(BaseGenerator):
20
+ dialect = "snowflake"
21
+
22
+ @singledispatchmethodlogger
23
+ def process(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
24
+ yield from super().process(expr, gen_ctx, pos_ctx)
25
+
26
+ @process.register
27
+ def process_put(
28
+ self, expr: exp.Put, gen_ctx: GeneratorContext, pos_ctx: PositionContext
29
+ ) -> t.Iterator[EdgeToCreate]:
30
+ """
31
+ PUT 'file:///tmp/data/mydata.csv' @my_int_stage;
32
+ - Creates two node: FileColumnNode and StageColumnNode
33
+ """
34
+ source = gen_ctx.query.source_info.expression
35
+ target = gen_ctx.query.target_info.expression
36
+
37
+ file_format = gen_ctx.query.get_original_self().parameters.file_format
38
+
39
+ file_ctx = gen_ctx.replace(expr=source)
40
+ stage_ctx = gen_ctx.replace(expr=target)
41
+
42
+ stage_query = gen_ctx.query.object_mapping.get_table_or_stage(table=target, raise_on_missing=False)
43
+
44
+ file_node = self.create_node(FileColumnNode(
45
+ column="?",
46
+ file_format=file_format,
47
+ file_path=source.name,
48
+ gen_ctx=file_ctx,
49
+ pos_ctx=pos_ctx,
50
+ ))
51
+ stage_node = self.create_node(StageColumnNode(
52
+ column="?",
53
+ stage=target,
54
+ gen_ctx=stage_ctx,
55
+ pos_ctx=pos_ctx,
56
+ path=stage_query.path,
57
+ ))
58
+
59
+ yield EdgeToCreate(file_node, stage_node)
60
+
61
+ @process.register
62
+ def process_copy(
63
+ self, expr: exp.Copy, gen_ctx: GeneratorContext, pos_ctx: PositionContext
64
+ ) -> t.Iterator[EdgeToCreate]:
65
+ yield from self.process_column(expr, gen_ctx, pos_ctx)
66
+
67
+ @process.register
68
+ def process_column(
69
+ self, expr: exp.Column, gen_ctx: GeneratorContext, pos_ctx: PositionContext
70
+ ) -> t.Iterator[EdgeToCreate]:
71
+ """
72
+ If the source is a Stage, create a StageColumnNode.
73
+ """
74
+ query = gen_ctx.query
75
+ if hasattr(query, "source_info") and query.source_info.type == SqlObjectType.STAGE:
76
+ stage_expr = query.source_info.expression
77
+ stage_name: exp.Var = stage_expr.this
78
+ stage_query = query.object_mapping.get_table_or_stage(table=stage_expr, raise_on_missing=False)
79
+ stage_path = stage_query.path
80
+
81
+ parent = self.create_node(StageColumnNode(
82
+ column=expr.name,
83
+ stage=stage_name,
84
+ gen_ctx=gen_ctx,
85
+ pos_ctx=pos_ctx,
86
+ path=stage_path,
87
+ ))
88
+ yield EdgeToCreate(parent, gen_ctx.child_node)
89
+ else:
90
+ yield from super().process_column(expr, gen_ctx, pos_ctx)