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,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
from sqlleaf import util
|
|
9
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
10
|
+
from sqlleaf.models.query import Q, QueryHolder
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NodeAttributes:
|
|
14
|
+
KIND: str = ""
|
|
15
|
+
WITH_POSITIONS: bool = False
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
gen_ctx: GeneratorContext,
|
|
20
|
+
pos_ctx: PositionContext,
|
|
21
|
+
name: str | None = "",
|
|
22
|
+
):
|
|
23
|
+
self.expr: exp.Expr = gen_ctx.expr
|
|
24
|
+
self._data_type: exp.DataType | None = gen_ctx.data_type
|
|
25
|
+
self.data_type: str = str(self._data_type) if self._data_type else ""
|
|
26
|
+
|
|
27
|
+
self.name: str = name
|
|
28
|
+
self.kind: str = self.KIND
|
|
29
|
+
self.with_positions: bool = self.WITH_POSITIONS
|
|
30
|
+
self.ctx: PositionContext = pos_ctx
|
|
31
|
+
|
|
32
|
+
# Allows the class to be used a networkx node
|
|
33
|
+
def __hash__(self) -> int:
|
|
34
|
+
return hash(self.full_name)
|
|
35
|
+
|
|
36
|
+
def get_data_type(self) -> exp.DataType:
|
|
37
|
+
assert self._data_type is not None
|
|
38
|
+
return self._data_type
|
|
39
|
+
|
|
40
|
+
def wrap(self, name: str) -> str:
|
|
41
|
+
return f"{self.kind}[{name}]"
|
|
42
|
+
|
|
43
|
+
def fields(self) -> dict[str, str]:
|
|
44
|
+
return {"type": self.data_type}
|
|
45
|
+
|
|
46
|
+
def friendly_fields(self) -> dict[str, str]:
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
def get_name(self) -> str:
|
|
50
|
+
return self.name
|
|
51
|
+
|
|
52
|
+
def _build_name(self, fields_dict: dict[str, str], with_positions: bool = False) -> str:
|
|
53
|
+
"""Build a formatted name with fields."""
|
|
54
|
+
fields_dict = fields_dict.copy()
|
|
55
|
+
main_parts = [f"name={self.get_name()}"]
|
|
56
|
+
|
|
57
|
+
type_val = fields_dict.pop("type", None)
|
|
58
|
+
if type_val:
|
|
59
|
+
main_parts.append(f"type={type_val}")
|
|
60
|
+
|
|
61
|
+
props = " ".join([f"{k}={v}" for k, v in fields_dict.items() if v])
|
|
62
|
+
if props:
|
|
63
|
+
main_parts.append(f"properties=[{props}]")
|
|
64
|
+
|
|
65
|
+
content = " ".join(main_parts)
|
|
66
|
+
if with_positions:
|
|
67
|
+
content = f"{content} position=[{self.ctx.as_str()}]"
|
|
68
|
+
|
|
69
|
+
return self.wrap(content)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def full_name(self) -> str:
|
|
73
|
+
return self._build_name(self.fields(), with_positions=self.with_positions)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def friendly_name(self) -> str:
|
|
77
|
+
name = self.get_name()
|
|
78
|
+
fields = self.friendly_fields()
|
|
79
|
+
|
|
80
|
+
parts = []
|
|
81
|
+
if name:
|
|
82
|
+
parts.append(name)
|
|
83
|
+
|
|
84
|
+
for k, v in fields.items():
|
|
85
|
+
if v:
|
|
86
|
+
parts.append(f"{k}={v}")
|
|
87
|
+
|
|
88
|
+
content = " ".join(parts)
|
|
89
|
+
return self.wrap(content)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def id(self) -> str:
|
|
93
|
+
name = "node:" + util.short_sha256_hash(self.full_name)
|
|
94
|
+
return name
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
return {
|
|
98
|
+
"id": self.id,
|
|
99
|
+
"kind": self.kind,
|
|
100
|
+
"name": self.name,
|
|
101
|
+
"data_type": self.data_type,
|
|
102
|
+
**self.fields(),
|
|
103
|
+
"position": asdict(self.ctx) if self.with_positions else {},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EdgeAttributes:
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
parent: NodeAttributes,
|
|
111
|
+
child: NodeAttributes,
|
|
112
|
+
query: Q,
|
|
113
|
+
path_idx: int,
|
|
114
|
+
):
|
|
115
|
+
self.parent = parent
|
|
116
|
+
self.child = child
|
|
117
|
+
self.query = query
|
|
118
|
+
self.path_idx = path_idx # The sequence number if a duplicate path
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def id(self) -> str:
|
|
122
|
+
edge_id = ":".join([
|
|
123
|
+
str(s)
|
|
124
|
+
for s in [
|
|
125
|
+
self.parent.id,
|
|
126
|
+
self.child.id,
|
|
127
|
+
self.path_idx,
|
|
128
|
+
]
|
|
129
|
+
])
|
|
130
|
+
return "edge:" + util.short_sha256_hash(edge_id)
|
|
131
|
+
|
|
132
|
+
def to_dict(self) -> dict:
|
|
133
|
+
result = {
|
|
134
|
+
"id": self.id,
|
|
135
|
+
"source": self.parent.id,
|
|
136
|
+
"target": self.child.id,
|
|
137
|
+
"query": self.query.id,
|
|
138
|
+
"path_index": self.path_idx,
|
|
139
|
+
}
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class GraphAttributes:
|
|
144
|
+
def __init__(self):
|
|
145
|
+
self.queries: t.List[QueryHolder] = []
|
|
146
|
+
|
|
147
|
+
def add_query_to_graph(self, holder: QueryHolder):
|
|
148
|
+
self.queries.append(holder)
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from sqlglot.optimizer import Scope
|
|
8
|
+
from sqlglot.optimizer.scope import ScopeType
|
|
9
|
+
|
|
10
|
+
from sqlleaf import exception
|
|
11
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
12
|
+
from sqlleaf.models.node import NodeAttributes
|
|
13
|
+
from sqlleaf.typing import TableOrScopeType, TableSubtype, TableType
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("sqlleaf")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ColumnNode(NodeAttributes):
|
|
19
|
+
KIND = "column"
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
catalog: str,
|
|
24
|
+
schema: str,
|
|
25
|
+
table: str,
|
|
26
|
+
column: str,
|
|
27
|
+
gen_ctx: GeneratorContext,
|
|
28
|
+
pos_ctx: PositionContext,
|
|
29
|
+
source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None = None,
|
|
30
|
+
):
|
|
31
|
+
super().__init__(gen_ctx, pos_ctx, name=column)
|
|
32
|
+
self.catalog = catalog
|
|
33
|
+
self.schema = schema
|
|
34
|
+
self.table = table
|
|
35
|
+
self.member = ""
|
|
36
|
+
|
|
37
|
+
self.parent_kind: str = ""
|
|
38
|
+
self.parent_subkind: str = ""
|
|
39
|
+
self.source_scope: TableOrScopeType | None = None
|
|
40
|
+
|
|
41
|
+
self.set_table_properties(catalog, schema, table, gen_ctx, source=source)
|
|
42
|
+
if source is not None and not isinstance(source, (exp.Subquery, Scope)):
|
|
43
|
+
self._apply_rename(source, gen_ctx.query.dialect)
|
|
44
|
+
|
|
45
|
+
# TODO: new algorithm
|
|
46
|
+
# if table_type == "cte":
|
|
47
|
+
# self.member = gen_ctx.node.recursive_cte_member_kind
|
|
48
|
+
|
|
49
|
+
def to_dict(self):
|
|
50
|
+
d = super().to_dict()
|
|
51
|
+
d.update({
|
|
52
|
+
"catalog": self.catalog,
|
|
53
|
+
"schema": self.schema,
|
|
54
|
+
"table": self.table,
|
|
55
|
+
})
|
|
56
|
+
return d
|
|
57
|
+
|
|
58
|
+
def _apply_rename(self, source: exp.Table | exp.Select | exp.Lateral, dialect: str) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Change the column's source table to be its fully qualified name, not its alias,
|
|
61
|
+
so that the ColumnNode is provided complete information.
|
|
62
|
+
"""
|
|
63
|
+
column = t.cast(exp.Column, self.expr)
|
|
64
|
+
_c = column.copy()
|
|
65
|
+
|
|
66
|
+
if isinstance(source, exp.Table):
|
|
67
|
+
if source.catalog:
|
|
68
|
+
column.set("catalog", exp.to_identifier(source.catalog))
|
|
69
|
+
if source.db:
|
|
70
|
+
column.set("db", exp.to_identifier(source.db))
|
|
71
|
+
if source.name:
|
|
72
|
+
if dialect == "snowflake":
|
|
73
|
+
if source.this.args.get("quoted", False): # exp.Identifier
|
|
74
|
+
column.set("table", exp.to_identifier(source.name))
|
|
75
|
+
else:
|
|
76
|
+
column.set("table", exp.to_identifier(source.name))
|
|
77
|
+
if _c != column:
|
|
78
|
+
logger.debug(f"Renamed node {_c.sql()} to {column.sql()}")
|
|
79
|
+
|
|
80
|
+
self.expr = column
|
|
81
|
+
self.catalog = column.catalog
|
|
82
|
+
self.schema = column.db
|
|
83
|
+
self.table = column.table
|
|
84
|
+
|
|
85
|
+
def set_table_properties(
|
|
86
|
+
self,
|
|
87
|
+
catalog: str,
|
|
88
|
+
schema: str,
|
|
89
|
+
table: str,
|
|
90
|
+
gen_ctx: GeneratorContext,
|
|
91
|
+
source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Figure out the table's kind (view, table, etc) and its subkind (temp, recursive, etc) by
|
|
95
|
+
inspecting the query's scope and the table mapping.
|
|
96
|
+
"""
|
|
97
|
+
scope = gen_ctx.scope
|
|
98
|
+
|
|
99
|
+
# logical_source is what we use to determine CTE/UDTF etc.
|
|
100
|
+
logical_source = source
|
|
101
|
+
if isinstance(scope, Scope):
|
|
102
|
+
# Always try to get the Scope from sources if it exists for this alias
|
|
103
|
+
logical_source = scope.sources.get(table) or source
|
|
104
|
+
|
|
105
|
+
if not logical_source:
|
|
106
|
+
# Nested 'rows_from' queries have their aliases in 'references'
|
|
107
|
+
logical_source = dict(scope.references).get(table)
|
|
108
|
+
|
|
109
|
+
self.source_scope = logical_source
|
|
110
|
+
|
|
111
|
+
# If it's in references but NOT in sources, it's a derived table (e.g. ROWS FROM)
|
|
112
|
+
if (
|
|
113
|
+
logical_source is not None
|
|
114
|
+
and isinstance(scope, Scope)
|
|
115
|
+
and table not in scope.sources
|
|
116
|
+
and table in dict(scope.references)
|
|
117
|
+
):
|
|
118
|
+
self.parent_kind = TableType.DERIVED_TABLE
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Determine logical properties (CTE, UDTF, Derived Table)
|
|
122
|
+
if self._set_kind_of_derived_table(logical_source, table, gen_ctx):
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Determine physical properties (TABLE, VIEW, etc.)
|
|
126
|
+
self._set_kind_of_physical_table(catalog, schema, table, source or logical_source, gen_ctx)
|
|
127
|
+
|
|
128
|
+
def _set_kind_of_derived_table(
|
|
129
|
+
self,
|
|
130
|
+
source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None,
|
|
131
|
+
table: str,
|
|
132
|
+
gen_ctx: GeneratorContext,
|
|
133
|
+
) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Identify if the source is a logical entity such as a CTE, UDTF, or derived table and set its properties.
|
|
136
|
+
"""
|
|
137
|
+
if isinstance(source, Scope):
|
|
138
|
+
if source.scope_type == ScopeType.CTE:
|
|
139
|
+
self._set_cte_properties(source, table, gen_ctx)
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
if source.scope_type == ScopeType.DERIVED_TABLE:
|
|
143
|
+
self.parent_kind = TableType.DERIVED_TABLE
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
if source.scope_type == ScopeType.UDTF:
|
|
147
|
+
self.parent_kind = TableType.UDTF
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
if isinstance(source, exp.Table) and "rows_from" in source.args:
|
|
151
|
+
self.parent_kind = TableType.DERIVED_TABLE
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
if isinstance(source, exp.Table) and isinstance(source.this, exp.Anonymous):
|
|
155
|
+
# Table function, e.g. SELECT * FROM FUNCTION()
|
|
156
|
+
self.parent_kind = TableType.UDTF
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
if isinstance(source, (exp.Select, exp.Subquery)):
|
|
160
|
+
self.parent_kind = TableType.DERIVED_TABLE
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def _set_cte_properties(self, source: Scope, table: str, gen_ctx: GeneratorContext) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Resolve and set properties specific to Common Table Expressions, including recursive and materialized status.
|
|
168
|
+
"""
|
|
169
|
+
self.parent_kind = TableType.CTE
|
|
170
|
+
logger.debug("Set node to be a CTE.")
|
|
171
|
+
|
|
172
|
+
# Check if it's recursive or materialized
|
|
173
|
+
if not source.parent:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
scope = gen_ctx.scope
|
|
177
|
+
if not isinstance(scope, Scope):
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
selected_table, _ = scope.selected_sources.get(table, (None, None))
|
|
181
|
+
if not selected_table:
|
|
182
|
+
# Try the regular sources; may occur when column nested inside subquery references a CTE outside itself
|
|
183
|
+
# In this case, skip it.
|
|
184
|
+
selected_source = scope.sources.get(table)
|
|
185
|
+
if selected_source and selected_source.scope_type == ScopeType.CTE:
|
|
186
|
+
return
|
|
187
|
+
else:
|
|
188
|
+
message = f"Table '{table}' is referenced but there is no FROM containing it."
|
|
189
|
+
raise exception.MappingError(message=message)
|
|
190
|
+
|
|
191
|
+
for cte in source.parent.ctes:
|
|
192
|
+
if cte.alias_or_name == selected_table.name:
|
|
193
|
+
if cte.args.get("materialized"):
|
|
194
|
+
self.parent_subkind = TableSubtype.MATERIALIZED
|
|
195
|
+
else:
|
|
196
|
+
with_ = cte.parent
|
|
197
|
+
if isinstance(with_, exp.With) and with_.recursive:
|
|
198
|
+
logger.debug("Set node to be a recursive CTE.")
|
|
199
|
+
self.parent_subkind = TableSubtype.RECURSIVE
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
def _set_kind_of_physical_table(
|
|
203
|
+
self,
|
|
204
|
+
catalog: str,
|
|
205
|
+
schema: str,
|
|
206
|
+
table: str,
|
|
207
|
+
source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None,
|
|
208
|
+
gen_ctx: GeneratorContext,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Determine the physical type of the table, such as a base table or view, by resolving its name against the object mapping.
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(source, exp.Table):
|
|
214
|
+
tokens = [str(s) for s in source.parts]
|
|
215
|
+
else:
|
|
216
|
+
tokens = [catalog, schema, table]
|
|
217
|
+
|
|
218
|
+
# Get the table type from the mapping
|
|
219
|
+
name = ".".join([tok for tok in tokens if tok])
|
|
220
|
+
if not name:
|
|
221
|
+
self.parent_kind = TableType.TABLE
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
tab = exp.to_table(name, dialect=gen_ctx.query.dialect)
|
|
225
|
+
query = gen_ctx.query.object_mapping.get_table_or_stage(table=tab, raise_on_missing=True)
|
|
226
|
+
|
|
227
|
+
if not query or query.kind == "ctas":
|
|
228
|
+
self.parent_kind = TableType.TABLE
|
|
229
|
+
else:
|
|
230
|
+
self.parent_kind = TableType(query.kind)
|
|
231
|
+
|
|
232
|
+
if query and query.property:
|
|
233
|
+
self.parent_subkind = TableSubtype(query.property)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def friendly_name(self) -> str:
|
|
237
|
+
tokens = [self.catalog, self.schema, self.table, self.name]
|
|
238
|
+
name = ".".join([tok for tok in tokens if tok])
|
|
239
|
+
fields = self.friendly_fields()
|
|
240
|
+
|
|
241
|
+
parts = []
|
|
242
|
+
if name:
|
|
243
|
+
parts.append(name)
|
|
244
|
+
|
|
245
|
+
for k, v in fields.items():
|
|
246
|
+
if v:
|
|
247
|
+
parts.append(f"{k}={v}")
|
|
248
|
+
|
|
249
|
+
content = " ".join(parts)
|
|
250
|
+
return self.wrap(content)
|
|
251
|
+
|
|
252
|
+
def as_table(self) -> exp.Table:
|
|
253
|
+
return exp.table_(catalog=self.catalog, db=self.schema, table=self.table)
|
|
254
|
+
|
|
255
|
+
def fields(self) -> dict[str, str]:
|
|
256
|
+
f = {"kind": self.parent_kind}
|
|
257
|
+
if self.parent_subkind:
|
|
258
|
+
f["subkind"] = self.parent_subkind
|
|
259
|
+
|
|
260
|
+
f |= {
|
|
261
|
+
"table": self.table,
|
|
262
|
+
"schema": self.schema,
|
|
263
|
+
"type": self.data_type,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if self.parent_kind == TableType.CTE and self.parent_subkind == TableSubtype.RECURSIVE:
|
|
267
|
+
f["member"] = self.member
|
|
268
|
+
|
|
269
|
+
if self.parent_kind in [TableType.CTE, TableType.DERIVED_TABLE]:
|
|
270
|
+
f["statement"] = str(self.ctx.statement_index)
|
|
271
|
+
|
|
272
|
+
return f
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class FileColumnNode(NodeAttributes):
|
|
276
|
+
KIND = "column"
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self,
|
|
280
|
+
column: str,
|
|
281
|
+
file_format: str,
|
|
282
|
+
file_path: str,
|
|
283
|
+
gen_ctx: GeneratorContext,
|
|
284
|
+
pos_ctx: PositionContext,
|
|
285
|
+
):
|
|
286
|
+
super().__init__(gen_ctx, pos_ctx, name=column)
|
|
287
|
+
self.file_format = file_format
|
|
288
|
+
self.file_path = file_path
|
|
289
|
+
|
|
290
|
+
def to_dict(self):
|
|
291
|
+
d = super().to_dict()
|
|
292
|
+
d.update({
|
|
293
|
+
"format": self.file_format,
|
|
294
|
+
"path": self.file_path,
|
|
295
|
+
})
|
|
296
|
+
return d
|
|
297
|
+
|
|
298
|
+
def fields(self) -> dict[str, str]:
|
|
299
|
+
return {
|
|
300
|
+
"type": self.data_type,
|
|
301
|
+
"kind": "file",
|
|
302
|
+
"format": self.file_format,
|
|
303
|
+
"path": self.file_path,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
def friendly_fields(self) -> dict[str, str]:
|
|
307
|
+
return {"path": self.file_path}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class StageColumnNode(NodeAttributes):
|
|
311
|
+
KIND = "column"
|
|
312
|
+
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
column: str,
|
|
316
|
+
stage: exp.Var,
|
|
317
|
+
gen_ctx: GeneratorContext,
|
|
318
|
+
pos_ctx: PositionContext,
|
|
319
|
+
path: str = "",
|
|
320
|
+
):
|
|
321
|
+
super().__init__(gen_ctx, pos_ctx, name=column)
|
|
322
|
+
self.stage = stage.name.removeprefix("@").replace('"', "")
|
|
323
|
+
self.path = path
|
|
324
|
+
|
|
325
|
+
def to_dict(self):
|
|
326
|
+
d = super().to_dict()
|
|
327
|
+
d.update({
|
|
328
|
+
"stage": self.stage,
|
|
329
|
+
"path": self.path,
|
|
330
|
+
})
|
|
331
|
+
return d
|
|
332
|
+
|
|
333
|
+
def fields(self) -> dict[str, str]:
|
|
334
|
+
return {
|
|
335
|
+
"type": self.data_type,
|
|
336
|
+
"kind": "stage",
|
|
337
|
+
"stage": self.stage,
|
|
338
|
+
"path": self.path,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
def friendly_fields(self) -> dict[str, str]:
|
|
342
|
+
return {
|
|
343
|
+
"stage": self.stage,
|
|
344
|
+
"path": self.path,
|
|
345
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
6
|
+
from sqlleaf.models.node import NodeAttributes
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("sqlleaf")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DynamoDbNode(NodeAttributes):
|
|
12
|
+
KIND = "column"
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
gen_ctx: GeneratorContext,
|
|
17
|
+
pos_ctx: PositionContext,
|
|
18
|
+
column: str,
|
|
19
|
+
):
|
|
20
|
+
super().__init__(gen_ctx, pos_ctx, name=column)
|
|
21
|
+
self.path = gen_ctx.expr.name
|
|
22
|
+
self.table_name = self.path.removeprefix("dynamodb://")
|
|
23
|
+
|
|
24
|
+
def to_dict(self):
|
|
25
|
+
d = super().to_dict()
|
|
26
|
+
d.update({"table": self.table_name})
|
|
27
|
+
return d
|
|
28
|
+
|
|
29
|
+
def fields(self) -> dict[str, str]:
|
|
30
|
+
return {
|
|
31
|
+
"type": self.data_type,
|
|
32
|
+
"kind": "dynamodb",
|
|
33
|
+
"table": self.table_name,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def friendly_fields(self) -> dict[str, str]:
|
|
37
|
+
return {
|
|
38
|
+
"path": self.path,
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf import util
|
|
8
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
9
|
+
from sqlleaf.models.node import NodeAttributes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FunctionNode(NodeAttributes):
|
|
13
|
+
KIND = "function"
|
|
14
|
+
WITH_POSITIONS = True
|
|
15
|
+
|
|
16
|
+
def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
|
|
17
|
+
expr = t.cast(t.Union[exp.Binary, exp.Func], gen_ctx.expr)
|
|
18
|
+
if isinstance(expr, exp.Binary):
|
|
19
|
+
name = expr.key
|
|
20
|
+
elif isinstance(expr, exp.Case):
|
|
21
|
+
name = "CASE"
|
|
22
|
+
elif isinstance(expr, exp.Anonymous):
|
|
23
|
+
_, name = util.get_udf_name(expr)
|
|
24
|
+
else:
|
|
25
|
+
name = util.calculate_function_name(expr, gen_ctx.query.dialect)
|
|
26
|
+
|
|
27
|
+
super().__init__(gen_ctx, pos_ctx, name=name)
|
|
28
|
+
|
|
29
|
+
def fields(self) -> dict[str, str]:
|
|
30
|
+
return {"type": self.data_type}
|
|
31
|
+
|
|
32
|
+
def get_name(self) -> str:
|
|
33
|
+
return f"{self.name}".upper()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
8
|
+
from sqlleaf.models.node import NodeAttributes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IntervalNode(NodeAttributes):
|
|
12
|
+
KIND = "interval"
|
|
13
|
+
WITH_POSITIONS = True
|
|
14
|
+
|
|
15
|
+
def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
|
|
16
|
+
expr = t.cast(exp.Interval, gen_ctx.expr)
|
|
17
|
+
name = f'"{str(expr.this.name)} {str(expr.unit)}"'
|
|
18
|
+
super().__init__(gen_ctx, pos_ctx, name=name)
|
|
19
|
+
|
|
20
|
+
def fields(self) -> dict[str, str]:
|
|
21
|
+
return {"type": self.data_type}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 NodeAttributes
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("sqlleaf")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JsonPathNode(NodeAttributes):
|
|
15
|
+
KIND = "jsonpath"
|
|
16
|
+
|
|
17
|
+
def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
|
|
18
|
+
expr = t.cast(exp.JSONExtract, gen_ctx.expr)
|
|
19
|
+
|
|
20
|
+
self.selectors = self.json_selectors(expr)
|
|
21
|
+
self.selector = "".join([str(s) for s in self.selectors])
|
|
22
|
+
self.selector_depth = len(self.selectors)
|
|
23
|
+
|
|
24
|
+
super().__init__(gen_ctx, pos_ctx, name=self.selector)
|
|
25
|
+
|
|
26
|
+
def json_selectors(self, expr: exp.JSONExtract):
|
|
27
|
+
"""
|
|
28
|
+
Collect all the JSON path elements recursively.
|
|
29
|
+
e.g.
|
|
30
|
+
SELECT my_json -> 'a' -> 'b'
|
|
31
|
+
produces
|
|
32
|
+
['a', 'b']
|
|
33
|
+
"""
|
|
34
|
+
methods = {
|
|
35
|
+
# Mapping of expression -> attribute getter
|
|
36
|
+
exp.JSONPathKey: None,
|
|
37
|
+
exp.JSONPathSubscript: "this",
|
|
38
|
+
}
|
|
39
|
+
elements = list(expr.expression.find_all(tuple(methods)))
|
|
40
|
+
|
|
41
|
+
left = expr.left
|
|
42
|
+
while isinstance(left, (exp.JSONExtract, exp.JSONExtractScalar)):
|
|
43
|
+
elements.extend(list(left.expression.find_all(tuple(methods))))
|
|
44
|
+
left = left.left
|
|
45
|
+
elements.reverse()
|
|
46
|
+
|
|
47
|
+
result = []
|
|
48
|
+
for elem in elements:
|
|
49
|
+
getter = methods[type(elem)]
|
|
50
|
+
result.append(getattr(elem, getter) if getter else elem)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def fields(self) -> dict[str, str]:
|
|
54
|
+
return {"depth": str(self.selector_depth)}
|
|
55
|
+
|
|
56
|
+
def friendly_fields(self) -> dict[str, str]:
|
|
57
|
+
return {}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.node import NodeAttributes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LiteralNode(NodeAttributes):
|
|
7
|
+
KIND = "literal"
|
|
8
|
+
WITH_POSITIONS = True
|
|
9
|
+
|
|
10
|
+
def fields(self) -> dict[str, str]:
|
|
11
|
+
return {"type": self.data_type}
|
|
12
|
+
|
|
13
|
+
def get_name(self) -> str:
|
|
14
|
+
if self.expr.is_string:
|
|
15
|
+
return f'"{self.name.replace("'", "")}"'
|
|
16
|
+
else:
|
|
17
|
+
return self.name
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.node import NodeAttributes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NullNode(NodeAttributes):
|
|
7
|
+
KIND = "null"
|
|
8
|
+
WITH_POSITIONS = True
|
|
9
|
+
|
|
10
|
+
def fields(self) -> dict[str, str]:
|
|
11
|
+
return {"type": "NULL"}
|
|
12
|
+
|
|
13
|
+
def get_name(self) -> str:
|
|
14
|
+
return "NULL"
|