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,827 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing as t
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
import sqlglot
|
|
7
|
+
from sqlglot import TokenType, exp
|
|
8
|
+
from sqlglot.expressions import ColumnDef
|
|
9
|
+
from sqlleaf.processors.transformer import udf
|
|
10
|
+
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
|
|
11
|
+
|
|
12
|
+
from sqlleaf import exception, mappings, settings, util, typing
|
|
13
|
+
from sqlleaf.models.query import (
|
|
14
|
+
CallQuery,
|
|
15
|
+
CopyQuery,
|
|
16
|
+
CTASQuery,
|
|
17
|
+
DatabaseQuery,
|
|
18
|
+
DeleteQuery,
|
|
19
|
+
ExecuteQuery,
|
|
20
|
+
InsertQuery,
|
|
21
|
+
MergeQuery,
|
|
22
|
+
MultitableInsertQuery,
|
|
23
|
+
PrepareQuery,
|
|
24
|
+
ProcedureQuery,
|
|
25
|
+
PutQuery,
|
|
26
|
+
Q,
|
|
27
|
+
QueryHolder,
|
|
28
|
+
ReplaceQuery,
|
|
29
|
+
SchemaQuery,
|
|
30
|
+
SelectQuery,
|
|
31
|
+
SequenceQuery,
|
|
32
|
+
SetQuery,
|
|
33
|
+
StageQuery,
|
|
34
|
+
TableQuery,
|
|
35
|
+
TriggerQuery,
|
|
36
|
+
TypeQuery,
|
|
37
|
+
UnloadQuery,
|
|
38
|
+
UpdateQuery,
|
|
39
|
+
UserDefinedFunctionQuery,
|
|
40
|
+
ValuesQuery,
|
|
41
|
+
ViewQuery,
|
|
42
|
+
)
|
|
43
|
+
from sqlleaf.processors.collector import substitute
|
|
44
|
+
from sqlleaf.processors.transformer.expressions.row import simplify_row_in_values
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger("sqlleaf")
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
Parses text for SQL statements and collects them into Query models.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class CollectQueryResult:
|
|
55
|
+
queries: t.List[QueryHolder] = field(default_factory=list) # Successfully collected queries
|
|
56
|
+
unknown: t.Dict[str, int] = field(default_factory=dict) # Unsupported by sqlleaf (no handler)
|
|
57
|
+
unsupported: t.List[t.Tuple[int, exp.Expr]] = field(default_factory=list) # Unsupported by sqlglot (no grammar)
|
|
58
|
+
object_mapping: mappings.ObjectMapping = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_prepare_supported(stmt: exp.Command) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Check if a PREPARE statement for Postgres is supported.
|
|
64
|
+
Syntax: PREPARE name AS statement
|
|
65
|
+
The variant where parameters are provided is not yet supported.
|
|
66
|
+
"""
|
|
67
|
+
expression_name = stmt.expression.name
|
|
68
|
+
tokens = sqlglot.tokenize(expression_name, dialect="postgres")
|
|
69
|
+
|
|
70
|
+
if len(tokens) < 3:
|
|
71
|
+
raise exception.InvalidQueryError(f"Invalid syntax for PREPARE expression: {stmt.sql(dialect='postgres')}")
|
|
72
|
+
|
|
73
|
+
# We cannot process arguments yet
|
|
74
|
+
if tokens[1].token_type != TokenType.ALIAS or tokens[1].text.upper() != "AS":
|
|
75
|
+
if tokens[1].token_type == TokenType.L_PAREN:
|
|
76
|
+
logger.warning("PREPARE with arguments is not currently supported.")
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Ensure there's an 'AS' token
|
|
80
|
+
if not any(t.token_type == TokenType.ALIAS and t.text.upper() == "AS" for t in tokens):
|
|
81
|
+
raise exception.InvalidQueryError(f"Could not find 'AS' in PREPARE expression: {expression_name}")
|
|
82
|
+
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_execute_supported(stmt: exp.Command) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Check if an EXECUTE statement for Postgres is supported.
|
|
89
|
+
Syntax: EXECUTE name [ ( parameter [, ...] ) ]
|
|
90
|
+
|
|
91
|
+
Only the bare-name form (exactly one token, no argument parentheses) is supported.
|
|
92
|
+
If the expression contains more than one token it means arguments were supplied
|
|
93
|
+
(e.g. EXECUTE my_plan(arg1, arg2)), which is not yet handled.
|
|
94
|
+
"""
|
|
95
|
+
expression_name = stmt.expression.name
|
|
96
|
+
tokens = sqlglot.tokenize(expression_name, dialect="postgres")
|
|
97
|
+
|
|
98
|
+
if not tokens or len(tokens) > 1:
|
|
99
|
+
raise exception.InvalidQueryError(f"Invalid syntax for EXECUTE expression: {stmt.sql(dialect='postgres')}")
|
|
100
|
+
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_replace_supported(stmt: exp.Command, dialect: str) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Check if a REPLACE statement is supported.
|
|
107
|
+
Transform to INSERT INTO to verify syntax.
|
|
108
|
+
"""
|
|
109
|
+
expression = stmt.args.get("expression")
|
|
110
|
+
new_sql = f"INSERT {expression.this}" if expression else "INSERT"
|
|
111
|
+
try:
|
|
112
|
+
sqlglot.parse_one(new_sql, dialect=dialect)
|
|
113
|
+
return True
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.warning(f"Invalid REPLACE statement syntax: {e}")
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _classify_command(stmt: exp.Command, dialect: str) -> tuple[str, bool]:
|
|
120
|
+
"""
|
|
121
|
+
Classify an exp.Command node into a (kind, is_supported) pair.
|
|
122
|
+
kind='' means the command is not recognised and should be treated as unsupported.
|
|
123
|
+
"""
|
|
124
|
+
if dialect in ["athena", "redshift"] and stmt.name == "UNLOAD":
|
|
125
|
+
return "unload", True
|
|
126
|
+
if dialect == "postgres" and stmt.name == "PREPARE":
|
|
127
|
+
return "prepare", _is_prepare_supported(stmt)
|
|
128
|
+
if dialect == "postgres" and stmt.name == "EXECUTE":
|
|
129
|
+
return "execute", _is_execute_supported(stmt)
|
|
130
|
+
if dialect == "mysql" and stmt.name == "REPLACE":
|
|
131
|
+
return "replace", _is_replace_supported(stmt, dialect)
|
|
132
|
+
if stmt.this.upper() == "CALL":
|
|
133
|
+
return "call", True
|
|
134
|
+
return "", False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def collect_queries(text: str, dialect: str, object_mapping: mappings.ObjectMapping | None = None) -> CollectQueryResult:
|
|
138
|
+
"""
|
|
139
|
+
Parse a series of SQL statements provided as text.
|
|
140
|
+
This includes tables, views, procedures, functions, sequences, etc.
|
|
141
|
+
|
|
142
|
+
Each query may contain multiple child queries. For example, a stored procedure often
|
|
143
|
+
has multiple individual queries. Each of these individual queries may also have
|
|
144
|
+
subqueries. For example, a MERGE query often has INSERTs or UPDATEs in its WHEN clauses.
|
|
145
|
+
|
|
146
|
+
The statements must be provided in the order in which they depend on each other.
|
|
147
|
+
If B depends on A, A must be created before B.
|
|
148
|
+
"""
|
|
149
|
+
queries = {}
|
|
150
|
+
unknown = {}
|
|
151
|
+
unsupported = []
|
|
152
|
+
counts: Counter[str] = Counter()
|
|
153
|
+
|
|
154
|
+
if not object_mapping:
|
|
155
|
+
object_mapping = mappings.ObjectMapping(dialect=dialect)
|
|
156
|
+
|
|
157
|
+
# Parse the statements
|
|
158
|
+
parsed = sqlglot.parse(text, dialect=dialect)
|
|
159
|
+
parsed = _split_combined_statements(parsed)
|
|
160
|
+
|
|
161
|
+
for index, stmt in enumerate(parsed):
|
|
162
|
+
if not stmt:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
kind = ""
|
|
166
|
+
if isinstance(stmt, exp.Command):
|
|
167
|
+
kind, supported = _classify_command(stmt, dialect)
|
|
168
|
+
if not kind:
|
|
169
|
+
logger.warning(f"Unsupported statement: {stmt.sql(dialect=dialect)}")
|
|
170
|
+
unsupported.append((index, stmt))
|
|
171
|
+
continue
|
|
172
|
+
if not supported:
|
|
173
|
+
unsupported.append((index, stmt))
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Remove duplicate queries
|
|
177
|
+
sql_text = stmt.sql(dialect=dialect)
|
|
178
|
+
_id = util.short_sha256_hash(sql_text)
|
|
179
|
+
if _id in queries:
|
|
180
|
+
logger.debug(f"Skipping duplicate query: {sql_text}")
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if not kind:
|
|
184
|
+
stmt, kind = _determine_query_kind(stmt, dialect)
|
|
185
|
+
|
|
186
|
+
if kind not in _QUERY_PROCESSORS:
|
|
187
|
+
unknown[kind] = unknown[kind] + 1 if kind in unknown else 1
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Convert the statement to uppercase if the dialect supports it
|
|
191
|
+
stmt = normalize_identifiers(stmt, dialect=dialect, store_original_column_identifiers=True)
|
|
192
|
+
_unnest_values_inside_select(stmt, dialect=dialect)
|
|
193
|
+
|
|
194
|
+
# Substitute any Snowflake session variables ($var) in non-SET statements
|
|
195
|
+
if kind != "set" and object_mapping.session_variables:
|
|
196
|
+
stmt = substitute.substitute_session_variables(stmt, object_mapping.session_variables)
|
|
197
|
+
|
|
198
|
+
query: t.Optional[Q] = _QUERY_PROCESSORS[kind](
|
|
199
|
+
statement=stmt, dialect=dialect, object_mapping=object_mapping, statement_index=index
|
|
200
|
+
)
|
|
201
|
+
if query:
|
|
202
|
+
holder = QueryHolder(original=query)
|
|
203
|
+
_collect_query_children(query, holder, dialect, object_mapping)
|
|
204
|
+
queries[_id] = holder
|
|
205
|
+
counts[kind] += 1
|
|
206
|
+
|
|
207
|
+
logger.debug("Found statements: %s", dict(+counts))
|
|
208
|
+
if unknown:
|
|
209
|
+
logger.warning("Unknown statements: %s", dict(unknown.items()))
|
|
210
|
+
if unsupported:
|
|
211
|
+
logger.warning("Unsupported statements: %s", len(unsupported))
|
|
212
|
+
|
|
213
|
+
return CollectQueryResult(queries=list(queries.values()), unknown=unknown, unsupported=unsupported, object_mapping=object_mapping)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _determine_query_kind(statement: exp.Expr, dialect: str) -> t.Tuple[exp.Expr, str]:
|
|
217
|
+
"""
|
|
218
|
+
Determine a query's "kind" from the expression, which maps to how it will be processed.
|
|
219
|
+
"""
|
|
220
|
+
if statement.key == "create" and isinstance(statement, exp.Create):
|
|
221
|
+
if statement.kind == "TABLE":
|
|
222
|
+
if isinstance(statement.expression, (exp.Select, exp.Values)) or statement.find(exp.ExecuteAsProperty):
|
|
223
|
+
kind = "ctas"
|
|
224
|
+
else:
|
|
225
|
+
kind = "table"
|
|
226
|
+
else:
|
|
227
|
+
kind = (statement.kind or "").lower()
|
|
228
|
+
elif statement.key == "select" and "into" in statement.args:
|
|
229
|
+
# sqlglot rewrites 'SELECT INTO' to 'CREATE TABLE AS' during parse()
|
|
230
|
+
# but it's not shown until we produce it with sql(), so we re-parse it
|
|
231
|
+
if dialect in ["redshift", "postgres"]:
|
|
232
|
+
statement = sqlglot.parse_one(statement.sql(dialect=""), dialect=dialect)
|
|
233
|
+
kind = "ctas"
|
|
234
|
+
else:
|
|
235
|
+
message = f"Expression 'SELECT INTO' has not been implemented yet for dialect: {dialect}"
|
|
236
|
+
raise exception.UnsupportedFeatureError(message=message)
|
|
237
|
+
else:
|
|
238
|
+
kind = statement.key.lower()
|
|
239
|
+
|
|
240
|
+
return statement, kind
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _collect_writable_cte_queries(
|
|
244
|
+
parent_query: Q, parent_holder: QueryHolder, dialect: str, object_mapping: mappings.ObjectMapping
|
|
245
|
+
):
|
|
246
|
+
"""
|
|
247
|
+
Collect any writable (DML) CTEs and attach them as downstream holders to the parent query.
|
|
248
|
+
|
|
249
|
+
If this query is of the form:
|
|
250
|
+
WITH cte AS (
|
|
251
|
+
INSERT ... RETURNING ...
|
|
252
|
+
)
|
|
253
|
+
INSERT INTO ...
|
|
254
|
+
|
|
255
|
+
then the outer and inner queries form a parent-child relationship.
|
|
256
|
+
The inner query is left as-is and copied, while the outer query transformer its
|
|
257
|
+
inner query's SELECT columns with the RETURNING columns. This is so that
|
|
258
|
+
the lineage functions collect the right columns during expression traversal.
|
|
259
|
+
The two queries are processed independently later.
|
|
260
|
+
"""
|
|
261
|
+
for i, cte in enumerate(parent_query.get_ctes()):
|
|
262
|
+
cte_expr = cte.this
|
|
263
|
+
|
|
264
|
+
if isinstance(cte_expr, exp.Select):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
query = _process_unnamed(cte_expr, dialect, object_mapping, i)
|
|
268
|
+
if not query:
|
|
269
|
+
logger.warning(f"Skipping unsupported query type in CTE: {type(cte_expr)}")
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
# Detach the query in the AST so that certain transformations work later
|
|
273
|
+
downstream_holder = QueryHolder(original=query)
|
|
274
|
+
parent_holder.add_downstream_holder(downstream_holder)
|
|
275
|
+
_collect_query_children(query, downstream_holder, dialect, object_mapping)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _collect_call_substitutions(
|
|
279
|
+
query: Q,
|
|
280
|
+
holder: QueryHolder,
|
|
281
|
+
dialect: str,
|
|
282
|
+
object_mapping: mappings.ObjectMapping,
|
|
283
|
+
) -> None:
|
|
284
|
+
"""
|
|
285
|
+
Performs argument substitution for CallQuery, ExecuteQuery, CTASQuery (EXECUTE AS),
|
|
286
|
+
and any query containing UDF call sites.
|
|
287
|
+
The resulting inner statements are classified as Queries and attached as downstream holders.
|
|
288
|
+
"""
|
|
289
|
+
subst_statements: t.List[exp.Expr] = []
|
|
290
|
+
|
|
291
|
+
if isinstance(query, CallQuery):
|
|
292
|
+
subst_statements = substitute.substitute_call(query=query)
|
|
293
|
+
elif isinstance(query, ExecuteQuery):
|
|
294
|
+
subst_statements = substitute.substitute_execute(query=query)
|
|
295
|
+
elif isinstance(query, CTASQuery) and query.source_info.type == typing.SqlObjectType.PREPARED_STATEMENT:
|
|
296
|
+
subst_statements = [substitute.substitute_create_execute(query=query)]
|
|
297
|
+
else:
|
|
298
|
+
expression = util.copy_expression(query.statement)
|
|
299
|
+
while True:
|
|
300
|
+
node, matched_udf = udf.find_next_udf_call(expression, query.object_mapping)
|
|
301
|
+
if not node:
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
inner_exprs = udf.substitute_udf(node, matched_udf)
|
|
305
|
+
subst_statements.extend(inner_exprs)
|
|
306
|
+
# Replace the processed call site with a placeholder so find_next_udf_call()
|
|
307
|
+
# no longer matches it on the next iteration, allowing the loop to advance
|
|
308
|
+
# to the next UDF call. The statement is a working copy and is not used
|
|
309
|
+
# after this function; only the extracted subst_statements matter.
|
|
310
|
+
node.replace(exp.Null())
|
|
311
|
+
|
|
312
|
+
parent_index = query.get_statement_index()
|
|
313
|
+
for i, stmt in enumerate(subst_statements):
|
|
314
|
+
composite_index = f"{parent_index}:{i}"
|
|
315
|
+
substituted_query = _process_unnamed(
|
|
316
|
+
statement=stmt,
|
|
317
|
+
dialect=dialect,
|
|
318
|
+
object_mapping=object_mapping,
|
|
319
|
+
statement_index=composite_index,
|
|
320
|
+
)
|
|
321
|
+
if substituted_query:
|
|
322
|
+
downstream_holder = QueryHolder(original=substituted_query)
|
|
323
|
+
holder.add_downstream_holder(downstream_holder)
|
|
324
|
+
# Recurse: the substituted child may itself contain calls/UDFs
|
|
325
|
+
_collect_query_children(substituted_query, downstream_holder, dialect, object_mapping)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _collect_query_children(query: Q, parent_holder: QueryHolder, dialect: str, object_mapping: mappings.ObjectMapping):
|
|
329
|
+
"""
|
|
330
|
+
Collect any nested child queries for a given query and attach them to the holder.
|
|
331
|
+
"""
|
|
332
|
+
if isinstance(query, InsertQuery):
|
|
333
|
+
_collect_insert_children(query, parent_holder, object_mapping)
|
|
334
|
+
elif isinstance(query, MergeQuery):
|
|
335
|
+
_collect_merge_children(query, parent_holder, object_mapping)
|
|
336
|
+
elif isinstance(query, MultitableInsertQuery):
|
|
337
|
+
_collect_multitable_insert_children(query, parent_holder, object_mapping)
|
|
338
|
+
elif isinstance(query, UserDefinedFunctionQuery):
|
|
339
|
+
_collect_udf_children(query, parent_holder, dialect, object_mapping)
|
|
340
|
+
|
|
341
|
+
# Always check for writable CTEs regardless of query type above
|
|
342
|
+
if not isinstance(query, (CopyQuery, PutQuery)):
|
|
343
|
+
_collect_writable_cte_queries(query, parent_holder, dialect, object_mapping)
|
|
344
|
+
|
|
345
|
+
# Perform call-site substitution immediately at collection time
|
|
346
|
+
_collect_call_substitutions(query, parent_holder, dialect, object_mapping)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _collect_udf_children(
|
|
350
|
+
query: UserDefinedFunctionQuery,
|
|
351
|
+
parent_holder: QueryHolder,
|
|
352
|
+
dialect: str,
|
|
353
|
+
object_mapping: mappings.ObjectMapping,
|
|
354
|
+
):
|
|
355
|
+
"""
|
|
356
|
+
Extract the function body from the UDF and parse it into individual statements.
|
|
357
|
+
Circular references in UDF/procedure definitions could cause infinite recursion.
|
|
358
|
+
"""
|
|
359
|
+
body_expr = query.statement.args.get("expression")
|
|
360
|
+
if not body_expr:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
for i, stmt in enumerate(util.iter_inner_statements(body_expr, dialect)):
|
|
364
|
+
child_query = _process_unnamed(stmt, dialect, object_mapping, i)
|
|
365
|
+
|
|
366
|
+
# If it's a simple select and _process_unnamed skipped it, create a SelectQuery
|
|
367
|
+
# TODO: VALUES and SELECT should be collected
|
|
368
|
+
if not child_query and isinstance(stmt, exp.Select):
|
|
369
|
+
child_query = SelectQuery(expr=stmt, dialect=dialect, object_mapping=object_mapping, statement_index=i)
|
|
370
|
+
|
|
371
|
+
if child_query:
|
|
372
|
+
downstream_holder = QueryHolder(original=child_query)
|
|
373
|
+
parent_holder.add_downstream_holder(downstream_holder)
|
|
374
|
+
_collect_query_children(child_query, downstream_holder, dialect, object_mapping)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _collect_insert_children(query: InsertQuery, parent_holder: QueryHolder, object_mapping: mappings.ObjectMapping):
|
|
378
|
+
"""
|
|
379
|
+
Collect any additional queries inside an INSERT. For Postgres, this is 'INSERT .. ON CONFLICT DO UPDATE'.
|
|
380
|
+
"""
|
|
381
|
+
if not isinstance(query.statement, exp.Insert):
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
on_conflict = query.statement.args.get("conflict")
|
|
385
|
+
|
|
386
|
+
if not isinstance(on_conflict, exp.OnConflict) or on_conflict.args["action"].name == "DO NOTHING":
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
update_query = UpdateQuery(
|
|
390
|
+
expr=on_conflict,
|
|
391
|
+
dialect=query.dialect,
|
|
392
|
+
object_mapping=object_mapping,
|
|
393
|
+
statement_index=0,
|
|
394
|
+
table=query.get_target_as_table(),
|
|
395
|
+
)
|
|
396
|
+
downstream_holder = QueryHolder(original=update_query)
|
|
397
|
+
parent_holder.add_downstream_holder(downstream_holder)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _collect_merge_children(
|
|
401
|
+
parent_query: MergeQuery, parent_holder: QueryHolder, object_mapping: mappings.ObjectMapping
|
|
402
|
+
):
|
|
403
|
+
"""
|
|
404
|
+
Transform any nested statements (INSERT or UPDATE) into fully qualified queries.
|
|
405
|
+
|
|
406
|
+
This is to allow the statements to be processed independently of the parent MERGE query.
|
|
407
|
+
|
|
408
|
+
For example, the merge query:
|
|
409
|
+
|
|
410
|
+
MERGE INTO fruit.processed AS t
|
|
411
|
+
USING fruit.raw AS s
|
|
412
|
+
ON t.kind = s.kind
|
|
413
|
+
WHEN MATCHED THEN
|
|
414
|
+
UPDATE SET name = s.name
|
|
415
|
+
WHEN NOT MATCHED THEN
|
|
416
|
+
INSERT (label) VALUES (s.kind);
|
|
417
|
+
|
|
418
|
+
has 2 nested queries that get transformed into:
|
|
419
|
+
|
|
420
|
+
UPDATE fruit.processed AS t
|
|
421
|
+
SET name = s.name
|
|
422
|
+
FROM fruit.raw AS t
|
|
423
|
+
WHERE t.kind = s.kind
|
|
424
|
+
|
|
425
|
+
INSERT INTO fruit.processed t
|
|
426
|
+
SELECT s.kind as label
|
|
427
|
+
FROM fruit.raw s;
|
|
428
|
+
"""
|
|
429
|
+
merge = parent_query
|
|
430
|
+
parent_expr = parent_query.statement
|
|
431
|
+
whens = [when.args["then"] for when in parent_expr.args["whens"].expressions]
|
|
432
|
+
|
|
433
|
+
for i, when in enumerate(whens):
|
|
434
|
+
# Ensure the full expression tree is kept
|
|
435
|
+
when_expr = util.copy_expression(when)
|
|
436
|
+
|
|
437
|
+
if isinstance(when_expr, exp.Update):
|
|
438
|
+
update_query = UpdateQuery(
|
|
439
|
+
expr=when_expr,
|
|
440
|
+
dialect=parent_query.dialect,
|
|
441
|
+
object_mapping=object_mapping,
|
|
442
|
+
statement_index=i,
|
|
443
|
+
table=merge.get_target_as_table(),
|
|
444
|
+
)
|
|
445
|
+
downstream_holder = QueryHolder(original=update_query)
|
|
446
|
+
parent_holder.add_downstream_holder(downstream_holder)
|
|
447
|
+
|
|
448
|
+
elif isinstance(when_expr, exp.Insert):
|
|
449
|
+
insert_query = InsertQuery(
|
|
450
|
+
expr=when_expr,
|
|
451
|
+
dialect=parent_query.dialect,
|
|
452
|
+
object_mapping=object_mapping,
|
|
453
|
+
statement_index=i,
|
|
454
|
+
table=merge.get_target_as_table(),
|
|
455
|
+
)
|
|
456
|
+
insert_query.target_info = merge.target_info
|
|
457
|
+
downstream_holder = QueryHolder(original=insert_query)
|
|
458
|
+
parent_holder.add_downstream_holder(downstream_holder)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _collect_multitable_insert_children(
|
|
462
|
+
parent_query: MultitableInsertQuery, parent_holder: QueryHolder, object_mapping: mappings.ObjectMapping
|
|
463
|
+
):
|
|
464
|
+
"""
|
|
465
|
+
Extract the ConditionalInsert (exp.Insert) branches from the Multitable statement.
|
|
466
|
+
"""
|
|
467
|
+
statement = parent_query.statement
|
|
468
|
+
for i, branch in enumerate(statement.expressions):
|
|
469
|
+
if not isinstance(branch, exp.ConditionalInsert):
|
|
470
|
+
continue
|
|
471
|
+
|
|
472
|
+
insert_expr = branch.this
|
|
473
|
+
if not isinstance(insert_expr, exp.Insert):
|
|
474
|
+
continue
|
|
475
|
+
|
|
476
|
+
copied_expr = util.copy_expression(insert_expr)
|
|
477
|
+
|
|
478
|
+
if not copied_expr.expression and not copied_expr.args.get("source"):
|
|
479
|
+
# If the branch doesn't have its own source, use the parent's shared source.
|
|
480
|
+
# This happens in Snowflake multi-table inserts without explicit VALUES.
|
|
481
|
+
copied_expr.set("expression", parent_query.source_info.expression)
|
|
482
|
+
|
|
483
|
+
child_query = InsertQuery(
|
|
484
|
+
expr=copied_expr,
|
|
485
|
+
dialect=parent_query.dialect,
|
|
486
|
+
object_mapping=object_mapping,
|
|
487
|
+
statement_index=i,
|
|
488
|
+
)
|
|
489
|
+
downstream_holder = QueryHolder(original=child_query)
|
|
490
|
+
parent_holder.add_downstream_holder(downstream_holder)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
_UNNAMED_TYPE_MAP: dict[type, type] = {
|
|
494
|
+
exp.Insert: InsertQuery,
|
|
495
|
+
exp.Update: UpdateQuery,
|
|
496
|
+
exp.Merge: MergeQuery,
|
|
497
|
+
exp.MultitableInserts: MultitableInsertQuery,
|
|
498
|
+
exp.Delete: DeleteQuery,
|
|
499
|
+
exp.Select: SelectQuery,
|
|
500
|
+
exp.Copy: CopyQuery,
|
|
501
|
+
exp.Values: ValuesQuery,
|
|
502
|
+
exp.Put: PutQuery,
|
|
503
|
+
exp.Set: SetQuery,
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _process_unnamed(
|
|
508
|
+
statement: exp.Expr, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int | str
|
|
509
|
+
) -> Q | None:
|
|
510
|
+
"""
|
|
511
|
+
Process an unnamed statement - one not inside a 'CREATE <name>' statement.
|
|
512
|
+
"""
|
|
513
|
+
query_class = _UNNAMED_TYPE_MAP.get(type(statement))
|
|
514
|
+
if query_class is not None:
|
|
515
|
+
query = query_class(
|
|
516
|
+
expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
|
|
517
|
+
)
|
|
518
|
+
if isinstance(query, DeleteQuery) and not statement.find(exp.Insert, exp.Update, exp.Merge):
|
|
519
|
+
logger.warning(
|
|
520
|
+
"Skipping statement: A DELETE query must have a data-modifying statement, "
|
|
521
|
+
"such as an INSERT, to contain lineage."
|
|
522
|
+
)
|
|
523
|
+
return query
|
|
524
|
+
|
|
525
|
+
if isinstance(statement, exp.Command) and statement.this.upper() == "CALL":
|
|
526
|
+
return CallQuery(
|
|
527
|
+
expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
|
|
528
|
+
)
|
|
529
|
+
if isinstance(statement, exp.Command) and statement.this.upper() == "EXECUTE":
|
|
530
|
+
return ExecuteQuery(
|
|
531
|
+
expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
|
|
532
|
+
)
|
|
533
|
+
if isinstance(statement, exp.Create):
|
|
534
|
+
if statement.kind == "TABLE":
|
|
535
|
+
if isinstance(statement.expression, (exp.Select, exp.Values)) or statement.find(exp.ExecuteAsProperty):
|
|
536
|
+
return _process_views_and_ctas(statement, dialect, object_mapping, statement_index)
|
|
537
|
+
else:
|
|
538
|
+
return _process_tables(statement, dialect, object_mapping, statement_index)
|
|
539
|
+
elif statement.kind == "VIEW":
|
|
540
|
+
return _process_views_and_ctas(statement, dialect, object_mapping, statement_index)
|
|
541
|
+
|
|
542
|
+
return None
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _process_tables(
|
|
546
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int | str
|
|
547
|
+
) -> Q | None:
|
|
548
|
+
"""
|
|
549
|
+
Process a 'CREATE TABLE' statement.
|
|
550
|
+
"""
|
|
551
|
+
query = None
|
|
552
|
+
|
|
553
|
+
if statement.kind == "TABLE":
|
|
554
|
+
# CREATE TABLE ...
|
|
555
|
+
query = TableQuery(
|
|
556
|
+
expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
|
|
557
|
+
)
|
|
558
|
+
query.set_column_defs()
|
|
559
|
+
object_mapping.add_table_query(
|
|
560
|
+
query=query,
|
|
561
|
+
column_mapping=query.get_column_names_with_types(include_system=True),
|
|
562
|
+
)
|
|
563
|
+
elif statement.kind == "SEQUENCE":
|
|
564
|
+
query = SequenceQuery(
|
|
565
|
+
expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
|
|
566
|
+
)
|
|
567
|
+
object_mapping.add_sequence_query(query=query)
|
|
568
|
+
|
|
569
|
+
return query
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _process_views_and_ctas(
|
|
573
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int | str
|
|
574
|
+
) -> Q:
|
|
575
|
+
"""
|
|
576
|
+
Convert a series of `CREATE VIEW/TABLE AS ...` SQL DDL statements into sqlglot's MappingSchema
|
|
577
|
+
to extract the table and column details.
|
|
578
|
+
"""
|
|
579
|
+
util.qualify_and_annotate(statement, dialect, object_mapping, remove_added_aliases=True)
|
|
580
|
+
col_defs = _determine_column_defs(statement, dialect, object_mapping)
|
|
581
|
+
|
|
582
|
+
if statement.kind == "VIEW":
|
|
583
|
+
# CREATE VIEW ...
|
|
584
|
+
query = ViewQuery(
|
|
585
|
+
expr=statement,
|
|
586
|
+
dialect=dialect,
|
|
587
|
+
object_mapping=object_mapping,
|
|
588
|
+
columns=col_defs,
|
|
589
|
+
statement_index=statement_index,
|
|
590
|
+
)
|
|
591
|
+
elif statement.kind == "TABLE":
|
|
592
|
+
# CREATE TABLE AS ...
|
|
593
|
+
query = CTASQuery(
|
|
594
|
+
expr=statement,
|
|
595
|
+
dialect=dialect,
|
|
596
|
+
object_mapping=object_mapping,
|
|
597
|
+
columns=col_defs,
|
|
598
|
+
statement_index=statement_index,
|
|
599
|
+
)
|
|
600
|
+
query.system_column_defs = settings.system_columns(dialect)
|
|
601
|
+
else:
|
|
602
|
+
raise exception.UnsupportedFeatureError(message=f"Unhandled situation for query: {statement.kind}")
|
|
603
|
+
|
|
604
|
+
object_mapping.add_table_query(
|
|
605
|
+
query=query,
|
|
606
|
+
column_mapping=query.get_column_names_with_types(include_system=True),
|
|
607
|
+
)
|
|
608
|
+
return query
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _unnest_values_inside_select(statement: exp.Create, dialect: str):
|
|
612
|
+
"""
|
|
613
|
+
Replace SELECT * FROM (VALUES ()) with VALUES ().
|
|
614
|
+
This prevents sqlglot from assigning its own aliases.
|
|
615
|
+
"""
|
|
616
|
+
# TODO: this should be done in a transform, checked against self.statement
|
|
617
|
+
for values_expr in statement.find_all(exp.Values):
|
|
618
|
+
simplify_row_in_values(values_expr, dialect)
|
|
619
|
+
parent = values_expr.parent_select
|
|
620
|
+
while isinstance(parent, exp.Select) and parent.is_star and parent.parent_select:
|
|
621
|
+
parent = parent.parent_select
|
|
622
|
+
|
|
623
|
+
if parent and parent.parent:
|
|
624
|
+
# sqlglot converts "SELECT UNION VALUES" into "SELECT UNION SELECT * FROM (VALUES ()) AS _values"
|
|
625
|
+
# We undo that transformation (risky, as actual query may have that alias)
|
|
626
|
+
if alias := values_expr.args["alias"]:
|
|
627
|
+
if alias.name != "_values":
|
|
628
|
+
return statement
|
|
629
|
+
alias.pop()
|
|
630
|
+
parent.replace(values_expr)
|
|
631
|
+
|
|
632
|
+
return statement
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _determine_column_defs(
|
|
636
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping
|
|
637
|
+
) -> t.List[ColumnDef]:
|
|
638
|
+
"""
|
|
639
|
+
Look up the columns for 'y' in 'INSERT INTO x TABLE y'
|
|
640
|
+
"""
|
|
641
|
+
if isinstance(statement.expression, exp.Values):
|
|
642
|
+
# CREATE TABLE AS VALUES
|
|
643
|
+
columns = [stmt.name for stmt in statement.this.expressions]
|
|
644
|
+
types = [val.type for val in statement.expression.expressions[0].expressions]
|
|
645
|
+
|
|
646
|
+
if not columns:
|
|
647
|
+
columns = util.default_column_index_iterator(dialect, types)
|
|
648
|
+
|
|
649
|
+
col_defs = [exp.ColumnDef(this=col_name, kind=col_type) for col_name, col_type in zip(columns, types)]
|
|
650
|
+
elif exec_prop := statement.find(exp.ExecuteAsProperty):
|
|
651
|
+
# CREATE TABLE AS EXECUTE
|
|
652
|
+
plan_name = exec_prop.this.name
|
|
653
|
+
plan_table = exp.to_table(plan_name)
|
|
654
|
+
matched_prepare = object_mapping.lookup_prepare_query(plan_table, raise_on_missing=True)
|
|
655
|
+
|
|
656
|
+
source = matched_prepare.statement
|
|
657
|
+
if isinstance(source, exp.Select):
|
|
658
|
+
col_defs = [
|
|
659
|
+
exp.ColumnDef(this=exp.to_identifier(col.alias_or_name), kind=col.unalias().type)
|
|
660
|
+
for col in source.expressions
|
|
661
|
+
]
|
|
662
|
+
else:
|
|
663
|
+
col_defs = []
|
|
664
|
+
else:
|
|
665
|
+
col_defs = [exp.ColumnDef(this=exp.to_identifier(s.alias), kind=s.type) for s in statement.selects]
|
|
666
|
+
|
|
667
|
+
return col_defs
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _process_functions(
|
|
671
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
672
|
+
) -> Q:
|
|
673
|
+
"""
|
|
674
|
+
Process a "CREATE FUNCTION" statement.
|
|
675
|
+
"""
|
|
676
|
+
query = UserDefinedFunctionQuery(
|
|
677
|
+
expr=statement,
|
|
678
|
+
dialect=dialect,
|
|
679
|
+
object_mapping=object_mapping,
|
|
680
|
+
statement_index=statement_index,
|
|
681
|
+
)
|
|
682
|
+
object_mapping.add_udf_query(query, column_mapping=query.get_column_names_with_types(include_system=True))
|
|
683
|
+
|
|
684
|
+
return query
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _process_triggers(
|
|
688
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
689
|
+
) -> Q:
|
|
690
|
+
"""
|
|
691
|
+
Process a "CREATE TRIGGER" statement.
|
|
692
|
+
"""
|
|
693
|
+
query = TriggerQuery(statement, dialect, object_mapping, statement_index)
|
|
694
|
+
object_mapping.add_trigger_query(query)
|
|
695
|
+
return query
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _process_stored_procedures(
|
|
699
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
700
|
+
) -> Q:
|
|
701
|
+
"""
|
|
702
|
+
Process a "CREATE PROCEDURE" statement.
|
|
703
|
+
"""
|
|
704
|
+
query = ProcedureQuery(
|
|
705
|
+
expr=statement, dialect=dialect, object_mapping=object_mapping, statement_index=statement_index
|
|
706
|
+
)
|
|
707
|
+
object_mapping.add_procedure_query(query)
|
|
708
|
+
return query
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _process_stage(
|
|
712
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
713
|
+
) -> Q:
|
|
714
|
+
query = StageQuery(statement, dialect, object_mapping=object_mapping, statement_index=statement_index)
|
|
715
|
+
object_mapping.add_stage_query(query)
|
|
716
|
+
return query
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _process_unload(
|
|
720
|
+
statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
721
|
+
) -> Q:
|
|
722
|
+
query = UnloadQuery(statement, dialect, object_mapping, statement_index)
|
|
723
|
+
return query
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _process_replace(
|
|
727
|
+
statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
728
|
+
) -> Q:
|
|
729
|
+
query = ReplaceQuery(statement, dialect, object_mapping, statement_index)
|
|
730
|
+
return query
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _process_prepare(
|
|
734
|
+
statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
735
|
+
) -> Q:
|
|
736
|
+
query = PrepareQuery(statement, dialect, object_mapping, statement_index)
|
|
737
|
+
object_mapping.add_prepare_query(query)
|
|
738
|
+
return query
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _process_execute(
|
|
742
|
+
statement: exp.Command, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
743
|
+
) -> Q:
|
|
744
|
+
query = ExecuteQuery(statement, dialect, object_mapping, statement_index)
|
|
745
|
+
return query
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _process_type(
|
|
749
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
750
|
+
) -> Q:
|
|
751
|
+
query = TypeQuery(statement, dialect, object_mapping, statement_index)
|
|
752
|
+
object_mapping.add_type_query(query)
|
|
753
|
+
return query
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _process_schema(
|
|
757
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
758
|
+
) -> Q:
|
|
759
|
+
query = SchemaQuery(statement, dialect, object_mapping, statement_index)
|
|
760
|
+
object_mapping.add_schema_query(query)
|
|
761
|
+
return query
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _process_database(
|
|
765
|
+
statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
766
|
+
) -> Q:
|
|
767
|
+
query = DatabaseQuery(statement, dialect, object_mapping, statement_index)
|
|
768
|
+
object_mapping.add_database_query(query)
|
|
769
|
+
return query
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
_QUERY_PROCESSORS: dict[str, t.Callable] = {
|
|
773
|
+
"table": _process_tables,
|
|
774
|
+
"ctas": _process_views_and_ctas,
|
|
775
|
+
"view": _process_views_and_ctas,
|
|
776
|
+
"sequence": _process_tables,
|
|
777
|
+
"procedure": _process_stored_procedures,
|
|
778
|
+
"multitableinserts": _process_unnamed,
|
|
779
|
+
"function": _process_functions,
|
|
780
|
+
"database": _process_database,
|
|
781
|
+
"trigger": _process_triggers,
|
|
782
|
+
"select": _process_unnamed,
|
|
783
|
+
"insert": _process_unnamed,
|
|
784
|
+
"update": _process_unnamed,
|
|
785
|
+
"merge": _process_unnamed,
|
|
786
|
+
"delete": _process_unnamed,
|
|
787
|
+
"schema": _process_schema,
|
|
788
|
+
"unload": _process_unload,
|
|
789
|
+
"replace": _process_replace,
|
|
790
|
+
"prepare": _process_prepare,
|
|
791
|
+
"execute": _process_execute,
|
|
792
|
+
"stage": _process_stage,
|
|
793
|
+
"call": _process_unnamed,
|
|
794
|
+
"copy": _process_unnamed,
|
|
795
|
+
"put": _process_unnamed,
|
|
796
|
+
"set": _process_unnamed,
|
|
797
|
+
"values": _process_unnamed,
|
|
798
|
+
"type": _process_type,
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _split_combined_statements(parsed: t.List[exp.Expr]) -> t.List[exp.Expr]:
|
|
803
|
+
"""
|
|
804
|
+
Workaround for sqlglot incorrectly bundling statements following a block-based CREATE.
|
|
805
|
+
For example, with the statement "CREATE PROCEDURE .. BEGIN .. END; CALL();" the CALL()
|
|
806
|
+
is included with the procedure definition.
|
|
807
|
+
"""
|
|
808
|
+
new_parsed = []
|
|
809
|
+
for stmt in parsed:
|
|
810
|
+
if isinstance(stmt, exp.Create) and isinstance(stmt.args.get("expression"), exp.Block):
|
|
811
|
+
block = stmt.args["expression"]
|
|
812
|
+
end_index = -1
|
|
813
|
+
for i, expr in enumerate(block.expressions):
|
|
814
|
+
if isinstance(expr, exp.EndStatement):
|
|
815
|
+
end_index = i
|
|
816
|
+
break
|
|
817
|
+
|
|
818
|
+
# Separate the statements
|
|
819
|
+
if end_index != -1 and end_index < len(block.expressions) - 1:
|
|
820
|
+
extra = block.expressions[end_index + 1 :]
|
|
821
|
+
block.set("expressions", block.expressions[: end_index + 1])
|
|
822
|
+
new_parsed.append(stmt)
|
|
823
|
+
new_parsed.extend(extra)
|
|
824
|
+
continue
|
|
825
|
+
|
|
826
|
+
new_parsed.append(stmt)
|
|
827
|
+
return new_parsed
|