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,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
4
|
+
from sqlleaf.models.node import NodeAttributes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _PivotNode(NodeAttributes):
|
|
8
|
+
def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
|
|
9
|
+
super().__init__(gen_ctx, pos_ctx)
|
|
10
|
+
self.source: str = ""
|
|
11
|
+
self.target: str = ""
|
|
12
|
+
|
|
13
|
+
def set(self, source: str, target: str):
|
|
14
|
+
self.source = source
|
|
15
|
+
self.target = target
|
|
16
|
+
|
|
17
|
+
def fields(self) -> dict[str, str]:
|
|
18
|
+
f = {}
|
|
19
|
+
# Keep empty strings as values to match expected test output
|
|
20
|
+
f["source"] = self.source
|
|
21
|
+
f["target"] = self.target
|
|
22
|
+
f["statement"] = str(self.ctx.statement_index)
|
|
23
|
+
return f
|
|
24
|
+
|
|
25
|
+
def get_name(self) -> str:
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PivotNode(_PivotNode):
|
|
30
|
+
KIND = "pivot"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UnpivotNode(_PivotNode):
|
|
34
|
+
KIND = "unpivot"
|
|
@@ -0,0 +1,27 @@
|
|
|
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 ProgramNode(NodeAttributes):
|
|
12
|
+
KIND = "program"
|
|
13
|
+
WITH_POSITIONS = True
|
|
14
|
+
|
|
15
|
+
def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
|
|
16
|
+
pos_ctx = pos_ctx.replace(select_index=0) # Prevent duplicate nodes
|
|
17
|
+
copy_stmt = gen_ctx.query.get_original_self().statement
|
|
18
|
+
expr = t.cast(exp.Copy, copy_stmt)
|
|
19
|
+
|
|
20
|
+
# TODO: put these inside COPY to prevent lookups to the original query here
|
|
21
|
+
program = expr.args["params"][0].name
|
|
22
|
+
name, args = (program + " ").split(" ", maxsplit=1)
|
|
23
|
+
super().__init__(gen_ctx, pos_ctx, name=name)
|
|
24
|
+
self.program_args = args.strip()
|
|
25
|
+
|
|
26
|
+
def fields(self) -> dict[str, str]:
|
|
27
|
+
return {"args": f"'{self.program_args}'"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
4
|
+
from sqlleaf.models.node import NodeAttributes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SequenceNode(NodeAttributes):
|
|
8
|
+
KIND = "sequence"
|
|
9
|
+
|
|
10
|
+
def __init__(self, name: str, gen_ctx: GeneratorContext, pos_ctx: PositionContext, subkind: str = ""):
|
|
11
|
+
super().__init__(gen_ctx, pos_ctx, name=name)
|
|
12
|
+
self.subkind = subkind
|
|
13
|
+
|
|
14
|
+
def fields(self) -> dict[str, str]:
|
|
15
|
+
f = {"type": self.data_type}
|
|
16
|
+
if self.subkind:
|
|
17
|
+
f["subkind"] = self.subkind
|
|
18
|
+
return f
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
4
|
+
from sqlleaf.models.node import NodeAttributes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StreamNode(NodeAttributes):
|
|
8
|
+
KIND = "stream"
|
|
9
|
+
|
|
10
|
+
def __init__(self, name: str, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
|
|
11
|
+
super().__init__(gen_ctx, pos_ctx, name=name)
|
|
12
|
+
|
|
13
|
+
def fields(self) -> dict[str, str]:
|
|
14
|
+
return {}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.context import GeneratorContext, PositionContext
|
|
4
|
+
from sqlleaf.models.node import NodeAttributes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserDefinedFunctionNode(NodeAttributes):
|
|
8
|
+
KIND = "udf"
|
|
9
|
+
WITH_POSITIONS = True
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
schema: str,
|
|
14
|
+
gen_ctx: GeneratorContext,
|
|
15
|
+
pos_ctx: PositionContext,
|
|
16
|
+
):
|
|
17
|
+
super().__init__(gen_ctx, pos_ctx, name=gen_ctx.expr.this)
|
|
18
|
+
self.schema = schema
|
|
19
|
+
|
|
20
|
+
def get_name(self) -> str:
|
|
21
|
+
return str(self.name).upper()
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def friendly_name(self) -> str:
|
|
25
|
+
tokens = [self.schema, self.name]
|
|
26
|
+
name = ".".join([tok for tok in tokens if tok]).upper()
|
|
27
|
+
return self.wrap(name)
|
|
28
|
+
|
|
29
|
+
def fields(self) -> dict[str, str]:
|
|
30
|
+
return {
|
|
31
|
+
"type": self.data_type,
|
|
32
|
+
"schema": self.schema.upper() if self.schema else "",
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 WindowNode(NodeAttributes):
|
|
13
|
+
KIND = "window"
|
|
14
|
+
|
|
15
|
+
def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
|
|
16
|
+
expr = t.cast(exp.Window, gen_ctx.expr.this)
|
|
17
|
+
name = util.calculate_function_name(expr, gen_ctx.query.dialect)
|
|
18
|
+
|
|
19
|
+
super().__init__(gen_ctx, pos_ctx, name=name)
|
|
20
|
+
|
|
21
|
+
def fields(self) -> dict[str, str]:
|
|
22
|
+
return {"type": self.data_type}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.query.base import Query as Query
|
|
4
|
+
from sqlleaf.models.query.call import CallQuery as CallQuery
|
|
5
|
+
from sqlleaf.models.query.copy import CopyQuery as CopyQuery
|
|
6
|
+
from sqlleaf.models.query.ctas import CTASQuery as CTASQuery
|
|
7
|
+
from sqlleaf.models.query.database import DatabaseQuery as DatabaseQuery
|
|
8
|
+
from sqlleaf.models.query.delete import DeleteQuery as DeleteQuery
|
|
9
|
+
from sqlleaf.models.query.execute import ExecuteQuery as ExecuteQuery
|
|
10
|
+
from sqlleaf.models.query.holder import QueryHolder as QueryHolder
|
|
11
|
+
from sqlleaf.models.query.insert import InsertQuery as InsertQuery
|
|
12
|
+
from sqlleaf.models.query.merge import MergeQuery as MergeQuery
|
|
13
|
+
from sqlleaf.models.query.multitable_insert import MultitableInsertQuery as MultitableInsertQuery
|
|
14
|
+
from sqlleaf.models.query.prepare import PrepareQuery as PrepareQuery
|
|
15
|
+
from sqlleaf.models.query.procedure import ProcedureQuery as ProcedureQuery
|
|
16
|
+
from sqlleaf.models.query.put import PutQuery as PutQuery
|
|
17
|
+
from sqlleaf.models.query.replace import ReplaceQuery as ReplaceQuery
|
|
18
|
+
from sqlleaf.models.query.schema import SchemaQuery as SchemaQuery
|
|
19
|
+
from sqlleaf.models.query.select import SelectQuery as SelectQuery
|
|
20
|
+
from sqlleaf.models.query.sequence import SequenceQuery as SequenceQuery
|
|
21
|
+
from sqlleaf.models.query.set import SetQuery as SetQuery
|
|
22
|
+
from sqlleaf.models.query.stage import StageQuery as StageQuery
|
|
23
|
+
from sqlleaf.models.query.table import TableQuery as TableQuery
|
|
24
|
+
from sqlleaf.models.query.trigger import TriggerQuery as TriggerQuery
|
|
25
|
+
from sqlleaf.models.query.type import TypeQuery as TypeQuery
|
|
26
|
+
from sqlleaf.models.query.unload import UnloadQuery as UnloadQuery
|
|
27
|
+
from sqlleaf.models.query.update import UpdateQuery as UpdateQuery
|
|
28
|
+
from sqlleaf.models.query.user_defined_function import FunctionParam as FunctionParam
|
|
29
|
+
from sqlleaf.models.query.user_defined_function import UserDefinedFunctionQuery as UserDefinedFunctionQuery
|
|
30
|
+
from sqlleaf.models.query.values import ValuesQuery as ValuesQuery
|
|
31
|
+
from sqlleaf.models.query.view import ViewQuery as ViewQuery
|
|
32
|
+
|
|
33
|
+
Q = t.TypeVar("Q", bound=Query)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
if t.TYPE_CHECKING:
|
|
7
|
+
from sqlleaf.models.query import QueryHolder
|
|
8
|
+
|
|
9
|
+
from sqlglot import exp
|
|
10
|
+
from sqlglot.optimizer.annotate_types import annotate_types
|
|
11
|
+
|
|
12
|
+
from sqlleaf import exception, mappings, util
|
|
13
|
+
from sqlleaf.typing import SourceExprType, SourceInfo, SqlObjectType, TargetExprType, TargetInfo
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("sqlleaf")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Query:
|
|
19
|
+
KIND: str = ""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
dialect: str,
|
|
24
|
+
statement: exp.Expr,
|
|
25
|
+
statement_index: int,
|
|
26
|
+
object_mapping: mappings.ObjectMapping,
|
|
27
|
+
source_info: SourceInfo | None,
|
|
28
|
+
target_info: TargetInfo,
|
|
29
|
+
skip_type_annotation: bool = False,
|
|
30
|
+
):
|
|
31
|
+
self.kind = self.KIND
|
|
32
|
+
self.dialect = dialect
|
|
33
|
+
self.object_mapping = object_mapping
|
|
34
|
+
self.parent_query = None
|
|
35
|
+
self.child_queries = []
|
|
36
|
+
self.column_defs: t.List[exp.ColumnDef] = []
|
|
37
|
+
self.property = ""
|
|
38
|
+
|
|
39
|
+
# Remove comments at initialization
|
|
40
|
+
for expr in statement.walk():
|
|
41
|
+
expr.pop_comments()
|
|
42
|
+
|
|
43
|
+
if skip_type_annotation:
|
|
44
|
+
# Prevent overwriting the types with UNKNOWN if they've already been annotated
|
|
45
|
+
annotate_types(statement, dialect=dialect, schema=object_mapping)
|
|
46
|
+
|
|
47
|
+
self.statement: exp.Expr = statement
|
|
48
|
+
self.statement_index = statement_index # The position of this query within a list of queries
|
|
49
|
+
|
|
50
|
+
self.source_info = source_info
|
|
51
|
+
self.target_info = target_info
|
|
52
|
+
|
|
53
|
+
logger.debug(
|
|
54
|
+
f"Created new query. Query => {self.__class__.__name__} | SourceType => {self.source_info and self.source_info.type.name} | TargetType => {self.target_info and self.target_info.type.name}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def set_holder(self, holder: QueryHolder):
|
|
58
|
+
self.holder = holder
|
|
59
|
+
|
|
60
|
+
def _determine_expression_type(self, expr: exp.Expr | t.List[exp.Expr], dialect: str) -> SqlObjectType:
|
|
61
|
+
"""
|
|
62
|
+
Determine the type of object that an expression represents.
|
|
63
|
+
For example, the literal "/tmp/data.csv" is of type FILE; identifier "STDIN" is of type STREAM.
|
|
64
|
+
"""
|
|
65
|
+
if isinstance(expr, exp.Literal):
|
|
66
|
+
_type = SqlObjectType.DYNAMODB if expr.this.lower().startswith("dynamodb://") else SqlObjectType.FILE
|
|
67
|
+
|
|
68
|
+
elif isinstance(expr, exp.Identifier):
|
|
69
|
+
if expr.name.lower() in ["stdin", "stdout"]:
|
|
70
|
+
_type = SqlObjectType.STREAM
|
|
71
|
+
elif expr.name.lower() in ["program"]:
|
|
72
|
+
_type = SqlObjectType.PROGRAM
|
|
73
|
+
else:
|
|
74
|
+
raise exception.InvalidQueryError(f"Unknown object type identifier: {expr.name}")
|
|
75
|
+
|
|
76
|
+
elif isinstance(expr, exp.Var):
|
|
77
|
+
if dialect == "snowflake":
|
|
78
|
+
_type = SqlObjectType.STAGE
|
|
79
|
+
else:
|
|
80
|
+
_type = SqlObjectType.PREPARED_STATEMENT
|
|
81
|
+
|
|
82
|
+
elif isinstance(expr, exp.Table):
|
|
83
|
+
if isinstance(expr.this, exp.Var) and dialect == "snowflake":
|
|
84
|
+
_type = SqlObjectType.STAGE
|
|
85
|
+
else:
|
|
86
|
+
_type = SqlObjectType.TABLE
|
|
87
|
+
|
|
88
|
+
elif isinstance(expr, exp.Anonymous):
|
|
89
|
+
if expr.this == "PROCEDURE":
|
|
90
|
+
# CALL() statement
|
|
91
|
+
_type = SqlObjectType.PROCEDURE
|
|
92
|
+
else:
|
|
93
|
+
# CTAS EXECUTE <statement(args)>
|
|
94
|
+
_type = SqlObjectType.PREPARED_STATEMENT
|
|
95
|
+
|
|
96
|
+
elif dialect == "postgres" and isinstance(expr, exp.Alias):
|
|
97
|
+
if expr.this.name.upper() == "TABLE":
|
|
98
|
+
# PREPARE X AS TABLE Y;
|
|
99
|
+
_type = SqlObjectType.SELECT
|
|
100
|
+
|
|
101
|
+
elif isinstance(expr, exp.Select):
|
|
102
|
+
_type = SqlObjectType.SELECT
|
|
103
|
+
|
|
104
|
+
elif isinstance(expr, exp.Values):
|
|
105
|
+
_type = SqlObjectType.VALUES
|
|
106
|
+
|
|
107
|
+
elif isinstance(expr, exp.OnConflict):
|
|
108
|
+
# temporary; figure out how to handle this case
|
|
109
|
+
_type = SqlObjectType.SET
|
|
110
|
+
|
|
111
|
+
elif isinstance(expr, list) and isinstance(expr[0], exp.EQ):
|
|
112
|
+
# UPDATE .. SET
|
|
113
|
+
_type = SqlObjectType.SET
|
|
114
|
+
|
|
115
|
+
elif isinstance(expr, (exp.Insert, exp.Update, exp.Delete, exp.Merge)):
|
|
116
|
+
_type = SqlObjectType.DML
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
raise exception.InvalidQueryError(f"Unknown source/target object type in query: {type(expr)}")
|
|
120
|
+
|
|
121
|
+
return _type
|
|
122
|
+
|
|
123
|
+
def get_original_self(self) -> Query:
|
|
124
|
+
return self.holder.original
|
|
125
|
+
|
|
126
|
+
def get_transformed_self(self) -> Query | None:
|
|
127
|
+
return self.holder.transformed
|
|
128
|
+
|
|
129
|
+
def get_columns_from_target(self) -> t.List[exp.ColumnDef]:
|
|
130
|
+
"""
|
|
131
|
+
Given a query, figure out its target object, including its columns.
|
|
132
|
+
|
|
133
|
+
The structure is: INSERT INTO <target> .. <source>
|
|
134
|
+
|
|
135
|
+
This is straightforward if 'source' isn't a JOIN: we just use the source object's columns.
|
|
136
|
+
But if it is a JOIN, we use the selected columns rather than the source's columns.
|
|
137
|
+
"""
|
|
138
|
+
if self.source_info is None:
|
|
139
|
+
# TODO: temp. This occurs when CALL() invokes an SP with only a SELECT
|
|
140
|
+
# Remove this after CopyQuery refactor is complete
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
source_expr = self.source_info.expression
|
|
144
|
+
target_expr = self.target_info.expression
|
|
145
|
+
target_type = self.target_info.type
|
|
146
|
+
|
|
147
|
+
if SqlObjectType.type_has_no_column_defs(target_type):
|
|
148
|
+
object_with_columns = source_expr
|
|
149
|
+
elif target_type == SqlObjectType.TABLE:
|
|
150
|
+
object_with_columns = target_expr
|
|
151
|
+
else:
|
|
152
|
+
raise exception.InvalidQueryError(f"Unhandled target object type: {target_type}")
|
|
153
|
+
|
|
154
|
+
column_defs = self._get_column_defs(object_with_columns)
|
|
155
|
+
if not column_defs:
|
|
156
|
+
raise exception.InvalidQueryError(f"Could not find any columns for expression: {object_with_columns}")
|
|
157
|
+
|
|
158
|
+
return column_defs
|
|
159
|
+
|
|
160
|
+
def _get_column_defs(
|
|
161
|
+
self,
|
|
162
|
+
expr: SourceExprType | TargetExprType,
|
|
163
|
+
) -> t.List[exp.ColumnDef]:
|
|
164
|
+
"""
|
|
165
|
+
Most of the time, the sources and target are tables.
|
|
166
|
+
However, with COPY/UNLOAD, they can be files or streams.
|
|
167
|
+
|
|
168
|
+
If the target is not a table and the source is a SELECT,
|
|
169
|
+
there may be a JOIN with many tables as the source.
|
|
170
|
+
"""
|
|
171
|
+
if not isinstance(expr, exp.Table):
|
|
172
|
+
# Fall back to the source
|
|
173
|
+
source = self.source_info.expression
|
|
174
|
+
if isinstance(source, exp.Select):
|
|
175
|
+
# TODO: this can't handle functions
|
|
176
|
+
return [
|
|
177
|
+
exp.ColumnDef(this=exp.to_identifier(col.alias_or_name), kind=col.unalias().type)
|
|
178
|
+
for col in source.expressions
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
table_query = self.object_mapping.get_table_or_stage(expr)
|
|
182
|
+
if not table_query:
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
return table_query.get_column_defs()
|
|
186
|
+
|
|
187
|
+
def get_target_as_table(self) -> exp.Table:
|
|
188
|
+
"""
|
|
189
|
+
For functions that only accept tables.
|
|
190
|
+
"""
|
|
191
|
+
if not isinstance(self.target_info.expression, exp.Table):
|
|
192
|
+
raise exception.InvalidQueryError(
|
|
193
|
+
message=f"Expected the target object to be a table but it is a {repr(self.target_info.type)}"
|
|
194
|
+
)
|
|
195
|
+
return self.target_info.expression
|
|
196
|
+
|
|
197
|
+
def get_statement_index(self) -> str:
|
|
198
|
+
"""
|
|
199
|
+
Get the statement index for this query (including its parents).
|
|
200
|
+
"""
|
|
201
|
+
if self.parent_query:
|
|
202
|
+
index = self.parent_query.get_statement_index()
|
|
203
|
+
return index + ":" + str(self.statement_index)
|
|
204
|
+
else:
|
|
205
|
+
return str(self.statement_index)
|
|
206
|
+
|
|
207
|
+
def get_ctes(self) -> t.List:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
def get_column_defs(self, include_system: bool = False) -> t.List[exp.ColumnDef]:
|
|
211
|
+
return self.column_defs
|
|
212
|
+
|
|
213
|
+
def get_column_names_with_types(self, include_system: bool = False) -> t.Dict[str, str]:
|
|
214
|
+
"""
|
|
215
|
+
Used by sqlglot's MappingSchema
|
|
216
|
+
"""
|
|
217
|
+
columns = {col.name: str(col.kind) for col in self.get_column_defs(include_system=include_system)}
|
|
218
|
+
return columns
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def id(self) -> str:
|
|
222
|
+
return "query:" + util.short_sha256_hash(self.statement.sql() + ":" + str(self.statement_index))
|
|
223
|
+
|
|
224
|
+
def add_child_query(self, child_query):
|
|
225
|
+
child_query.parent_query = self
|
|
226
|
+
self.child_queries.append(child_query)
|
|
227
|
+
|
|
228
|
+
def get_all_queries(self, types: t.Tuple | None = None):
|
|
229
|
+
"""
|
|
230
|
+
Collect all queries (self + children recursively), optionally filtered by type.
|
|
231
|
+
"""
|
|
232
|
+
queries = [self]
|
|
233
|
+
|
|
234
|
+
for child in self.child_queries:
|
|
235
|
+
queries.extend(child.get_all_queries(types))
|
|
236
|
+
|
|
237
|
+
if types:
|
|
238
|
+
queries = [q for q in queries if isinstance(q, types)]
|
|
239
|
+
|
|
240
|
+
return queries
|
|
241
|
+
|
|
242
|
+
def to_dict(self):
|
|
243
|
+
result = {
|
|
244
|
+
"id": self.id,
|
|
245
|
+
"kind": self.kind,
|
|
246
|
+
"statement_index": self.statement_index,
|
|
247
|
+
}
|
|
248
|
+
return result
|
|
@@ -0,0 +1,111 @@
|
|
|
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 util
|
|
10
|
+
from sqlleaf.models.query.base import Query
|
|
11
|
+
from sqlleaf.typing import SqlObjectType, TargetInfo
|
|
12
|
+
|
|
13
|
+
if t.TYPE_CHECKING:
|
|
14
|
+
from sqlleaf import mappings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class CallQueryProperties:
|
|
19
|
+
schema: t.Optional[str]
|
|
20
|
+
procedure: str
|
|
21
|
+
args: t.List[exp.Expr]
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_expression(cls, statement: exp.Command, dialect: str) -> CallQueryProperties:
|
|
25
|
+
# The 'expression' part of the command contains the procedure call
|
|
26
|
+
# e.g., CALL hello('world') -> expression is "hello('world')"
|
|
27
|
+
call_str = statement.args.get("expression").this
|
|
28
|
+
|
|
29
|
+
# Parse the call string as a function call
|
|
30
|
+
inner_expr = sqlglot.parse_one(call_str, read=dialect)
|
|
31
|
+
|
|
32
|
+
schema = None
|
|
33
|
+
if isinstance(inner_expr, exp.Anonymous):
|
|
34
|
+
procedure_name = inner_expr.this
|
|
35
|
+
args = inner_expr.expressions
|
|
36
|
+
elif isinstance(inner_expr, exp.Column):
|
|
37
|
+
procedure_name = inner_expr.name
|
|
38
|
+
args = []
|
|
39
|
+
elif isinstance(inner_expr, exp.Dot):
|
|
40
|
+
schema = inner_expr.left.name
|
|
41
|
+
if isinstance(inner_expr.right, exp.Anonymous):
|
|
42
|
+
procedure_name = inner_expr.right.this
|
|
43
|
+
args = inner_expr.right.expressions
|
|
44
|
+
else:
|
|
45
|
+
procedure_name = inner_expr.right.name
|
|
46
|
+
args = []
|
|
47
|
+
else:
|
|
48
|
+
procedure_name = call_str
|
|
49
|
+
args = []
|
|
50
|
+
|
|
51
|
+
return cls(schema=schema, procedure=procedure_name, args=args)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CallQuery(Query):
|
|
55
|
+
"""
|
|
56
|
+
Holds metadata related to procedure calls (CALL statement).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
KIND = "call"
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
expr: exp.Command,
|
|
64
|
+
dialect: str,
|
|
65
|
+
object_mapping: mappings.ObjectMapping,
|
|
66
|
+
statement_index: int,
|
|
67
|
+
):
|
|
68
|
+
self.properties = CallQueryProperties.from_expression(expr, dialect)
|
|
69
|
+
|
|
70
|
+
# We can represent the procedure being called as the target
|
|
71
|
+
target_info = TargetInfo(
|
|
72
|
+
expression=exp.Table(
|
|
73
|
+
this=exp.to_identifier(self.properties.procedure),
|
|
74
|
+
db=exp.to_identifier(self.properties.schema) if self.properties.schema else None,
|
|
75
|
+
),
|
|
76
|
+
type=SqlObjectType.PROCEDURE,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
super().__init__(
|
|
80
|
+
dialect=dialect,
|
|
81
|
+
statement=expr,
|
|
82
|
+
statement_index=statement_index,
|
|
83
|
+
object_mapping=object_mapping,
|
|
84
|
+
source_info=None,
|
|
85
|
+
target_info=target_info,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def schema(self) -> t.Optional[str]:
|
|
90
|
+
return self.properties.schema
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def procedure(self) -> str:
|
|
94
|
+
return self.properties.procedure
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def args(self) -> t.List[exp.Expr]:
|
|
98
|
+
return self.properties.args
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def name(self):
|
|
102
|
+
return ".".join([var for var in [self.schema, self.procedure] if var])
|
|
103
|
+
|
|
104
|
+
def to_dict(self):
|
|
105
|
+
return {
|
|
106
|
+
"id": self.id,
|
|
107
|
+
"name": self.name,
|
|
108
|
+
"procedure": self.procedure,
|
|
109
|
+
"schema": self.schema,
|
|
110
|
+
"args": [str(arg) for arg in self.args],
|
|
111
|
+
}
|