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