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,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import sqlglot
|
|
7
|
+
from sqlglot import TokenType, exp
|
|
8
|
+
|
|
9
|
+
from sqlleaf import exception, mappings, util
|
|
10
|
+
from sqlleaf.models.query.base import Query
|
|
11
|
+
from sqlleaf.typing import SourceInfo, TargetInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class UnloadQueryProperties:
|
|
16
|
+
file_format: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnloadQuery(Query):
|
|
20
|
+
KIND = "unload"
|
|
21
|
+
|
|
22
|
+
def __init__(self, expr: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
23
|
+
source, target = self.get_source_and_target_expressions(expr)
|
|
24
|
+
|
|
25
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
26
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
27
|
+
|
|
28
|
+
super().__init__(
|
|
29
|
+
dialect=dialect,
|
|
30
|
+
statement=source,
|
|
31
|
+
statement_index=statement_index,
|
|
32
|
+
object_mapping=object_mapping,
|
|
33
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
34
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
35
|
+
)
|
|
36
|
+
self.parameters = UnloadQueryProperties(file_format="UNKNOWN")
|
|
37
|
+
util.qualify_and_annotate(self.source_info.expression, self.dialect, self.object_mapping)
|
|
38
|
+
|
|
39
|
+
def get_source_and_target_expressions(self, statement: exp.Command) -> t.Tuple[exp.Select, exp.Literal]:
|
|
40
|
+
"""
|
|
41
|
+
Parse an UNLOAD statement for Redshift.
|
|
42
|
+
We parse this ourselves due to missing support in sqlglot.
|
|
43
|
+
"""
|
|
44
|
+
# Syntax: "UNLOAD ('SELECT ...') TO ..."
|
|
45
|
+
expected_tokens = [TokenType.L_PAREN, TokenType.STRING, TokenType.R_PAREN, TokenType.VAR, TokenType.STRING]
|
|
46
|
+
actual_tokens = sqlglot.tokenize(statement.expression.name, dialect="redshift")
|
|
47
|
+
|
|
48
|
+
# Basic validation - ensure the token types match
|
|
49
|
+
for i in range(len(expected_tokens)):
|
|
50
|
+
if expected_tokens[i] != actual_tokens[i].token_type:
|
|
51
|
+
# This may be incorrect! Use the parser instead once available.
|
|
52
|
+
raise exception.InvalidQueryError(
|
|
53
|
+
message=f"Invalid syntax for UNLOAD expression: {statement.sql(dialect='redshift')}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
select_expr = sqlglot.parse_one(actual_tokens[1].text, dialect="redshift")
|
|
57
|
+
if not isinstance(select_expr, exp.Select):
|
|
58
|
+
raise exception.InvalidQueryError(
|
|
59
|
+
message=f"Invalid expression inside UNLOAD. Expected SELECT "
|
|
60
|
+
f"but got: {select_expr.sql(dialect='redshift')}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
to_location = actual_tokens[4].text
|
|
64
|
+
return select_expr, t.cast(exp.Literal, exp.convert(to_location))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import mappings, util
|
|
6
|
+
from sqlleaf.models.query.base import Query
|
|
7
|
+
from sqlleaf.typing import SourceInfo, TargetInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UpdateQuery(Query):
|
|
11
|
+
KIND = "update"
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
expr: exp.Update,
|
|
16
|
+
dialect: str,
|
|
17
|
+
object_mapping: mappings.ObjectMapping,
|
|
18
|
+
statement_index: int,
|
|
19
|
+
table: exp.Table | None = None,
|
|
20
|
+
):
|
|
21
|
+
if not table:
|
|
22
|
+
table = util.get_table(expr)
|
|
23
|
+
|
|
24
|
+
if isinstance(expr, exp.OnConflict):
|
|
25
|
+
source = expr
|
|
26
|
+
else:
|
|
27
|
+
source = expr.args["expressions"]
|
|
28
|
+
target = table
|
|
29
|
+
|
|
30
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
31
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
32
|
+
|
|
33
|
+
super().__init__(
|
|
34
|
+
dialect=dialect,
|
|
35
|
+
statement=expr,
|
|
36
|
+
statement_index=statement_index,
|
|
37
|
+
object_mapping=object_mapping,
|
|
38
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
39
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
40
|
+
)
|
|
41
|
+
self.only = table.args.get("only", False) if table else False # Not available inside a MERGE
|
|
42
|
+
|
|
43
|
+
def get_ctes(self):
|
|
44
|
+
with_ = self.statement.args.get("with_", None)
|
|
45
|
+
return with_.expressions if with_ else []
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
from sqlleaf import mappings, util
|
|
9
|
+
from sqlleaf.models.query.base import Query
|
|
10
|
+
from sqlleaf.typing import TargetInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class UserDefinedFunctionParameters:
|
|
15
|
+
schema_name: t.Optional[str]
|
|
16
|
+
function_name: str
|
|
17
|
+
return_type: t.Optional[exp.DataType]
|
|
18
|
+
language: t.Optional[str]
|
|
19
|
+
parameters: t.List[FunctionParam]
|
|
20
|
+
return_columns: t.List[exp.ColumnDef]
|
|
21
|
+
inner_statements: t.List[exp.Expr]
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_expression(cls, expr: exp.Create, object_mapping: mappings.ObjectMapping) -> UserDefinedFunctionParameters:
|
|
25
|
+
"""
|
|
26
|
+
Collect the required information from the function DDL.
|
|
27
|
+
"""
|
|
28
|
+
function_name, schema_name, parameters = _extract_function_info(expr)
|
|
29
|
+
return_type, return_columns = _extract_return_info(expr, parameters, object_mapping)
|
|
30
|
+
|
|
31
|
+
# Filter parameters to only include those that can be passed as input
|
|
32
|
+
input_parameters = [p for p in parameters if p.is_input or p.is_variadic]
|
|
33
|
+
|
|
34
|
+
language = _extract_language(expr)
|
|
35
|
+
|
|
36
|
+
body_expr = expr.args.get("expression")
|
|
37
|
+
inner_statements = []
|
|
38
|
+
|
|
39
|
+
if body_expr:
|
|
40
|
+
inner_statements = util.iter_inner_statements(body_expr, object_mapping.dialect, wrap=True)
|
|
41
|
+
|
|
42
|
+
return cls(
|
|
43
|
+
schema_name=schema_name,
|
|
44
|
+
function_name=function_name,
|
|
45
|
+
return_type=return_type,
|
|
46
|
+
language=language,
|
|
47
|
+
parameters=input_parameters,
|
|
48
|
+
return_columns=return_columns,
|
|
49
|
+
inner_statements=inner_statements,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UserDefinedFunctionQuery(Query):
|
|
54
|
+
KIND = "udf"
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
expr: exp.Create,
|
|
59
|
+
dialect,
|
|
60
|
+
object_mapping: mappings.ObjectMapping,
|
|
61
|
+
statement_index: int,
|
|
62
|
+
):
|
|
63
|
+
source = None
|
|
64
|
+
target = expr.this.this
|
|
65
|
+
|
|
66
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
67
|
+
|
|
68
|
+
super().__init__(
|
|
69
|
+
dialect=dialect,
|
|
70
|
+
statement=expr,
|
|
71
|
+
statement_index=statement_index,
|
|
72
|
+
object_mapping=object_mapping,
|
|
73
|
+
source_info=source,
|
|
74
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.properties = self.get_params()
|
|
78
|
+
self.column_defs = self.properties.return_columns
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def parameters(self) -> t.List[FunctionParam]:
|
|
82
|
+
return self.properties.parameters
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def schema_name(self) -> t.Optional[str]:
|
|
86
|
+
return self.properties.schema_name
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def function_name(self) -> str:
|
|
90
|
+
return self.properties.function_name
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def return_type(self) -> t.Optional[exp.DataType]:
|
|
94
|
+
return self.properties.return_type
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def language(self) -> t.Optional[str]:
|
|
98
|
+
return self.properties.language
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def return_columns(self) -> t.List[exp.ColumnDef]:
|
|
102
|
+
return self.properties.return_columns
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def inner_statements(self) -> t.List[exp.Expr]:
|
|
106
|
+
return self.properties.inner_statements
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def name(self):
|
|
110
|
+
return ".".join([var for var in [self.schema_name, self.function_name] if var])
|
|
111
|
+
|
|
112
|
+
def get_params(self) -> UserDefinedFunctionParameters:
|
|
113
|
+
return UserDefinedFunctionParameters.from_expression(self.statement, self.object_mapping)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_user_defined_data_type(kind: t.Optional[str | exp.Identifier | exp.Dot] = None) -> exp.DataType:
|
|
117
|
+
if kind:
|
|
118
|
+
if isinstance(kind, str):
|
|
119
|
+
if "." in kind:
|
|
120
|
+
parts = kind.split(".")
|
|
121
|
+
kind = exp.Dot.build([exp.Identifier(this=p, quoted=False) for p in parts])
|
|
122
|
+
else:
|
|
123
|
+
kind = exp.Identifier(this=kind, quoted=False)
|
|
124
|
+
return exp.DataType.build(kind, udt=True, dialect="postgres")
|
|
125
|
+
return exp.DataType.build("USER-DEFINED", dialect="postgres")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class FunctionParam:
|
|
130
|
+
"""A parameter of a user-defined function."""
|
|
131
|
+
|
|
132
|
+
name: str
|
|
133
|
+
type: exp.DataType
|
|
134
|
+
default: t.Optional[exp.Expression] = None
|
|
135
|
+
is_input: bool = True
|
|
136
|
+
is_output: bool = False
|
|
137
|
+
is_variadic: bool = False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _extract_function_info(expression: exp.Create) -> t.Tuple[str, t.Optional[str], t.List[FunctionParam]]:
|
|
141
|
+
"""Extracts the name, schema, and parameters from a CREATE FUNCTION expression."""
|
|
142
|
+
target = expression.this
|
|
143
|
+
parameters = []
|
|
144
|
+
|
|
145
|
+
# Process the function definition which includes name and parameters
|
|
146
|
+
if isinstance(target, exp.UserDefinedFunction):
|
|
147
|
+
function_expr = target.this
|
|
148
|
+
# Iterate through column definitions to find parameters
|
|
149
|
+
for i, col_def in enumerate(target.expressions):
|
|
150
|
+
if isinstance(col_def, exp.ColumnDef):
|
|
151
|
+
param_name = col_def.this.name
|
|
152
|
+
param_type = col_def.kind
|
|
153
|
+
param_default = None
|
|
154
|
+
is_input = True
|
|
155
|
+
is_output = False
|
|
156
|
+
is_variadic = False
|
|
157
|
+
|
|
158
|
+
# Determine the parameter types (IN, OUT, etc)
|
|
159
|
+
constraints = col_def.args.get("constraints")
|
|
160
|
+
if constraints:
|
|
161
|
+
for constraint in constraints:
|
|
162
|
+
kind = constraint.args.get("kind")
|
|
163
|
+
if isinstance(kind, exp.DefaultColumnConstraint):
|
|
164
|
+
param_default = kind.this
|
|
165
|
+
elif isinstance(constraint, exp.InOutColumnConstraint):
|
|
166
|
+
is_input = constraint.args.get("input_")
|
|
167
|
+
is_output = constraint.args.get("output")
|
|
168
|
+
is_variadic = constraint.args.get("variadic")
|
|
169
|
+
|
|
170
|
+
# Unnamed parameter logic inside ColumnDef (e.g., VARIADIC numeric[])
|
|
171
|
+
if param_name.upper() == "VARIADIC":
|
|
172
|
+
is_variadic = True
|
|
173
|
+
param_name = f"${i + 1}"
|
|
174
|
+
# The type might be stored in a way that includes the rest of the definition
|
|
175
|
+
# In Postgres unnamed VARIADIC, sqlglot might parse it such that 'variadic' is the name
|
|
176
|
+
# and the type is numeric[]
|
|
177
|
+
|
|
178
|
+
# Handle special keywords used as parameter names by assigning positional placeholders
|
|
179
|
+
if param_name.upper() in ("IN", "OUT", "INOUT"):
|
|
180
|
+
if param_name.upper() == "OUT":
|
|
181
|
+
is_input = False
|
|
182
|
+
is_output = True
|
|
183
|
+
elif param_name.upper() == "INOUT":
|
|
184
|
+
is_input = True
|
|
185
|
+
is_output = True
|
|
186
|
+
param_name = f"${i + 1}"
|
|
187
|
+
parameters.append(
|
|
188
|
+
FunctionParam(
|
|
189
|
+
name=param_name,
|
|
190
|
+
type=param_type,
|
|
191
|
+
default=param_default,
|
|
192
|
+
is_input=is_input,
|
|
193
|
+
is_output=is_output,
|
|
194
|
+
is_variadic=is_variadic,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
elif _is_unnamed_parameter(col_def):
|
|
198
|
+
# Handle unnamed parameters like CREATE FUNCTION hello(TEXT) or (VARIADIC numeric[])
|
|
199
|
+
param_name = f"${i + 1}"
|
|
200
|
+
is_variadic = False
|
|
201
|
+
|
|
202
|
+
type_node = col_def
|
|
203
|
+
if isinstance(col_def, exp.UserDefinedFunction):
|
|
204
|
+
is_variadic = col_def.sql().upper().startswith("variadic")
|
|
205
|
+
type_node = col_def.this
|
|
206
|
+
|
|
207
|
+
type_sql = type_node.this.lower()
|
|
208
|
+
|
|
209
|
+
if type_sql in ("anyelement", "anyarray"):
|
|
210
|
+
param_type = get_user_defined_data_type(kind=type_sql)
|
|
211
|
+
else:
|
|
212
|
+
try:
|
|
213
|
+
param_type = exp.DataType.build(type_node.this, dialect="postgres")
|
|
214
|
+
except Exception:
|
|
215
|
+
# e.g. for tables as parameters - CREATE FUNCTION hello(people), where 'people' is a table
|
|
216
|
+
param_type = get_user_defined_data_type(kind=type_node.this)
|
|
217
|
+
|
|
218
|
+
parameters.append(FunctionParam(name=param_name, type=param_type, is_variadic=is_variadic))
|
|
219
|
+
else:
|
|
220
|
+
function_expr = target.this
|
|
221
|
+
|
|
222
|
+
# Determine function name and schema from the table-like expression structure
|
|
223
|
+
if isinstance(function_expr, exp.Table):
|
|
224
|
+
name = function_expr.name
|
|
225
|
+
schema = function_expr.args.get("db").this if function_expr.args.get("db") else None
|
|
226
|
+
else:
|
|
227
|
+
name = function_expr.sql()
|
|
228
|
+
schema = None
|
|
229
|
+
|
|
230
|
+
return name, schema, parameters
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_unnamed_parameter(col_def: exp.ColumnDef) -> bool:
|
|
234
|
+
return isinstance(col_def, exp.Identifier) or (
|
|
235
|
+
isinstance(col_def, exp.UserDefinedFunction) and isinstance(col_def.this, exp.Identifier)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _extract_return_info(
|
|
240
|
+
expression: exp.Create, parameters: t.List[FunctionParam], object_mapping: mappings.ObjectMapping
|
|
241
|
+
) -> t.Tuple[t.Optional[exp.DataType], t.List[exp.ColumnDef]]:
|
|
242
|
+
"""Extracts return type and return columns from a CREATE FUNCTION expression."""
|
|
243
|
+
return_type = None
|
|
244
|
+
return_columns = []
|
|
245
|
+
|
|
246
|
+
# Check for OUT/INOUT parameters that determine the return structure
|
|
247
|
+
out_params: t.List[FunctionParam] = [p for p in parameters if p.is_output]
|
|
248
|
+
|
|
249
|
+
if out_params:
|
|
250
|
+
if len(out_params) == 1:
|
|
251
|
+
return_type = out_params[0].type
|
|
252
|
+
else:
|
|
253
|
+
return_type = get_user_defined_data_type()
|
|
254
|
+
for param in out_params:
|
|
255
|
+
col_name = param.name
|
|
256
|
+
if col_name.startswith("$"):
|
|
257
|
+
col_name = f"column{col_name[1:]}"
|
|
258
|
+
return_columns.append(exp.ColumnDef(this=exp.to_identifier(col_name), kind=param.type))
|
|
259
|
+
|
|
260
|
+
# Loop through properties to find the RETURNS clause
|
|
261
|
+
for prop in expression.args.get("properties", {}).expressions:
|
|
262
|
+
if isinstance(prop, exp.ReturnsProperty):
|
|
263
|
+
if prop.this is None:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# Check if it's a RETURNS TABLE(...) with multiple columns
|
|
267
|
+
if isinstance(prop.this, exp.Schema):
|
|
268
|
+
return_type = get_user_defined_data_type()
|
|
269
|
+
# Extract individual column definitions for table-returning functions
|
|
270
|
+
for col_def in prop.this.expressions:
|
|
271
|
+
if isinstance(col_def, exp.ColumnDef):
|
|
272
|
+
return_columns.append(col_def)
|
|
273
|
+
else:
|
|
274
|
+
# Standard return type (e.g., RETURNS TEXT, RETURNS person, RETURNS TABLE_NAME)
|
|
275
|
+
return_type_sql = prop.this.sql().lower()
|
|
276
|
+
return_type = prop.this
|
|
277
|
+
|
|
278
|
+
if return_type_sql == "void":
|
|
279
|
+
return_type = exp.DataType.build("NULL")
|
|
280
|
+
else:
|
|
281
|
+
target_table = exp.to_table(return_type_sql)
|
|
282
|
+
found_query = object_mapping.lookup_type_query(target_table, raise_on_missing=False)
|
|
283
|
+
if not found_query:
|
|
284
|
+
found_query = object_mapping.lookup_table_query(target_table, raise_on_missing=False)
|
|
285
|
+
|
|
286
|
+
if found_query:
|
|
287
|
+
return_columns = found_query.get_column_defs()
|
|
288
|
+
|
|
289
|
+
return return_type, return_columns
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _extract_language(expression: exp.Create) -> t.Optional[str]:
|
|
293
|
+
"""Extracts the language from a CREATE FUNCTION expression."""
|
|
294
|
+
for prop in expression.args.get("properties", {}).expressions:
|
|
295
|
+
if isinstance(prop, exp.LanguageProperty):
|
|
296
|
+
return prop.this.sql().lower()
|
|
297
|
+
return None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from sqlglot import exp
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.query.base import Query
|
|
4
|
+
from sqlleaf.typing import SourceInfo, SqlObjectType, TargetInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ValuesQuery(Query):
|
|
8
|
+
KIND = "values"
|
|
9
|
+
|
|
10
|
+
def __init__(self, expr: exp.Values, dialect: str, object_mapping, statement_index: int):
|
|
11
|
+
source_info = SourceInfo(expression=expr, type=SqlObjectType.VALUES)
|
|
12
|
+
target_info = TargetInfo(expression=None, type=SqlObjectType.NONE)
|
|
13
|
+
super().__init__(
|
|
14
|
+
dialect=dialect,
|
|
15
|
+
statement=expr,
|
|
16
|
+
statement_index=statement_index,
|
|
17
|
+
object_mapping=object_mapping,
|
|
18
|
+
source_info=source_info,
|
|
19
|
+
target_info=target_info,
|
|
20
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf import mappings, util
|
|
8
|
+
from sqlleaf.models.query.base import Query
|
|
9
|
+
from sqlleaf.models.query.table import TableQuery
|
|
10
|
+
from sqlleaf.typing import SourceInfo, TargetInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ViewQuery(Query):
|
|
14
|
+
KIND = "view"
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
expr: exp.Create,
|
|
19
|
+
dialect: str,
|
|
20
|
+
object_mapping: mappings.ObjectMapping,
|
|
21
|
+
columns: t.List[exp.ColumnDef],
|
|
22
|
+
statement_index: int,
|
|
23
|
+
):
|
|
24
|
+
source = expr.expression
|
|
25
|
+
target = util.get_table(expr)
|
|
26
|
+
|
|
27
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
28
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
29
|
+
|
|
30
|
+
super().__init__(
|
|
31
|
+
dialect=dialect,
|
|
32
|
+
statement=expr,
|
|
33
|
+
statement_index=statement_index,
|
|
34
|
+
object_mapping=object_mapping,
|
|
35
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
36
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
37
|
+
)
|
|
38
|
+
self.column_defs: t.List[exp.ColumnDef] = columns
|
|
39
|
+
self.inherited_by: t.List[TableQuery] = []
|
|
40
|
+
|
|
41
|
+
self.property: str = util.find_property(expr, self.target_info.expression, dialect)
|
sqlleaf/path.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
import networkx as nx
|
|
7
|
+
|
|
8
|
+
from sqlleaf import exception, util
|
|
9
|
+
from sqlleaf.models.node import EdgeAttributes, N
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("sqlleaf")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LineagePath:
|
|
15
|
+
def __init__(self, hops: t.List[EdgeAttributes]):
|
|
16
|
+
self.hops = hops
|
|
17
|
+
self.length = len(hops)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def id(self) -> str:
|
|
21
|
+
return "path:" + util.short_sha256_hash(":".join(self.get_edge_ids()))
|
|
22
|
+
|
|
23
|
+
def node_hops(self) -> t.List[N]:
|
|
24
|
+
"""
|
|
25
|
+
Return the list of nodes on this path.
|
|
26
|
+
"""
|
|
27
|
+
hops = [self.hops[0].parent]
|
|
28
|
+
for hop in self.hops:
|
|
29
|
+
hops.append(hop.child)
|
|
30
|
+
return hops
|
|
31
|
+
|
|
32
|
+
def get_edge_ids(self) -> list[str]:
|
|
33
|
+
"""
|
|
34
|
+
In order to distinguish between multiple edges that are part of the same path,
|
|
35
|
+
we need to create a unique id based off data that differentiates them.
|
|
36
|
+
This is done using the edges' "id" attribute.
|
|
37
|
+
"""
|
|
38
|
+
return [edge.id for edge in self.hops]
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict:
|
|
41
|
+
return {
|
|
42
|
+
"id": self.id,
|
|
43
|
+
"length": len(self.hops),
|
|
44
|
+
"hops": [edge.id for edge in self.hops],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def find_all_paths(graph: nx.MultiDiGraph) -> t.Generator[LineagePath]:
|
|
49
|
+
"""
|
|
50
|
+
Find all the unique paths in the graph.
|
|
51
|
+
|
|
52
|
+
There are two algorithms depending on the graph's structure:
|
|
53
|
+
- one for those containing root node (a Rooted graph)
|
|
54
|
+
- one for those containing only cycles (a Circuit graph)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Cycle handling logic.
|
|
58
|
+
# [x] Stage 1. Error: Throw an error/warning on cycles that include multiple columns.
|
|
59
|
+
# [ ] Stage 2. Recovery: For every cycle, print it to the console, and then
|
|
60
|
+
# remove all their edges; then proceed with printing the remainder.
|
|
61
|
+
cycles = []
|
|
62
|
+
cycle_errors = 0
|
|
63
|
+
|
|
64
|
+
for cycle, errors in util.get_cycles(graph):
|
|
65
|
+
if errors > 0:
|
|
66
|
+
cycle_errors += errors
|
|
67
|
+
else:
|
|
68
|
+
cycles.append(cycle)
|
|
69
|
+
|
|
70
|
+
if cycle_errors:
|
|
71
|
+
raise exception.GraphError(message=f"Found {cycle_errors} errors with cycles in graph. Remove these.")
|
|
72
|
+
|
|
73
|
+
cycles = sorted(cycles) # simple_cycles() underneath is non-deterministic
|
|
74
|
+
root_columns = util.get_root_nodes(graph)
|
|
75
|
+
|
|
76
|
+
if root_columns:
|
|
77
|
+
logger.debug(f"Found root columns in graph: {root_columns}")
|
|
78
|
+
for i, root in enumerate(root_columns):
|
|
79
|
+
for path in find_edges_from_root(graph, root):
|
|
80
|
+
if not path:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if path[0].parent.id == path[0].child.id:
|
|
84
|
+
# Skip selfloops; these are covered in the dedicated loop below
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
logger.debug(
|
|
88
|
+
"Found edge path using root: %s --- %s",
|
|
89
|
+
[e.id for e in path],
|
|
90
|
+
[(e.parent.friendly_name, e.child.friendly_name) for e in path],
|
|
91
|
+
)
|
|
92
|
+
lineage_path = LineagePath(hops=path)
|
|
93
|
+
yield lineage_path
|
|
94
|
+
|
|
95
|
+
# Check for selfloops (e.g. a column that goes through another node, such as a function)
|
|
96
|
+
for cycle in cycles:
|
|
97
|
+
for path in find_edges_along_cycle_path(graph, cycle):
|
|
98
|
+
logger.debug(
|
|
99
|
+
"Found edge path from cycle: %s --- %s",
|
|
100
|
+
[e.id for e in path],
|
|
101
|
+
[(e.parent.friendly_name, e.child.friendly_name) for e in path],
|
|
102
|
+
)
|
|
103
|
+
lineage_path = LineagePath(hops=path)
|
|
104
|
+
yield lineage_path
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def find_edges_along_cycle_path(
|
|
108
|
+
g: nx.MultiDiGraph, cycle: t.List[str], path: t.Optional[t.List[EdgeAttributes]] = None
|
|
109
|
+
) -> t.Generator[t.List[EdgeAttributes]]:
|
|
110
|
+
"""
|
|
111
|
+
Given a cycle, find all the edges along it and return these as the new path.
|
|
112
|
+
A path must be provided so that iteration doesn't deviate into non-cycle paths.
|
|
113
|
+
"""
|
|
114
|
+
if path is None:
|
|
115
|
+
path = []
|
|
116
|
+
|
|
117
|
+
depth = len(path)
|
|
118
|
+
if depth == len(cycle) - 1:
|
|
119
|
+
yield path
|
|
120
|
+
else:
|
|
121
|
+
node_src = cycle[depth]
|
|
122
|
+
node_dst = cycle[depth + 1]
|
|
123
|
+
edges = g.get_edge_data(node_src, node_dst)
|
|
124
|
+
for idx, data in edges.items():
|
|
125
|
+
hop = data["attrs"]
|
|
126
|
+
yield from find_edges_along_cycle_path(g, cycle, path + [hop])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def find_edges_from_root(
|
|
130
|
+
g: nx.MultiDiGraph, node: str, path: t.Optional[t.List[EdgeAttributes]] = None, seen: t.Optional[t.Set[str]] = None
|
|
131
|
+
) -> t.Generator[t.List[EdgeAttributes]]:
|
|
132
|
+
"""
|
|
133
|
+
Find all the complete paths in a graph by traversing the descendants of a node until we find
|
|
134
|
+
a node without any descendants.
|
|
135
|
+
|
|
136
|
+
This is the same as the regular find_paths(), but we iterate over all the edges between any two node
|
|
137
|
+
so that we include all paths.
|
|
138
|
+
|
|
139
|
+
For example, given the graph:
|
|
140
|
+
A -> B -> C -> D
|
|
141
|
+
|
|
142
|
+
If there are two edges between A->B and two edges between C->D:
|
|
143
|
+
__ __
|
|
144
|
+
/ \\ / \
|
|
145
|
+
--> A B --> C D -->
|
|
146
|
+
\\__/ \\__/
|
|
147
|
+
|
|
148
|
+
then traversing all the edges gives:
|
|
149
|
+
A -> B -> C -> D
|
|
150
|
+
A -> B -> C -> D
|
|
151
|
+
A -> B -> C -> D
|
|
152
|
+
A -> B -> C -> D
|
|
153
|
+
|
|
154
|
+
Thus we consider each path during traversal, as each likely has slightly different attributes.
|
|
155
|
+
"""
|
|
156
|
+
if path is None:
|
|
157
|
+
path = []
|
|
158
|
+
if seen is None:
|
|
159
|
+
seen = set()
|
|
160
|
+
|
|
161
|
+
if node in seen:
|
|
162
|
+
yield path
|
|
163
|
+
else:
|
|
164
|
+
# If there's a loop [.., X, .., X], discard everything outside of the Xs as the path.
|
|
165
|
+
if selfloop_edges := g.get_edge_data(node, node):
|
|
166
|
+
for idx, data in selfloop_edges.items():
|
|
167
|
+
hop = data["attrs"]
|
|
168
|
+
yield [hop]
|
|
169
|
+
|
|
170
|
+
# Get direct descendants
|
|
171
|
+
desc = nx.descendants_at_distance(g, node, 1)
|
|
172
|
+
if not desc:
|
|
173
|
+
yield path
|
|
174
|
+
else:
|
|
175
|
+
desc = sorted(desc) # nx.desc() above is non-deterministic
|
|
176
|
+
for n in desc:
|
|
177
|
+
yield from _traverse_path_along_edges(g, node, n, path, seen)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _traverse_path_along_edges(
|
|
181
|
+
g: nx.MultiDiGraph, node_src: str, node_dst: str, path: t.Optional[t.List] = None, seen: t.Optional[t.Set] = None
|
|
182
|
+
) -> t.Generator[t.List[EdgeAttributes]]:
|
|
183
|
+
"""
|
|
184
|
+
Get the list of edges between two node, and find the paths for each of them.
|
|
185
|
+
"""
|
|
186
|
+
if path is None:
|
|
187
|
+
path = []
|
|
188
|
+
if seen is None:
|
|
189
|
+
seen = set()
|
|
190
|
+
|
|
191
|
+
edges = g.get_edge_data(node_src, node_dst)
|
|
192
|
+
for idx, data in edges.items():
|
|
193
|
+
hop = data["attrs"]
|
|
194
|
+
yield from find_edges_from_root(g, node_dst, path + [hop], seen.union([node_src]))
|
|
File without changes
|
|
File without changes
|