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,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from sqlglot import exp
|
|
8
|
+
|
|
9
|
+
from sqlleaf import exception, mappings, util
|
|
10
|
+
from sqlleaf.models.query.base import Query
|
|
11
|
+
from sqlleaf.typing import SourceExprType, SourceInfo, TargetExprType, TargetInfo
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("sqlleaf")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class CopyQueryProperties:
|
|
18
|
+
file_format: str = "TEXT"
|
|
19
|
+
load_data: bool = True
|
|
20
|
+
is_a_job: bool = False
|
|
21
|
+
job_action: str = ""
|
|
22
|
+
job_name: str = ""
|
|
23
|
+
job_auto_run: bool = True
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_expression(cls, expr: exp.Copy, source_info: SourceInfo, target_info: TargetInfo) -> CopyQueryProperties:
|
|
27
|
+
"""
|
|
28
|
+
Extract the parameters of the COPY statement.
|
|
29
|
+
|
|
30
|
+
For example,
|
|
31
|
+
Input: "COPY FROM ... FORMAT AS CSV NOLOAD"
|
|
32
|
+
Params: ["FORMAT AS CSV", "NOLOAD"]
|
|
33
|
+
"""
|
|
34
|
+
params_dict: t.Dict[str, t.Any] = {}
|
|
35
|
+
|
|
36
|
+
params_list = []
|
|
37
|
+
# Filter for the parameters we need
|
|
38
|
+
for param in expr.args.get("params", []):
|
|
39
|
+
if isinstance(param, exp.CopyParameter) and isinstance(param.this, exp.Var):
|
|
40
|
+
params_list.append(param)
|
|
41
|
+
|
|
42
|
+
for i, param in enumerate(params_list):
|
|
43
|
+
param_name = param.this.name.upper()
|
|
44
|
+
if param_name == "FORMAT":
|
|
45
|
+
params_dict["file_format"] = str(param.expression)
|
|
46
|
+
elif param_name == "NOLOAD":
|
|
47
|
+
params_dict["load_data"] = False
|
|
48
|
+
elif param_name == "JOB":
|
|
49
|
+
params_dict["is_a_job"] = True
|
|
50
|
+
try:
|
|
51
|
+
# COPY <command> JOB CREATE <name>
|
|
52
|
+
job_command = params_list[i + 1].this.name.upper()
|
|
53
|
+
job_name = params_list[i + 1].expression.name
|
|
54
|
+
params_dict["job_action"] = job_command
|
|
55
|
+
params_dict["job_name"] = job_name
|
|
56
|
+
except IndexError:
|
|
57
|
+
message = "Missing one or more parameters to follow expression 'COPY .. JOB'"
|
|
58
|
+
raise exception.SqlLeafException(message=message)
|
|
59
|
+
elif param_name == "AUTO":
|
|
60
|
+
# COPY <command> JOB CREATE <name> AUTO ON | OFF
|
|
61
|
+
params_dict["job_auto_run"] = str(param.expression) == "ON"
|
|
62
|
+
return cls(**params_dict)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_active(self) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
An active COPY query is one that has lineage.
|
|
68
|
+
It is either:
|
|
69
|
+
- a regular COPY query
|
|
70
|
+
- a JOB that is either 'RUN' or 'CREATE and AUTO=ON'
|
|
71
|
+
Otherwise (e.g. `COPY JOB LIST`) it is not.
|
|
72
|
+
"""
|
|
73
|
+
if not self.is_a_job and self.load_data:
|
|
74
|
+
return True
|
|
75
|
+
if self.job_action == "RUN":
|
|
76
|
+
return True
|
|
77
|
+
if self.job_auto_run and self.job_action == "CREATE":
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CopyQuery(Query):
|
|
83
|
+
KIND = "copy"
|
|
84
|
+
|
|
85
|
+
def __init__(self, expr: exp.Copy, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
86
|
+
source, target = self.get_source_and_target_expressions(expr, dialect)
|
|
87
|
+
|
|
88
|
+
if dialect == "snowflake":
|
|
89
|
+
util.rename_if_stage(source, target)
|
|
90
|
+
|
|
91
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
92
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
93
|
+
|
|
94
|
+
super().__init__(
|
|
95
|
+
dialect=dialect,
|
|
96
|
+
statement=expr,
|
|
97
|
+
statement_index=statement_index,
|
|
98
|
+
object_mapping=object_mapping,
|
|
99
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
100
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
101
|
+
)
|
|
102
|
+
self.parameters = self.get_params()
|
|
103
|
+
util.qualify_and_annotate(self.source_info.expression, self.dialect, self.object_mapping)
|
|
104
|
+
|
|
105
|
+
def get_params(
|
|
106
|
+
self,
|
|
107
|
+
) -> CopyQueryProperties:
|
|
108
|
+
return CopyQueryProperties.from_expression(self.statement, self.source_info, self.target_info)
|
|
109
|
+
|
|
110
|
+
def is_query_active(self) -> bool:
|
|
111
|
+
logger.debug(self.parameters)
|
|
112
|
+
return self.parameters.is_active
|
|
113
|
+
|
|
114
|
+
def get_source_and_target_expressions(
|
|
115
|
+
self, expr: exp.Copy, dialect: str
|
|
116
|
+
) -> t.Tuple[SourceExprType, TargetExprType]:
|
|
117
|
+
"""
|
|
118
|
+
Determine the source and target expressions of the query.
|
|
119
|
+
"""
|
|
120
|
+
if dialect in ["postgres", "redshift"]:
|
|
121
|
+
# Postgres treats STDOUT and STDIN the same
|
|
122
|
+
|
|
123
|
+
if expr.args["kind"]:
|
|
124
|
+
# COPY X FROM STDOUT/STDIN
|
|
125
|
+
source = expr.args["files"][0]
|
|
126
|
+
target = expr.args["this"]
|
|
127
|
+
if isinstance(target, exp.Schema):
|
|
128
|
+
target = target.this
|
|
129
|
+
else:
|
|
130
|
+
# COPY X TO STDOUT/STDIN
|
|
131
|
+
source = expr.args["this"]
|
|
132
|
+
target = expr.args["files"][0]
|
|
133
|
+
if isinstance(source, exp.Schema):
|
|
134
|
+
source = source.this
|
|
135
|
+
|
|
136
|
+
elif dialect == "snowflake":
|
|
137
|
+
source = expr.args["files"][0]
|
|
138
|
+
target = expr.args["this"]
|
|
139
|
+
|
|
140
|
+
# It may be a subquery
|
|
141
|
+
source = source.unnest()
|
|
142
|
+
target = target.unnest()
|
|
143
|
+
|
|
144
|
+
return source, target
|
|
@@ -0,0 +1,61 @@
|
|
|
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 CTASQuery(Query):
|
|
14
|
+
KIND = "ctas"
|
|
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
|
+
if source is None:
|
|
26
|
+
if exec_prop := expr.find(exp.ExecuteAsProperty):
|
|
27
|
+
source = exec_prop.this
|
|
28
|
+
|
|
29
|
+
target = util.get_table(expr)
|
|
30
|
+
|
|
31
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
32
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
33
|
+
|
|
34
|
+
super().__init__(
|
|
35
|
+
dialect=dialect,
|
|
36
|
+
statement=expr,
|
|
37
|
+
statement_index=statement_index,
|
|
38
|
+
object_mapping=object_mapping,
|
|
39
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
40
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
41
|
+
)
|
|
42
|
+
self.column_defs: t.List[exp.ColumnDef] = columns
|
|
43
|
+
self.system_column_defs: t.List[exp.ColumnDef] = []
|
|
44
|
+
self.inherited_by: t.List[TableQuery] = []
|
|
45
|
+
|
|
46
|
+
self.load_data: bool = True
|
|
47
|
+
if props := expr.args["properties"]:
|
|
48
|
+
if load_data := props.find(exp.WithDataProperty):
|
|
49
|
+
self.load_data: bool = not load_data.args["no"]
|
|
50
|
+
|
|
51
|
+
self.property: str = util.find_property(expr, self.target_info.expression, dialect)
|
|
52
|
+
|
|
53
|
+
def get_column_defs(self, include_system: bool = False) -> t.List[exp.ColumnDef]:
|
|
54
|
+
return self.column_defs + self.system_column_defs if include_system else self.column_defs
|
|
55
|
+
|
|
56
|
+
def get_column_names_with_types(self, include_system: bool = False) -> t.Dict[str, str]:
|
|
57
|
+
"""
|
|
58
|
+
Used by sqlglot's MappingSchema
|
|
59
|
+
"""
|
|
60
|
+
columns = {col.name: str(col.kind) for col in self.get_column_defs(include_system=include_system)}
|
|
61
|
+
return columns
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf import mappings, util
|
|
8
|
+
from sqlleaf.models.query.base import Query
|
|
9
|
+
from sqlleaf.typing import SqlObjectType, TargetInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class DatabaseQueryProperties:
|
|
14
|
+
location: str | None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_expression(cls, expr: exp.Create, dialect: str):
|
|
18
|
+
location = util.get_location_property(expr, dialect)
|
|
19
|
+
return cls(location=location)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DatabaseQuery(Query):
|
|
23
|
+
KIND = "database"
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
expr: exp.Create,
|
|
28
|
+
dialect: str,
|
|
29
|
+
object_mapping: mappings.ObjectMapping,
|
|
30
|
+
statement_index: int,
|
|
31
|
+
):
|
|
32
|
+
source = None
|
|
33
|
+
target = expr.this
|
|
34
|
+
|
|
35
|
+
super().__init__(
|
|
36
|
+
dialect=dialect,
|
|
37
|
+
statement=expr,
|
|
38
|
+
statement_index=statement_index,
|
|
39
|
+
object_mapping=object_mapping,
|
|
40
|
+
source_info=source,
|
|
41
|
+
target_info=TargetInfo(expression=target, type=SqlObjectType.DATABASE),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.properties = DatabaseQueryProperties.from_expression(expr, dialect)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def location(self) -> str | None:
|
|
48
|
+
return self.properties.location
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import mappings
|
|
6
|
+
from sqlleaf.models.query.base import Query
|
|
7
|
+
from sqlleaf.typing import SqlObjectType, TargetInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeleteQuery(Query):
|
|
11
|
+
KIND = "delete"
|
|
12
|
+
|
|
13
|
+
def __init__(self, expr: exp.Delete, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
14
|
+
super().__init__(
|
|
15
|
+
dialect=dialect,
|
|
16
|
+
statement=expr,
|
|
17
|
+
statement_index=statement_index,
|
|
18
|
+
object_mapping=object_mapping,
|
|
19
|
+
source_info=None,
|
|
20
|
+
target_info=TargetInfo(expression=expr.this, type=SqlObjectType.TABLE),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def get_ctes(self):
|
|
24
|
+
if "with_" in self.statement.args:
|
|
25
|
+
return self.statement.args["with_"].expressions
|
|
26
|
+
return []
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import sqlglot
|
|
7
|
+
from sqlglot import exp
|
|
8
|
+
|
|
9
|
+
from sqlleaf import mappings
|
|
10
|
+
from sqlleaf.models.query.base import Query
|
|
11
|
+
from sqlleaf.typing import SqlObjectType, TargetInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ExecuteQueryProperties:
|
|
16
|
+
name: str
|
|
17
|
+
arguments: t.List[exp.Literal]
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_expression(cls, expr: exp.Command, dialect: str):
|
|
21
|
+
"""
|
|
22
|
+
EXECUTE <name>(<arguments>);
|
|
23
|
+
"""
|
|
24
|
+
expression_name = expr.expression.name
|
|
25
|
+
parsed = sqlglot.parse_one(expression_name, read=dialect)
|
|
26
|
+
|
|
27
|
+
if isinstance(parsed, exp.Anonymous):
|
|
28
|
+
name = parsed.this
|
|
29
|
+
arguments = parsed.expressions
|
|
30
|
+
else:
|
|
31
|
+
name = parsed.name
|
|
32
|
+
arguments = []
|
|
33
|
+
|
|
34
|
+
return cls(name=name, arguments=arguments)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ExecuteQuery(Query):
|
|
38
|
+
KIND = "execute"
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
expr: exp.Command,
|
|
43
|
+
dialect: str,
|
|
44
|
+
object_mapping: mappings.ObjectMapping,
|
|
45
|
+
statement_index: int,
|
|
46
|
+
):
|
|
47
|
+
self.parameters = ExecuteQueryProperties.from_expression(expr, dialect)
|
|
48
|
+
|
|
49
|
+
super().__init__(
|
|
50
|
+
dialect=dialect,
|
|
51
|
+
statement=expr,
|
|
52
|
+
statement_index=statement_index,
|
|
53
|
+
object_mapping=object_mapping,
|
|
54
|
+
source_info=None,
|
|
55
|
+
target_info=TargetInfo(
|
|
56
|
+
expression=exp.to_table(self.parameters.name), type=SqlObjectType.PREPARED_STATEMENT
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def name(self) -> str:
|
|
62
|
+
return self.parameters.name
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def arguments(self) -> t.List[exp.Literal]:
|
|
66
|
+
return self.parameters.arguments
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
if t.TYPE_CHECKING:
|
|
6
|
+
from sqlleaf.models.query import Q
|
|
7
|
+
from sqlleaf.models.query.base import Query
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QueryHolder:
|
|
11
|
+
"""
|
|
12
|
+
A container that holds up to three versions of the same SQL statement
|
|
13
|
+
as three distinct Query instances.
|
|
14
|
+
|
|
15
|
+
- original: The parsed, unmodified Query.
|
|
16
|
+
- transformed: The Query after transformer.py has processed it.
|
|
17
|
+
- downstream_holders: The containers for downstream queries. Used both for structural
|
|
18
|
+
children (MERGE branches, INSERT ON CONFLICT, UDF bodies) and for
|
|
19
|
+
call-site substituted queries (CallQuery, ExecuteQuery, UDF call sites).
|
|
20
|
+
Populated entirely during the Collector phase.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, original: Query):
|
|
24
|
+
self.original: Query = original
|
|
25
|
+
self.transformed: Query | None = None
|
|
26
|
+
self.downstream_holders: t.List[QueryHolder] = []
|
|
27
|
+
self.parent_holder: QueryHolder | None = None
|
|
28
|
+
|
|
29
|
+
self.original.set_holder(self)
|
|
30
|
+
|
|
31
|
+
def add_downstream_holder(self, holder: QueryHolder) -> None:
|
|
32
|
+
holder.parent_holder = self
|
|
33
|
+
self.downstream_holders.append(holder)
|
|
34
|
+
|
|
35
|
+
def get_all_holders(self, types: t.Tuple | None = None) -> t.List[QueryHolder]:
|
|
36
|
+
"""
|
|
37
|
+
Recursively collect all holders (self + children).
|
|
38
|
+
Optionally filter by the type of their `original` query.
|
|
39
|
+
"""
|
|
40
|
+
holders = [self]
|
|
41
|
+
for child in self.downstream_holders:
|
|
42
|
+
holders.extend(child.get_all_holders(types))
|
|
43
|
+
if types:
|
|
44
|
+
holders = [h for h in holders if isinstance(h.original, types)]
|
|
45
|
+
return holders
|
|
46
|
+
|
|
47
|
+
def set_transformed_query(self, query: Q) -> None:
|
|
48
|
+
self.transformed = query
|
|
49
|
+
self.transformed.set_holder(self)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlglot
|
|
4
|
+
from sqlglot import exp
|
|
5
|
+
|
|
6
|
+
from sqlleaf import mappings, util
|
|
7
|
+
from sqlleaf.models.query.base import Query
|
|
8
|
+
from sqlleaf.typing import SourceInfo, SqlObjectType, TargetInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InsertQuery(Query):
|
|
12
|
+
KIND = "insert"
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
expr: exp.Insert,
|
|
17
|
+
dialect: str,
|
|
18
|
+
object_mapping: mappings.ObjectMapping,
|
|
19
|
+
statement_index: int,
|
|
20
|
+
table: exp.Table | None = None,
|
|
21
|
+
# Whether to skip column annotation of types.
|
|
22
|
+
# Used to preserve column types after conversion from COPY to INSERT.
|
|
23
|
+
skip_annotate: bool = False,
|
|
24
|
+
):
|
|
25
|
+
if not table:
|
|
26
|
+
table = util.get_table(expr)
|
|
27
|
+
|
|
28
|
+
target = table
|
|
29
|
+
source = expr.expression
|
|
30
|
+
if source:
|
|
31
|
+
# Subquery(Select) -> Select
|
|
32
|
+
source = source.unnest()
|
|
33
|
+
elif src := expr.args.get("source"):
|
|
34
|
+
# INSERT .. DEFAULT VALUES
|
|
35
|
+
source = src
|
|
36
|
+
else:
|
|
37
|
+
# INSERT .. (VALUES())
|
|
38
|
+
exprs = expr.this.expressions
|
|
39
|
+
if len(exprs) == 1 and (ex := exprs[0]):
|
|
40
|
+
if isinstance(ex, exp.Anonymous) and ex.name.upper() == "VALUES":
|
|
41
|
+
# sqlglot parses this incorrectly; fix it by re-parsing it to an exp.Values
|
|
42
|
+
source = sqlglot.parse_one(ex.sql(dialect=dialect), dialect=dialect)
|
|
43
|
+
expr.set("expression", source)
|
|
44
|
+
[e.pop() for e in (expr.this.expressions or [])]
|
|
45
|
+
|
|
46
|
+
# Set the correct source type. Hacky, but works for now
|
|
47
|
+
is_default_values = expr.args.get("default", False)
|
|
48
|
+
if is_default_values:
|
|
49
|
+
source_type = SqlObjectType.VALUES
|
|
50
|
+
elif isinstance(source, exp.Tuple):
|
|
51
|
+
source_type = SqlObjectType.TUPLE
|
|
52
|
+
elif isinstance(source, exp.SetOperation):
|
|
53
|
+
source_type = SqlObjectType.SELECT
|
|
54
|
+
else:
|
|
55
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
56
|
+
|
|
57
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
58
|
+
|
|
59
|
+
super().__init__(
|
|
60
|
+
dialect=dialect,
|
|
61
|
+
statement=expr,
|
|
62
|
+
statement_index=statement_index,
|
|
63
|
+
object_mapping=object_mapping,
|
|
64
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
65
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def get_ctes(self):
|
|
69
|
+
return getattr(self.statement, "ctes", [])
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import mappings
|
|
6
|
+
from sqlleaf.models.query.base import Query
|
|
7
|
+
from sqlleaf.typing import TargetInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MergeQuery(Query):
|
|
11
|
+
KIND = "merge"
|
|
12
|
+
|
|
13
|
+
def __init__(self, expr: exp.Merge, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
14
|
+
source = None
|
|
15
|
+
target = expr.this
|
|
16
|
+
|
|
17
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
18
|
+
|
|
19
|
+
super().__init__(
|
|
20
|
+
dialect=dialect,
|
|
21
|
+
statement=expr,
|
|
22
|
+
statement_index=statement_index,
|
|
23
|
+
object_mapping=object_mapping,
|
|
24
|
+
source_info=source,
|
|
25
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def get_ctes(self):
|
|
29
|
+
return getattr(self.statement, "ctes", [])
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf import mappings
|
|
8
|
+
from sqlleaf.models.query.base import Query
|
|
9
|
+
from sqlleaf.typing import SourceInfo, SqlObjectType, TargetInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MultitableInsertQuery(Query):
|
|
13
|
+
KIND = "multitableinserts"
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
expr: exp.MultitableInserts,
|
|
18
|
+
dialect: str,
|
|
19
|
+
object_mapping: mappings.ObjectMapping,
|
|
20
|
+
statement_index: int,
|
|
21
|
+
):
|
|
22
|
+
source = expr.args["source"]
|
|
23
|
+
source_type = SqlObjectType.SELECT
|
|
24
|
+
|
|
25
|
+
# For multi-table inserts, the target is actually multiple tables,
|
|
26
|
+
# but the base Query model expects a single TargetInfo.
|
|
27
|
+
# We'll use the first one or a dummy for the parent query.
|
|
28
|
+
# The children will have the actual target info.
|
|
29
|
+
# TODO: fix this.
|
|
30
|
+
target = expr.expressions[0].this.this
|
|
31
|
+
|
|
32
|
+
super().__init__(
|
|
33
|
+
dialect=dialect,
|
|
34
|
+
statement=expr,
|
|
35
|
+
statement_index=statement_index,
|
|
36
|
+
object_mapping=object_mapping,
|
|
37
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
38
|
+
target_info=TargetInfo(expression=target, type=SqlObjectType.TABLE),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def get_ctes(self) -> t.List[exp.CTE]:
|
|
42
|
+
return getattr(self.statement, "ctes", [])
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
import sqlglot
|
|
8
|
+
from sqlglot import exp
|
|
9
|
+
|
|
10
|
+
from sqlleaf import exception, mappings, util
|
|
11
|
+
from sqlleaf.models.query.base import Query
|
|
12
|
+
from sqlleaf.typing import SourceInfo, SqlObjectType, TargetInfo
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("sqlleaf")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PrepareQuery(Query):
|
|
18
|
+
KIND = "prepare"
|
|
19
|
+
|
|
20
|
+
def __init__(self, expr: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
21
|
+
source, target = self.get_source_and_target_expressions(expr)
|
|
22
|
+
|
|
23
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
24
|
+
target_type = SqlObjectType.PREPARED_STATEMENT
|
|
25
|
+
|
|
26
|
+
super().__init__(
|
|
27
|
+
dialect=dialect,
|
|
28
|
+
statement=source,
|
|
29
|
+
statement_index=statement_index,
|
|
30
|
+
object_mapping=object_mapping,
|
|
31
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
32
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
33
|
+
)
|
|
34
|
+
util.qualify_and_annotate(self.source_info.expression, self.dialect, self.object_mapping)
|
|
35
|
+
self.parameter_count = self._count_parameters(source)
|
|
36
|
+
|
|
37
|
+
def _count_parameters(self, source: exp.Expr) -> int:
|
|
38
|
+
"""
|
|
39
|
+
Count the number of expected arguments for this prepared statement.
|
|
40
|
+
Positional parameters are $1, $2, etc.
|
|
41
|
+
"""
|
|
42
|
+
params = [int(p.name) for p in source.find_all(exp.Parameter) if p.name.isdigit()]
|
|
43
|
+
return max(params) if params else 0
|
|
44
|
+
|
|
45
|
+
def get_source_and_target_expressions(self, statement: exp.Command) -> t.Tuple[exp.Expr, exp.Table]:
|
|
46
|
+
"""
|
|
47
|
+
Parse a PREPARE statement for Postgres.
|
|
48
|
+
Syntax: PREPARE name AS statement
|
|
49
|
+
"""
|
|
50
|
+
expression_name = statement.expression.name
|
|
51
|
+
|
|
52
|
+
# Find the 'AS' keyword to split name and statement
|
|
53
|
+
match = re.search(r"\s+AS\s+", expression_name, re.IGNORECASE)
|
|
54
|
+
if not match:
|
|
55
|
+
raise exception.SqlLeafException(message=f"Could not find 'AS' in PREPARE expression: {expression_name}")
|
|
56
|
+
|
|
57
|
+
name = expression_name[: match.start()].strip()
|
|
58
|
+
statement_text = expression_name[match.end() :].strip()
|
|
59
|
+
|
|
60
|
+
prepared_expr = sqlglot.parse_one(statement_text, dialect="postgres")
|
|
61
|
+
return prepared_expr, exp.to_table(name)
|
|
@@ -0,0 +1,72 @@
|
|
|
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.user_defined_function import _extract_function_info
|
|
10
|
+
from sqlleaf.typing import TargetInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcedureQuery(Query):
|
|
14
|
+
"""
|
|
15
|
+
Holds metadata related to stored procedures.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
KIND = "procedure"
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
expr: exp.Create,
|
|
23
|
+
dialect: str,
|
|
24
|
+
object_mapping: mappings.ObjectMapping,
|
|
25
|
+
statement_index: int,
|
|
26
|
+
):
|
|
27
|
+
table = util.get_table(expr)
|
|
28
|
+
target_type = self._determine_expression_type(table, dialect)
|
|
29
|
+
|
|
30
|
+
super().__init__(
|
|
31
|
+
dialect=dialect,
|
|
32
|
+
statement=expr,
|
|
33
|
+
statement_index=statement_index,
|
|
34
|
+
object_mapping=object_mapping,
|
|
35
|
+
source_info=None,
|
|
36
|
+
target_info=TargetInfo(expression=table, type=target_type),
|
|
37
|
+
)
|
|
38
|
+
self.schema = table.db
|
|
39
|
+
self.procedure = table.name
|
|
40
|
+
self.signature = str(expr.this) # e.g. etl.my_proc(v_session_id VARCHAR)
|
|
41
|
+
|
|
42
|
+
# TODO: support 'default'
|
|
43
|
+
self.column_defs: t.List[exp.ColumnDef] = expr.this.expressions
|
|
44
|
+
_, _, self.parameters = _extract_function_info(expr)
|
|
45
|
+
self.args = [ # e.g. {'name': 'v_session_id', 'type': 'VARCHAR'}
|
|
46
|
+
{"name": p.name, "type": str(p.type)} for p in self.parameters
|
|
47
|
+
]
|
|
48
|
+
self.inner_statements = self._extract_inner_statements(expr)
|
|
49
|
+
|
|
50
|
+
def _extract_inner_statements(self, expr: exp.Create) -> t.List[exp.Expr]:
|
|
51
|
+
body_expr = expr.args.get("expression")
|
|
52
|
+
if not body_expr:
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
inner_statements = util.iter_inner_statements(body_expr, self.dialect, wrap=True)
|
|
56
|
+
|
|
57
|
+
# Filter out statements that do not contain lineage (e.g. END;)
|
|
58
|
+
return [
|
|
59
|
+
stmt for stmt in inner_statements if not isinstance(stmt, (exp.EndStatement, exp.Column, exp.Identifier))
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def to_dict(self):
|
|
63
|
+
return {
|
|
64
|
+
"id": self.id,
|
|
65
|
+
"name": self.name,
|
|
66
|
+
"signature": self.signature,
|
|
67
|
+
"args": self.args,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def name(self):
|
|
72
|
+
return ".".join([var for var in [self.schema, self.procedure] if var])
|