sqlleaf 0.0.1__py3-none-any.whl → 0.0.2__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 +1 -0
- sqlleaf/dialects/__init__.py +1 -0
- sqlleaf/dialects/plpgsql/__init__.py +1 -0
- sqlleaf/dialects/plpgsql/p_classes.py +1 -1
- sqlleaf/dialects/plpgsql/p_generator.py +2 -2
- sqlleaf/dialects/plpgsql/p_parser.py +4 -4
- sqlleaf/exception.py +2 -2
- sqlleaf/lineage.py +8 -65
- sqlleaf/mappings.py +94 -34
- sqlleaf/models/context.py +1 -1
- sqlleaf/models/node/base.py +1 -1
- sqlleaf/models/query/__init__.py +51 -0
- sqlleaf/models/query/base.py +12 -21
- sqlleaf/models/query/block.py +35 -0
- sqlleaf/models/query/call.py +4 -4
- sqlleaf/models/query/copy.py +2 -2
- sqlleaf/models/query/ctas.py +6 -6
- sqlleaf/models/query/declare_item.py +33 -0
- sqlleaf/models/query/execute.py +2 -2
- sqlleaf/models/query/execute_dynamic.py +75 -0
- sqlleaf/models/query/fetch.py +25 -0
- sqlleaf/models/query/for_in.py +31 -0
- sqlleaf/models/query/holder.py +2 -2
- sqlleaf/models/query/if_.py +35 -0
- sqlleaf/models/query/loop.py +31 -0
- sqlleaf/models/query/multitable_insert.py +1 -1
- sqlleaf/models/query/open.py +54 -0
- sqlleaf/models/query/perform.py +12 -0
- sqlleaf/models/query/prepare.py +1 -1
- sqlleaf/models/query/procedure.py +76 -24
- sqlleaf/models/query/set.py +43 -11
- sqlleaf/models/query/stage.py +1 -1
- sqlleaf/models/query/table.py +10 -10
- sqlleaf/models/query/type.py +1 -1
- sqlleaf/models/query/unload.py +1 -1
- sqlleaf/models/query/user_defined_function.py +20 -28
- sqlleaf/models/query/view.py +3 -3
- sqlleaf/models/query/while_.py +30 -0
- sqlleaf/path.py +8 -8
- sqlleaf/processors/collector/classify.py +115 -0
- sqlleaf/processors/collector/collector.py +178 -284
- sqlleaf/processors/generator/dialects/base.py +50 -5
- sqlleaf/processors/generator/generator.py +61 -8
- sqlleaf/processors/substitutor/substitute.py +781 -0
- sqlleaf/processors/transformer/__init__.py +6 -2
- sqlleaf/processors/transformer/assignment.py +18 -0
- sqlleaf/processors/transformer/base.py +87 -88
- sqlleaf/processors/transformer/call.py +2 -1
- sqlleaf/processors/transformer/copy.py +2 -0
- sqlleaf/processors/transformer/ctas.py +2 -0
- sqlleaf/processors/transformer/delete.py +2 -0
- sqlleaf/processors/transformer/execute.py +2 -1
- sqlleaf/processors/transformer/execute_dynamic.py +10 -0
- sqlleaf/processors/transformer/expressions/__init__.py +5 -1
- sqlleaf/processors/transformer/expressions/format.py +251 -0
- sqlleaf/processors/transformer/expressions/row.py +12 -12
- sqlleaf/processors/transformer/expressions/values.py +115 -89
- sqlleaf/processors/transformer/insert.py +3 -1
- sqlleaf/processors/transformer/merge.py +2 -0
- sqlleaf/processors/transformer/multitable_insert.py +2 -0
- sqlleaf/processors/transformer/open.py +78 -0
- sqlleaf/processors/transformer/replace.py +3 -1
- sqlleaf/processors/transformer/transformer.py +90 -48
- sqlleaf/processors/transformer/unload.py +2 -0
- sqlleaf/processors/transformer/update.py +2 -0
- sqlleaf/processors/transformer/values.py +3 -2
- sqlleaf/settings/__init__.py +2 -2
- sqlleaf/settings/athena.py +1 -1
- sqlleaf/settings/base.py +2 -2
- sqlleaf/settings/postgres.py +2 -2
- sqlleaf/util/__init__.py +2 -0
- sqlleaf/util/expression.py +25 -4
- sqlleaf/util/graph.py +3 -3
- sqlleaf/util/helpers.py +2 -2
- sqlleaf/util/iterators.py +17 -6
- sqlleaf-0.0.2.dist-info/METADATA +229 -0
- sqlleaf-0.0.2.dist-info/RECORD +125 -0
- sqlleaf/processors/collector/substitute.py +0 -396
- sqlleaf/processors/transformer/udf.py +0 -235
- sqlleaf-0.0.1.dist-info/METADATA +0 -46
- sqlleaf-0.0.1.dist-info/RECORD +0 -111
- /sqlleaf/{system_tables/load.py → processors/substitutor/__init__.py} +0 -0
- {sqlleaf-0.0.1.dist-info → sqlleaf-0.0.2.dist-info}/WHEEL +0 -0
- {sqlleaf-0.0.1.dist-info → sqlleaf-0.0.2.dist-info}/licenses/LICENSE +0 -0
- {sqlleaf-0.0.1.dist-info → sqlleaf-0.0.2.dist-info}/top_level.txt +0 -0
sqlleaf/__init__.py
CHANGED
|
@@ -2,3 +2,4 @@ from sqlleaf.lineage import Lineage as Lineage
|
|
|
2
2
|
from sqlleaf.processors.collector.collector import collect_queries as collect_queries
|
|
3
3
|
from sqlleaf.processors.generator.generator import generate_lineage_for_query as generate_lineage_for_query
|
|
4
4
|
from sqlleaf.processors.transformer.transformer import transform_query as transform_query
|
|
5
|
+
from sqlleaf.dialects import PlPgSQL
|
sqlleaf/dialects/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from sqlleaf.dialects.plpgsql.p_dialect import PlPgSQL
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from sqlleaf.dialects.plpgsql import p_classes as pgexp
|
|
@@ -146,7 +146,7 @@ class PGGetDiagnostics(exp.Expression):
|
|
|
146
146
|
arg_types = {"current": False, "stacked": False, "expressions": True}
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
class
|
|
149
|
+
class PGOpen(exp.Expression):
|
|
150
150
|
"""An OPEN cursor statement."""
|
|
151
151
|
arg_types = {"this": True, "expression": False, "scroll": False, "expressions": False}
|
|
152
152
|
|
|
@@ -25,7 +25,7 @@ class PlPgSQLGenerator(PostgresGenerator):
|
|
|
25
25
|
PGContinue: lambda self, e: self.pgcontinue_sql(e),
|
|
26
26
|
PGRaise: lambda self, e: self.pgraise_sql(e),
|
|
27
27
|
PGSqlState: lambda self, e: self.pgsqlstate_sql(e),
|
|
28
|
-
|
|
28
|
+
PGOpen: lambda self, e: self.pgopen_sql(e),
|
|
29
29
|
PGFetch: lambda self, e: self.pgfetch_sql(e),
|
|
30
30
|
PGMove: lambda self, e: self.pgmove_sql(e),
|
|
31
31
|
PGClose: lambda self, e: self.pgclose_sql(e),
|
|
@@ -419,7 +419,7 @@ class PlPgSQLGenerator(PostgresGenerator):
|
|
|
419
419
|
return f"{self.sql(dir_expr)} {prep} {self.sql(expression.this)}"
|
|
420
420
|
return self.sql(expression.this)
|
|
421
421
|
|
|
422
|
-
def
|
|
422
|
+
def pgopen_sql(self, expression: PGOpen) -> str:
|
|
423
423
|
# Unbound form: OPEN c [NO|SCROLL] FOR <query>
|
|
424
424
|
if expression.args.get("expression") is not None:
|
|
425
425
|
parts: list[str] = ["OPEN", self.sql(expression.this)]
|
|
@@ -1053,7 +1053,7 @@ class PlPgSQLParser(PostgresParser):
|
|
|
1053
1053
|
|
|
1054
1054
|
return args
|
|
1055
1055
|
|
|
1056
|
-
def _parse_pgopen(self) ->
|
|
1056
|
+
def _parse_pgopen(self) -> PGOpen:
|
|
1057
1057
|
# Cursor variable identifier
|
|
1058
1058
|
cursor = self._parse_id_var()
|
|
1059
1059
|
|
|
@@ -1062,7 +1062,7 @@ class PlPgSQLParser(PostgresParser):
|
|
|
1062
1062
|
if self._match(TokenType.L_PAREN, advance=False):
|
|
1063
1063
|
# Parse cursor call arguments directly (positional, name := value, name => value)
|
|
1064
1064
|
args = self._parse_cursor_call_args()
|
|
1065
|
-
return self.expression(
|
|
1065
|
+
return self.expression(PGOpen(this=cursor, expressions=args))
|
|
1066
1066
|
|
|
1067
1067
|
# Optional [[NO] SCROLL]
|
|
1068
1068
|
scroll: bool | None = None
|
|
@@ -1075,7 +1075,7 @@ class PlPgSQLParser(PostgresParser):
|
|
|
1075
1075
|
# If there's no FOR and no SCROLL/NO SCROLL, treat as bound cursor with zero args: OPEN c;
|
|
1076
1076
|
if not self._match(TokenType.FOR):
|
|
1077
1077
|
if scroll is None:
|
|
1078
|
-
return self.expression(
|
|
1078
|
+
return self.expression(PGOpen(this=cursor))
|
|
1079
1079
|
# If SCROLL/NO SCROLL was provided, FOR is required
|
|
1080
1080
|
self._match_expect(TokenType.FOR)
|
|
1081
1081
|
|
|
@@ -1088,7 +1088,7 @@ class PlPgSQLParser(PostgresParser):
|
|
|
1088
1088
|
if query is None:
|
|
1089
1089
|
self.raise_error("Expected query after OPEN ... FOR")
|
|
1090
1090
|
|
|
1091
|
-
return self.expression(
|
|
1091
|
+
return self.expression(PGOpen(this=cursor, expression=query, scroll=scroll))
|
|
1092
1092
|
|
|
1093
1093
|
def _parse_pgfetch(self) -> PGFetch:
|
|
1094
1094
|
# Parse optional direction, optional preposition (required when direction given), and cursor
|
sqlleaf/exception.py
CHANGED
|
@@ -6,12 +6,12 @@ ERROR_LEVEL = "stop"
|
|
|
6
6
|
logger = logging.getLogger("sqlleaf")
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def raise_error(
|
|
9
|
+
def raise_error(cls: type, message: str):
|
|
10
10
|
"""
|
|
11
11
|
Raise a custom exception based on the error level.
|
|
12
12
|
"""
|
|
13
13
|
if ERROR_LEVEL == "stop":
|
|
14
|
-
raise
|
|
14
|
+
raise cls(message)
|
|
15
15
|
elif ERROR_LEVEL == "continue":
|
|
16
16
|
logger.error(message)
|
|
17
17
|
|
sqlleaf/lineage.py
CHANGED
|
@@ -8,21 +8,11 @@ from sqlleaf import exception, mappings, path, util
|
|
|
8
8
|
from sqlleaf.models.hook import Hook
|
|
9
9
|
from sqlleaf.models.node import EdgeAttributes, GraphAttributes, N
|
|
10
10
|
from sqlleaf.models.query import (
|
|
11
|
-
CopyQuery,
|
|
12
|
-
CTASQuery,
|
|
13
|
-
InsertQuery,
|
|
14
|
-
PutQuery,
|
|
15
11
|
Q,
|
|
16
|
-
TableQuery,
|
|
17
|
-
UnloadQuery,
|
|
18
|
-
UpdateQuery,
|
|
19
|
-
ViewQuery,
|
|
20
12
|
)
|
|
21
13
|
from sqlleaf.path import LineagePath
|
|
22
14
|
from sqlleaf.processors.collector import collector
|
|
23
15
|
from sqlleaf.processors.generator import generator
|
|
24
|
-
from sqlleaf.processors.transformer import transformer
|
|
25
|
-
from sqlleaf.typing import SqlObjectType
|
|
26
16
|
|
|
27
17
|
logging.getLogger("sqlglot").setLevel(logging.WARNING)
|
|
28
18
|
logger = logging.getLogger("sqlleaf")
|
|
@@ -35,8 +25,8 @@ class Lineage:
|
|
|
35
25
|
|
|
36
26
|
def __init__(self):
|
|
37
27
|
self.graph = new_graph() # The graph that contains all lineage
|
|
38
|
-
self.subgraphs:
|
|
39
|
-
self.paths:
|
|
28
|
+
self.subgraphs: list[nx.MultiDiGraph] = [] # The subgraphs that make up the main graph
|
|
29
|
+
self.paths: dict[str, list[LineagePath]] = {} # The paths throughout the graph
|
|
40
30
|
self.collected_queries: collector.CollectQueryResult | None = None
|
|
41
31
|
self.user_defined_hooks: list[Hook] = []
|
|
42
32
|
self.object_mapping: mappings.ObjectMapping | None = None
|
|
@@ -60,11 +50,8 @@ class Lineage:
|
|
|
60
50
|
query_holders = parent_holder.get_all_holders()
|
|
61
51
|
|
|
62
52
|
for holder in query_holders:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for holder in query_holders:
|
|
66
|
-
# Transform and produce lineage only for certain queries
|
|
67
|
-
if query_has_lineage(holder.original):
|
|
53
|
+
# Produce lineage only for certain queries
|
|
54
|
+
if generator.query_has_lineage(holder.original):
|
|
68
55
|
generator.generate_lineage_for_query(holder, graph, self.user_defined_hooks)
|
|
69
56
|
|
|
70
57
|
# Associate the query with the graph even if it has no lineage
|
|
@@ -96,14 +83,14 @@ class Lineage:
|
|
|
96
83
|
|
|
97
84
|
self.graph.add_edges_from(subgraph.edges(data=True))
|
|
98
85
|
|
|
99
|
-
def get_edges(self) ->
|
|
86
|
+
def get_edges(self) -> list[EdgeAttributes]:
|
|
100
87
|
edges = [data["attrs"] for par, chi, data in self.graph.edges(data=True)]
|
|
101
88
|
edges = sorted(edges, key=lambda e: (e.parent.full_name, e.child.full_name))
|
|
102
89
|
for edge in edges:
|
|
103
90
|
logger.debug(f"Found edge: {edge.parent.friendly_name} -> {edge.child.friendly_name}")
|
|
104
91
|
return edges
|
|
105
92
|
|
|
106
|
-
def get_nodes(self) ->
|
|
93
|
+
def get_nodes(self) -> list[N]:
|
|
107
94
|
nodes = [data["attrs"] for n, data in self.graph.nodes(data=True)]
|
|
108
95
|
# TODO: sort on selected index?
|
|
109
96
|
nodes = sorted(
|
|
@@ -111,13 +98,13 @@ class Lineage:
|
|
|
111
98
|
)
|
|
112
99
|
return nodes
|
|
113
100
|
|
|
114
|
-
def get_original_queries(self) ->
|
|
101
|
+
def get_original_queries(self) -> list[Q]:
|
|
115
102
|
"""
|
|
116
103
|
Get the original queries from the graph.
|
|
117
104
|
"""
|
|
118
105
|
return [q.original for q in self.graph.graph["attrs"].queries]
|
|
119
106
|
|
|
120
|
-
def get_transformed_queries(self) ->
|
|
107
|
+
def get_transformed_queries(self) -> list[Q]:
|
|
121
108
|
"""
|
|
122
109
|
Get the transformed queries from the graph.
|
|
123
110
|
"""
|
|
@@ -280,47 +267,3 @@ def new_graph() -> nx.MultiDiGraph:
|
|
|
280
267
|
A graph has attributes along with its node and edges.
|
|
281
268
|
"""
|
|
282
269
|
return nx.MultiDiGraph(attrs=GraphAttributes())
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
QUERIES_WITH_LINEAGE = (
|
|
286
|
-
CTASQuery,
|
|
287
|
-
CopyQuery,
|
|
288
|
-
InsertQuery,
|
|
289
|
-
PutQuery,
|
|
290
|
-
TableQuery,
|
|
291
|
-
UnloadQuery,
|
|
292
|
-
UpdateQuery,
|
|
293
|
-
ViewQuery,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def query_has_lineage(query: Q) -> bool:
|
|
298
|
-
"""
|
|
299
|
-
Check if a query has lineage within its expressions.
|
|
300
|
-
|
|
301
|
-
We distinguish between a query's definition and a query that is called.
|
|
302
|
-
That is, in order for a `CREATE FUNCTION` or `CREATE PROCEDURE` to have
|
|
303
|
-
lineage, it must be executed by an invoking statement, e.g. `CALL()` or `SELECT UDF()`
|
|
304
|
-
Simply having statements inside its definition is not sufficient to produce lineage.
|
|
305
|
-
"""
|
|
306
|
-
has_lineage = True
|
|
307
|
-
if not isinstance(query, QUERIES_WITH_LINEAGE):
|
|
308
|
-
has_lineage = False
|
|
309
|
-
elif isinstance(query, CopyQuery) and query.source_info.type == SqlObjectType.VALUES:
|
|
310
|
-
# COPY TO STDOUT VALUES (..) # TODO: this should have lineage
|
|
311
|
-
has_lineage = False
|
|
312
|
-
elif isinstance(query, CopyQuery) and not query.is_query_active():
|
|
313
|
-
has_lineage = False
|
|
314
|
-
elif isinstance(query, CTASQuery) and not query.load_data:
|
|
315
|
-
# CREATE TABLE WITH NO DATA
|
|
316
|
-
has_lineage = False
|
|
317
|
-
elif isinstance(query, CTASQuery) and query.source_info.type == SqlObjectType.PREPARED_STATEMENT:
|
|
318
|
-
# CREATE TABLE AS EXECUTE
|
|
319
|
-
has_lineage = False
|
|
320
|
-
elif isinstance(query, TableQuery) and query.property != "external":
|
|
321
|
-
# CREATE EXTERNAL TABLE
|
|
322
|
-
has_lineage = False
|
|
323
|
-
|
|
324
|
-
if not has_lineage:
|
|
325
|
-
logger.debug(f"Query type '{query.__class__.__name__}' does NOT have lineage. Skipping.")
|
|
326
|
-
return has_lineage
|
sqlleaf/mappings.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import typing as t
|
|
4
5
|
|
|
5
6
|
from sqlglot import MappingSchema, exp
|
|
@@ -24,68 +25,119 @@ from sqlleaf.models.query import (
|
|
|
24
25
|
ViewQuery,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
|
-
ColumnMapping = t.Union[
|
|
28
|
+
ColumnMapping = t.Union[dict, str, list]
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("sqlleaf")
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class ObjectMapping(MappingSchema):
|
|
31
34
|
"""
|
|
32
|
-
|
|
35
|
+
Contains all the objects that get created, such as tables, procedures, functions, and sequences.
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
We extends sqlglot.MappingSchema to provide additional functionality related to tracking these objects,
|
|
38
|
+
while still allowing optimizer inside sqlglot to work (as it needs a mapping of objects to perform functions
|
|
39
|
+
like qualification and simplification).
|
|
36
40
|
"""
|
|
37
41
|
|
|
38
42
|
def __init__(self, dialect: str):
|
|
39
|
-
"""
|
|
40
|
-
Initialize a mapping of tables parts to exp.Table
|
|
41
|
-
"""
|
|
42
43
|
super().__init__(
|
|
43
44
|
dialect=dialect, normalize=False
|
|
44
45
|
) # Set `normalize=False` to prevent an unnecessary second parse.
|
|
45
46
|
self.kind_mapping = {}
|
|
46
47
|
self.kind_mapping_trie = {}
|
|
48
|
+
# Session/global variables (e.g., SET var = ...)
|
|
47
49
|
self.session_variables: dict[str, exp.Expr] = {}
|
|
50
|
+
# Block-scoped variables: one dict per active block, innermost scope at the end
|
|
51
|
+
self.variable_scopes: list[dict[str, exp.Expr]] = []
|
|
52
|
+
|
|
53
|
+
# ---- Variable scope API ----
|
|
54
|
+
def push_variable_scope(self) -> None:
|
|
55
|
+
"""Enter a new block scope."""
|
|
56
|
+
self.variable_scopes.append({})
|
|
57
|
+
|
|
58
|
+
def pop_variable_scope(self) -> None:
|
|
59
|
+
"""Leave the current block scope."""
|
|
60
|
+
if self.variable_scopes:
|
|
61
|
+
self.variable_scopes.pop()
|
|
62
|
+
|
|
63
|
+
def set_variable(self, name: str, value: exp.Expr) -> None:
|
|
64
|
+
"""Assign or declare a variable in the current scope."""
|
|
65
|
+
self.variable_scopes[-1][name] = value
|
|
66
|
+
logger.debug(f"Set variable value: {name} := {value.sql()}")
|
|
67
|
+
|
|
68
|
+
def get_variable(self, name: str, raise_on_missing: bool = True) -> exp.Expr | None:
|
|
69
|
+
"""Resolve a variable by searching from innermost to outermost scopes, then session vars."""
|
|
70
|
+
for scope in reversed(self.variable_scopes):
|
|
71
|
+
if name in scope:
|
|
72
|
+
return scope[name]
|
|
73
|
+
if raise_on_missing:
|
|
74
|
+
raise exception.MappingError(f"Could not find value for variable: {name}")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def get_variables_for(self, expressions: list[exp.Expr]) -> list[exp.Expr]:
|
|
78
|
+
"""
|
|
79
|
+
Given a list of expressions as keys, fetch their values from the variable scope if they exist.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
expressions = [ Literal[42], Column[name] ]
|
|
83
|
+
variable scope = { Column[name] -> Literal['john'] }
|
|
84
|
+
->
|
|
85
|
+
[ Literal['john'] ]
|
|
86
|
+
"""
|
|
87
|
+
resolved = []
|
|
88
|
+
for expr in expressions:
|
|
89
|
+
if isinstance(expr, exp.Column) and not expr.table:
|
|
90
|
+
variable = self.get_variable(expr.name)
|
|
91
|
+
if variable is not None:
|
|
92
|
+
resolved.append(variable.copy())
|
|
93
|
+
continue
|
|
94
|
+
resolved.append(expr)
|
|
95
|
+
return resolved
|
|
96
|
+
|
|
97
|
+
def get_scope(self) -> dict[str, exp.Expr]:
|
|
98
|
+
"""Return the current scope dict (innermost)."""
|
|
99
|
+
return self.variable_scopes[-1]
|
|
48
100
|
|
|
49
101
|
def add_database_query(self, query: DatabaseQuery) -> None:
|
|
50
|
-
self._add_query(
|
|
102
|
+
self._add_query(query=query, dialect=query.dialect)
|
|
51
103
|
|
|
52
104
|
def add_prepare_query(self, query: PrepareQuery) -> None:
|
|
53
|
-
self._add_query(
|
|
105
|
+
self._add_query(query=query, dialect=query.dialect)
|
|
54
106
|
|
|
55
107
|
def add_procedure_query(self, query: ProcedureQuery) -> None:
|
|
56
|
-
self._add_query(
|
|
108
|
+
self._add_query(query=query, dialect=query.dialect)
|
|
57
109
|
|
|
58
110
|
def add_schema_query(self, query: SchemaQuery) -> None:
|
|
59
|
-
self._add_query(
|
|
111
|
+
self._add_query(query=query, dialect=query.dialect)
|
|
60
112
|
|
|
61
113
|
def add_sequence_query(self, query: SequenceQuery) -> None:
|
|
62
|
-
self._add_query(
|
|
114
|
+
self._add_query(query=query, dialect=query.dialect)
|
|
63
115
|
|
|
64
116
|
def add_stage_query(self, query: StageQuery) -> None:
|
|
65
|
-
self._add_query(
|
|
117
|
+
self._add_query(query=query, dialect=query.dialect)
|
|
66
118
|
|
|
67
119
|
def add_table_query(
|
|
68
|
-
self, query: TableQuery | ViewQuery | CTASQuery, column_mapping:
|
|
120
|
+
self, query: TableQuery | ViewQuery | CTASQuery, column_mapping: ColumnMapping | None = None
|
|
69
121
|
) -> None:
|
|
70
|
-
self._add_query(
|
|
122
|
+
self._add_query(query=query, column_mapping=column_mapping, dialect=query.dialect, kind="table")
|
|
71
123
|
|
|
72
124
|
def add_trigger_query(self, query: TriggerQuery) -> None:
|
|
73
|
-
self._add_query(
|
|
125
|
+
self._add_query(query=query, dialect=query.dialect)
|
|
74
126
|
|
|
75
127
|
def add_type_query(self, query: TypeQuery) -> None:
|
|
76
|
-
self._add_query(
|
|
128
|
+
self._add_query( query=query, dialect=query.dialect)
|
|
77
129
|
|
|
78
|
-
def add_udf_query(self, query: UserDefinedFunctionQuery, column_mapping:
|
|
79
|
-
self._add_query(
|
|
130
|
+
def add_udf_query(self, query: UserDefinedFunctionQuery, column_mapping: ColumnMapping | None = None) -> None:
|
|
131
|
+
self._add_query(query=query, column_mapping=column_mapping, dialect=query.dialect)
|
|
80
132
|
|
|
81
133
|
def _add_query(
|
|
82
134
|
self,
|
|
83
|
-
kind: str,
|
|
84
135
|
query: Q,
|
|
85
|
-
column_mapping:
|
|
136
|
+
column_mapping: ColumnMapping | None = None,
|
|
86
137
|
dialect: DialectType = None,
|
|
87
|
-
normalize:
|
|
138
|
+
normalize: bool | None = None,
|
|
88
139
|
match_depth: bool = False,
|
|
140
|
+
kind: str = ""
|
|
89
141
|
) -> None:
|
|
90
142
|
"""
|
|
91
143
|
Register or update a table. Updates are only performed if a new column mapping is provided.
|
|
@@ -100,6 +152,9 @@ class ObjectMapping(MappingSchema):
|
|
|
100
152
|
match_depth: whether to enforce that the table must match the schema's depth or not.
|
|
101
153
|
"""
|
|
102
154
|
# Initialize the structures. These are required by sqlglot.
|
|
155
|
+
if not kind:
|
|
156
|
+
kind = query.kind
|
|
157
|
+
|
|
103
158
|
if kind not in self.kind_mapping:
|
|
104
159
|
self.kind_mapping[kind] = {}
|
|
105
160
|
self.kind_mapping_trie[kind] = new_trie({})
|
|
@@ -133,11 +188,11 @@ class ObjectMapping(MappingSchema):
|
|
|
133
188
|
def _add_columns_for_table(
|
|
134
189
|
self,
|
|
135
190
|
table: exp.Table,
|
|
136
|
-
column_mapping:
|
|
191
|
+
column_mapping: ColumnMapping | None = None,
|
|
137
192
|
dialect: DialectType = None,
|
|
138
|
-
normalize:
|
|
193
|
+
normalize: bool | None = None,
|
|
139
194
|
match_depth: bool = False,
|
|
140
|
-
):
|
|
195
|
+
) -> None:
|
|
141
196
|
super().add_table(
|
|
142
197
|
table=table,
|
|
143
198
|
column_mapping=column_mapping,
|
|
@@ -151,7 +206,7 @@ class ObjectMapping(MappingSchema):
|
|
|
151
206
|
table: exp.Table,
|
|
152
207
|
raise_on_missing: bool = True,
|
|
153
208
|
ensure_data_types: bool = False,
|
|
154
|
-
):
|
|
209
|
+
) -> None:
|
|
155
210
|
"""
|
|
156
211
|
A nicer name for the parent's function.
|
|
157
212
|
"""
|
|
@@ -184,7 +239,7 @@ class ObjectMapping(MappingSchema):
|
|
|
184
239
|
|
|
185
240
|
def lookup_udf_query(
|
|
186
241
|
self, table: exp.Table, raise_on_missing: bool = True
|
|
187
|
-
) ->
|
|
242
|
+
) -> list[UserDefinedFunctionQuery] | None:
|
|
188
243
|
# A UDF can have multiple names but different types, thus we return a list
|
|
189
244
|
return self._lookup_query(kind="udf", object=table, raise_on_missing=raise_on_missing)
|
|
190
245
|
|
|
@@ -210,7 +265,12 @@ class ObjectMapping(MappingSchema):
|
|
|
210
265
|
"""
|
|
211
266
|
if kind not in self.kind_mapping:
|
|
212
267
|
if raise_on_missing:
|
|
213
|
-
|
|
268
|
+
if kind == "prepare":
|
|
269
|
+
knd = "PREPARE"
|
|
270
|
+
else:
|
|
271
|
+
knd = "CREATE " + {"udf": "FUNCTION"}.get(kind, kind).upper()
|
|
272
|
+
|
|
273
|
+
raise exception.MappingError(f"Could not find '{object.this}' of type '{kind}' in mapping. Did you forget to define a '{knd}' statement?")
|
|
214
274
|
return None
|
|
215
275
|
|
|
216
276
|
parts = self.table_parts(object)[0: len(self.supported_table_args)]
|
|
@@ -234,7 +294,7 @@ class ObjectMapping(MappingSchema):
|
|
|
234
294
|
|
|
235
295
|
# Override sqlglot's property. It seems to be buggy when using different dict sizes (catalog, schema, etc)
|
|
236
296
|
@property
|
|
237
|
-
def supported_table_args(self) ->
|
|
297
|
+
def supported_table_args(self) -> tuple[str, ...]:
|
|
238
298
|
return exp.TABLE_PARTS
|
|
239
299
|
|
|
240
300
|
def get_table_or_stage(
|
|
@@ -248,7 +308,7 @@ class ObjectMapping(MappingSchema):
|
|
|
248
308
|
table_expr = exp.Table(this=table)
|
|
249
309
|
|
|
250
310
|
if str(table_expr).startswith("@"):
|
|
251
|
-
child_object_query = self.lookup_stage_query(table=table_expr, raise_on_missing=
|
|
311
|
+
child_object_query = self.lookup_stage_query(table=table_expr, raise_on_missing=False)
|
|
252
312
|
else:
|
|
253
313
|
child_object_query = self.lookup_table_query(table=table_expr, raise_on_missing=raise_on_missing)
|
|
254
314
|
|
|
@@ -260,7 +320,7 @@ class ObjectMapping(MappingSchema):
|
|
|
260
320
|
def lookup_udf_call(
|
|
261
321
|
self,
|
|
262
322
|
node: exp.Anonymous,
|
|
263
|
-
) ->
|
|
323
|
+
) -> UserDefinedFunctionQuery | None:
|
|
264
324
|
"""
|
|
265
325
|
Looks up the UDF definition for a single exp.Anonymous node.
|
|
266
326
|
Returns the matched UDF definition, or None if not found.
|
|
@@ -275,8 +335,8 @@ class ObjectMapping(MappingSchema):
|
|
|
275
335
|
|
|
276
336
|
|
|
277
337
|
def resolve_overloaded_function(
|
|
278
|
-
node: exp.Anonymous, candidates:
|
|
279
|
-
) ->
|
|
338
|
+
node: exp.Anonymous, candidates: list[UserDefinedFunctionQuery]
|
|
339
|
+
) -> UserDefinedFunctionQuery | None:
|
|
280
340
|
"""
|
|
281
341
|
Resolves the best function candidate for an overloaded function call.
|
|
282
342
|
Applies some precedence rules (e.g., type matching, or non-variadic > variadic) but many are currently
|
|
@@ -323,7 +383,7 @@ def resolve_overloaded_function(
|
|
|
323
383
|
return matches[0]
|
|
324
384
|
|
|
325
385
|
|
|
326
|
-
def match_function_arguments(args:
|
|
386
|
+
def match_function_arguments(args: list[exp.Expr], candidate: UserDefinedFunctionQuery) -> bool:
|
|
327
387
|
"""
|
|
328
388
|
Checks if the provided arguments match the function candidate's parameters.
|
|
329
389
|
Handles exact types, polymorphic types (anyelement, anyarray), and VARIADIC parameters.
|
sqlleaf/models/context.py
CHANGED
|
@@ -17,7 +17,7 @@ logger = logging.getLogger("sqlleaf")
|
|
|
17
17
|
|
|
18
18
|
class ScopePositions:
|
|
19
19
|
def __init__(self):
|
|
20
|
-
self.positions:
|
|
20
|
+
self.positions: dict[int, dict[int, int]] = {}
|
|
21
21
|
|
|
22
22
|
def get_position_for_expr(self, expr: exp.Expr) -> dict[int, int]:
|
|
23
23
|
return self.positions[id(expr)]
|
sqlleaf/models/node/base.py
CHANGED
sqlleaf/models/query/__init__.py
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
|
|
3
3
|
from sqlleaf.models.query.base import Query as Query
|
|
4
|
+
from sqlleaf.models.query.block import BlockQuery as BlockQuery
|
|
4
5
|
from sqlleaf.models.query.call import CallQuery as CallQuery
|
|
5
6
|
from sqlleaf.models.query.copy import CopyQuery as CopyQuery
|
|
6
7
|
from sqlleaf.models.query.ctas import CTASQuery as CTASQuery
|
|
7
8
|
from sqlleaf.models.query.database import DatabaseQuery as DatabaseQuery
|
|
9
|
+
from sqlleaf.models.query.declare_item import AssignmentQuery as AssignmentQuery
|
|
8
10
|
from sqlleaf.models.query.delete import DeleteQuery as DeleteQuery
|
|
9
11
|
from sqlleaf.models.query.execute import ExecuteQuery as ExecuteQuery
|
|
12
|
+
from sqlleaf.models.query.execute_dynamic import ExecuteDynamicQuery as ExecuteDynamicQuery
|
|
13
|
+
from sqlleaf.models.query.fetch import FetchQuery as FetchQuery
|
|
14
|
+
from sqlleaf.models.query.for_in import ForInQuery as ForInQuery
|
|
10
15
|
from sqlleaf.models.query.holder import QueryHolder as QueryHolder
|
|
11
16
|
from sqlleaf.models.query.insert import InsertQuery as InsertQuery
|
|
17
|
+
from sqlleaf.models.query.loop import LoopQuery as LoopQuery
|
|
12
18
|
from sqlleaf.models.query.merge import MergeQuery as MergeQuery
|
|
13
19
|
from sqlleaf.models.query.multitable_insert import MultitableInsertQuery as MultitableInsertQuery
|
|
20
|
+
from sqlleaf.models.query.open import OpenQuery as OpenQuery
|
|
14
21
|
from sqlleaf.models.query.prepare import PrepareQuery as PrepareQuery
|
|
15
22
|
from sqlleaf.models.query.procedure import ProcedureQuery as ProcedureQuery
|
|
16
23
|
from sqlleaf.models.query.put import PutQuery as PutQuery
|
|
17
24
|
from sqlleaf.models.query.replace import ReplaceQuery as ReplaceQuery
|
|
18
25
|
from sqlleaf.models.query.schema import SchemaQuery as SchemaQuery
|
|
19
26
|
from sqlleaf.models.query.select import SelectQuery as SelectQuery
|
|
27
|
+
from sqlleaf.models.query.perform import PerformQuery as PerformQuery
|
|
20
28
|
from sqlleaf.models.query.sequence import SequenceQuery as SequenceQuery
|
|
21
29
|
from sqlleaf.models.query.set import SetQuery as SetQuery
|
|
22
30
|
from sqlleaf.models.query.stage import StageQuery as StageQuery
|
|
@@ -29,5 +37,48 @@ from sqlleaf.models.query.user_defined_function import FunctionParam as Function
|
|
|
29
37
|
from sqlleaf.models.query.user_defined_function import UserDefinedFunctionQuery as UserDefinedFunctionQuery
|
|
30
38
|
from sqlleaf.models.query.values import ValuesQuery as ValuesQuery
|
|
31
39
|
from sqlleaf.models.query.view import ViewQuery as ViewQuery
|
|
40
|
+
from sqlleaf.models.query.while_ import WhileQuery as WhileQuery
|
|
32
41
|
|
|
33
42
|
Q = t.TypeVar("Q", bound=Query)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"AssignmentQuery",
|
|
46
|
+
"BlockQuery",
|
|
47
|
+
"CallQuery",
|
|
48
|
+
"CopyQuery",
|
|
49
|
+
"CTASQuery",
|
|
50
|
+
"DatabaseQuery",
|
|
51
|
+
"DeleteQuery",
|
|
52
|
+
"ExecuteDynamicQuery",
|
|
53
|
+
"ExecuteQuery",
|
|
54
|
+
"FetchQuery",
|
|
55
|
+
"ForInQuery",
|
|
56
|
+
"FunctionParam",
|
|
57
|
+
"InsertQuery",
|
|
58
|
+
"LoopQuery",
|
|
59
|
+
"MergeQuery",
|
|
60
|
+
"MultitableInsertQuery",
|
|
61
|
+
"OpenQuery",
|
|
62
|
+
"PerformQuery",
|
|
63
|
+
"PrepareQuery",
|
|
64
|
+
"ProcedureQuery",
|
|
65
|
+
"PutQuery",
|
|
66
|
+
"Q",
|
|
67
|
+
"Query",
|
|
68
|
+
"QueryHolder",
|
|
69
|
+
"ReplaceQuery",
|
|
70
|
+
"SchemaQuery",
|
|
71
|
+
"SelectQuery",
|
|
72
|
+
"SequenceQuery",
|
|
73
|
+
"SetQuery",
|
|
74
|
+
"StageQuery",
|
|
75
|
+
"TableQuery",
|
|
76
|
+
"TriggerQuery",
|
|
77
|
+
"TypeQuery",
|
|
78
|
+
"UnloadQuery",
|
|
79
|
+
"UpdateQuery",
|
|
80
|
+
"UserDefinedFunctionQuery",
|
|
81
|
+
"ValuesQuery",
|
|
82
|
+
"ViewQuery",
|
|
83
|
+
"WhileQuery",
|
|
84
|
+
]
|
sqlleaf/models/query/base.py
CHANGED
|
@@ -25,15 +25,14 @@ class Query:
|
|
|
25
25
|
statement_index: int,
|
|
26
26
|
object_mapping: mappings.ObjectMapping,
|
|
27
27
|
source_info: SourceInfo | None,
|
|
28
|
-
target_info: TargetInfo,
|
|
28
|
+
target_info: TargetInfo | None,
|
|
29
29
|
skip_type_annotation: bool = False,
|
|
30
30
|
):
|
|
31
31
|
self.kind = self.KIND
|
|
32
32
|
self.dialect = dialect
|
|
33
33
|
self.object_mapping = object_mapping
|
|
34
|
-
self.parent_query = None
|
|
35
34
|
self.child_queries = []
|
|
36
|
-
self.column_defs:
|
|
35
|
+
self.column_defs: list[exp.ColumnDef] = []
|
|
37
36
|
self.property = ""
|
|
38
37
|
|
|
39
38
|
# Remove comments at initialization
|
|
@@ -54,10 +53,10 @@ class Query:
|
|
|
54
53
|
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
54
|
)
|
|
56
55
|
|
|
57
|
-
def set_holder(self, holder: QueryHolder):
|
|
56
|
+
def set_holder(self, holder: QueryHolder) -> None:
|
|
58
57
|
self.holder = holder
|
|
59
58
|
|
|
60
|
-
def _determine_expression_type(self, expr: exp.Expr |
|
|
59
|
+
def _determine_expression_type(self, expr: exp.Expr | list[exp.Expr], dialect: str) -> SqlObjectType:
|
|
61
60
|
"""
|
|
62
61
|
Determine the type of object that an expression represents.
|
|
63
62
|
For example, the literal "/tmp/data.csv" is of type FILE; identifier "STDIN" is of type STREAM.
|
|
@@ -126,7 +125,7 @@ class Query:
|
|
|
126
125
|
def get_transformed_self(self) -> Query | None:
|
|
127
126
|
return self.holder.transformed
|
|
128
127
|
|
|
129
|
-
def get_columns_from_target(self) ->
|
|
128
|
+
def get_columns_from_target(self) -> list[exp.ColumnDef]:
|
|
130
129
|
"""
|
|
131
130
|
Given a query, figure out its target object, including its columns.
|
|
132
131
|
|
|
@@ -160,7 +159,7 @@ class Query:
|
|
|
160
159
|
def _get_column_defs(
|
|
161
160
|
self,
|
|
162
161
|
expr: SourceExprType | TargetExprType,
|
|
163
|
-
) ->
|
|
162
|
+
) -> list[exp.ColumnDef]:
|
|
164
163
|
"""
|
|
165
164
|
Most of the time, the sources and target are tables.
|
|
166
165
|
However, with COPY/UNLOAD, they can be files or streams.
|
|
@@ -198,19 +197,15 @@ class Query:
|
|
|
198
197
|
"""
|
|
199
198
|
Get the statement index for this query (including its parents).
|
|
200
199
|
"""
|
|
201
|
-
|
|
202
|
-
index = self.parent_query.get_statement_index()
|
|
203
|
-
return index + ":" + str(self.statement_index)
|
|
204
|
-
else:
|
|
205
|
-
return str(self.statement_index)
|
|
200
|
+
return str(self.statement_index)
|
|
206
201
|
|
|
207
|
-
def get_ctes(self) ->
|
|
202
|
+
def get_ctes(self) -> list:
|
|
208
203
|
return []
|
|
209
204
|
|
|
210
|
-
def get_column_defs(self, include_system: bool = False) ->
|
|
205
|
+
def get_column_defs(self, include_system: bool = False) -> list[exp.ColumnDef]:
|
|
211
206
|
return self.column_defs
|
|
212
207
|
|
|
213
|
-
def get_column_names_with_types(self, include_system: bool = False) ->
|
|
208
|
+
def get_column_names_with_types(self, include_system: bool = False) -> dict[str, str]:
|
|
214
209
|
"""
|
|
215
210
|
Used by sqlglot's MappingSchema
|
|
216
211
|
"""
|
|
@@ -219,13 +214,9 @@ class Query:
|
|
|
219
214
|
|
|
220
215
|
@property
|
|
221
216
|
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)
|
|
217
|
+
return "query:" + util.short_sha256_hash(self.statement.sql(dialect=self.dialect) + ":" + str(self.statement_index))
|
|
227
218
|
|
|
228
|
-
def get_all_queries(self, types:
|
|
219
|
+
def get_all_queries(self, types: tuple | None = None):
|
|
229
220
|
"""
|
|
230
221
|
Collect all queries (self + children recursively), optionally filtered by type.
|
|
231
222
|
"""
|