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,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CopyTransformer — handles COPY statement transformations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf.processors.transformer.base import BaseQueryTransformer
|
|
8
|
+
from sqlleaf.util import expression as util
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CopyTransformer(BaseQueryTransformer):
|
|
12
|
+
"""Transformer for COPY statements."""
|
|
13
|
+
|
|
14
|
+
def transform(self, statement: exp.Insert) -> exp.Insert:
|
|
15
|
+
return self._convert_copy_to_insert()
|
|
16
|
+
|
|
17
|
+
@BaseQueryTransformer._validate_syntax
|
|
18
|
+
def _convert_copy_to_insert(self) -> exp.Insert:
|
|
19
|
+
"""
|
|
20
|
+
Convert the COPY statement into an INSERT statement.
|
|
21
|
+
|
|
22
|
+
COPY INTO <table> FROM @stage
|
|
23
|
+
-> INSERT INTO <table> SELECT * FROM @stage
|
|
24
|
+
=> produces lineage: @stage -> N table columns
|
|
25
|
+
COPY INTO @stage FROM <table>
|
|
26
|
+
-> INSERT INTO @stage SELECT * FROM <table>
|
|
27
|
+
=> produces lineage: N table columns -> @stage
|
|
28
|
+
"""
|
|
29
|
+
query = self.query
|
|
30
|
+
dialect = query.dialect
|
|
31
|
+
|
|
32
|
+
target_columns = query.get_columns_from_target()
|
|
33
|
+
column_names = [col.name for col in target_columns]
|
|
34
|
+
|
|
35
|
+
# Transform to a SELECT
|
|
36
|
+
src = query.source_info.expression
|
|
37
|
+
if isinstance(src, exp.Select):
|
|
38
|
+
select = src
|
|
39
|
+
else:
|
|
40
|
+
# Add the column names from the target object
|
|
41
|
+
columns = [util.column_def_to_column(c.copy()) for c in target_columns]
|
|
42
|
+
for c in columns:
|
|
43
|
+
c.set("catalog", "")
|
|
44
|
+
c.set("schema", "")
|
|
45
|
+
c.set("table", "")
|
|
46
|
+
|
|
47
|
+
# Only use the named columns if provided
|
|
48
|
+
if isinstance(query.statement.this, exp.Schema):
|
|
49
|
+
# COPY (name, age) ...
|
|
50
|
+
named_columns = [s.name for s in query.statement.this.expressions]
|
|
51
|
+
columns = [col for col in columns if col.name in named_columns]
|
|
52
|
+
column_names = named_columns
|
|
53
|
+
|
|
54
|
+
select = exp.select(*columns, dialect=dialect).from_(src)
|
|
55
|
+
|
|
56
|
+
# Convert the Copy to an Insert
|
|
57
|
+
insert_expr = exp.insert(
|
|
58
|
+
expression=select,
|
|
59
|
+
into=query.target_info.expression, # ty: ignore[invalid-argument-type]
|
|
60
|
+
columns=column_names,
|
|
61
|
+
dialect=dialect,
|
|
62
|
+
)
|
|
63
|
+
return insert_expr
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CTASTransformer — handles CREATE TABLE AS SELECT (CTAS) statement transformations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf.processors.transformer.base import BaseQueryTransformer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CTASTransformer(BaseQueryTransformer):
|
|
11
|
+
"""Transformer for CTAS (CREATE TABLE AS) statements."""
|
|
12
|
+
|
|
13
|
+
def transform(self, statement: exp.Create) -> exp.Create:
|
|
14
|
+
# Note: VALUES->SELECT conversion is already performed by preprocess() (via
|
|
15
|
+
# BaseQueryTransformer.normalize_all_values); statement.expression is already
|
|
16
|
+
# clean of any exp.Values nodes.
|
|
17
|
+
if statement.expression:
|
|
18
|
+
statement = self._convert_substituted_table_to_select(statement)
|
|
19
|
+
return statement
|
|
20
|
+
|
|
21
|
+
def _convert_substituted_table_to_select(self, statement: exp.Create) -> exp.Create:
|
|
22
|
+
"""
|
|
23
|
+
Convert "CREATE TABLE w AS TABLE x" to "CREATE TABLE w AS SELECT * FROM x".
|
|
24
|
+
|
|
25
|
+
The former statement is not actually valid SQL; it only appears after the substitution functions have
|
|
26
|
+
run over an EXECUTE, i.e.:
|
|
27
|
+
PREPARE stmt AS TABLE x;
|
|
28
|
+
CREATE TABLE w AS EXECUTE stmt;
|
|
29
|
+
->
|
|
30
|
+
CREATE TABLE w AS TABLE x;
|
|
31
|
+
->
|
|
32
|
+
CREATE TABLE w AS SELECT * FROM x;
|
|
33
|
+
This is handled separately from _convert_table_as_select(), which handles all other TABLE cases.
|
|
34
|
+
"""
|
|
35
|
+
expr = statement.expression
|
|
36
|
+
if self.query.dialect == "postgres" and isinstance(expr, exp.Alias):
|
|
37
|
+
if expr.this.name.upper() == "TABLE":
|
|
38
|
+
expr.replace(exp.select("*").from_(expr.alias))
|
|
39
|
+
return statement
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeleteTransformer — handles DELETE statement transformations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf.processors.transformer.base import BaseQueryTransformer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeleteTransformer(BaseQueryTransformer):
|
|
11
|
+
"""Transformer for DELETE statements."""
|
|
12
|
+
|
|
13
|
+
def transform(self, statement: exp.Delete) -> exp.Delete:
|
|
14
|
+
return self._process_inner_ctes(statement)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from sqlleaf.processors.transformer.expressions.row import (
|
|
2
|
+
add_parens_for_composite_field_access as add_parens_for_composite_field_access,
|
|
3
|
+
simplify_row as simplify_row,
|
|
4
|
+
)
|
|
5
|
+
from sqlleaf.processors.transformer.expressions.values import (
|
|
6
|
+
normalize_values as normalize_values,
|
|
7
|
+
_rewrite_values_statement as _rewrite_values_statement,
|
|
8
|
+
)
|
|
9
|
+
from sqlleaf.processors.transformer.expressions.functional_notation import (
|
|
10
|
+
rewrite_functional_notation_columns as rewrite_functional_notation_columns,
|
|
11
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def rewrite_functional_notation_columns(statement: exp.Expr) -> exp.Expr:
|
|
7
|
+
"""
|
|
8
|
+
Rewrite Postgres functional-notation column references from exp.TableColumn to exp.Column.
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
SELECT name(source) FROM source -> SELECT source.name FROM source
|
|
12
|
+
SELECT name(s) AS n FROM source s -> SELECT s.name AS n FROM source s
|
|
13
|
+
"""
|
|
14
|
+
# Convert Anonymous(name)(TableColumn(this=<table ident>)) -> Column(table.name)
|
|
15
|
+
for anon in statement.find_all(exp.Anonymous):
|
|
16
|
+
if len(args := anon.expressions) == 1:
|
|
17
|
+
arg = args[0]
|
|
18
|
+
if isinstance(arg, exp.TableColumn):
|
|
19
|
+
new_col = exp.Column(this=anon.this, table=arg.this)
|
|
20
|
+
anon.replace(new_col)
|
|
21
|
+
|
|
22
|
+
return statement
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
from sqlglot import exp
|
|
5
|
+
|
|
6
|
+
from sqlleaf import util, exception
|
|
7
|
+
from sqlleaf.models.query import Q, UserDefinedFunctionQuery
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("sqlleaf")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parens_for_composite_field_access(statement: exp.Expr) -> exp.Expr:
|
|
13
|
+
"""
|
|
14
|
+
Ensure parentheses surround composite field dereference on expressions used with dot notation.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
CAST(ROW(2) AS people).age -> (CAST(ROW(2) AS people)).age
|
|
18
|
+
"""
|
|
19
|
+
for dot in statement.find_all(exp.Dot):
|
|
20
|
+
left = dot.this
|
|
21
|
+
if isinstance(left, exp.Cast) and util.is_row_function(left.this):
|
|
22
|
+
dot.set("this", exp.Paren(this=left.copy()))
|
|
23
|
+
return statement
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def simplify_row_in_values(values_expr: exp.Values, dialect: str) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Simplify MySQL's VALUES ROW(...) syntax by flattening the ROW() function.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
VALUES (ROW(1, 2)), (ROW(3, 4)) -> VALUES (1, 2), (3, 4)
|
|
32
|
+
"""
|
|
33
|
+
if dialect == "mysql":
|
|
34
|
+
for tuple_node in values_expr.expressions:
|
|
35
|
+
if isinstance(tuple_node, exp.Tuple) and len(tuple_node.expressions) == 1:
|
|
36
|
+
inner = tuple_node.expressions[0]
|
|
37
|
+
if util.is_row_function(inner):
|
|
38
|
+
tuple_node.set("expressions", [e.copy() for e in inner.expressions])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def simplify_row(statement: exp.Expr, query: Q) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Simplify occurrences of the ROW() function.
|
|
44
|
+
|
|
45
|
+
ROW() is used to group multiple values into a single composite value. Its default output is an anonymous record.
|
|
46
|
+
It can often be simplified depending on where it appears in a query. We focus on:
|
|
47
|
+
- field access: `(ROW(e1, e2, e3)::my_type).field_n` -> `en AS field_n`
|
|
48
|
+
- star access: `(ROW(e1, e2, e3)::my_type).*` -> `e1 AS a, e2 AS b, e3 AS c`
|
|
49
|
+
|
|
50
|
+
Transformation is skipped when the ROW() cast is inside functions like UNNEST/ARRAY.
|
|
51
|
+
"""
|
|
52
|
+
if row_nodes := _collect_row_functions(statement):
|
|
53
|
+
_expand_row_cast_aliases(row_nodes, statement, query)
|
|
54
|
+
_simplify_field_access(row_nodes, statement, query)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _collect_row_functions(statement: exp.Expr) -> t.Set[exp.Anonymous]:
|
|
58
|
+
"""
|
|
59
|
+
Collect all ROW() expressions from the statement.
|
|
60
|
+
"""
|
|
61
|
+
return {node for node in statement.find_all(exp.Anonymous) if node.name.upper() == "ROW"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _expand_row_cast_aliases(row_nodes: t.Set[exp.Anonymous], statement: exp.Expr, query: Q) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Expand ROWs with casts into lists of columns, as well as expressions of the form '(alias_name).field'.
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
`SELECT ROW(a, b)::my_type AS r` -> `SELECT a AS a, b AS b`
|
|
70
|
+
`SELECT (r).a FROM (SELECT ROW(a, b)::my_type AS r) AS sub` -> `SELECT sub.a AS a FROM (SELECT a AS a, b AS b) AS sub`
|
|
71
|
+
"""
|
|
72
|
+
for row_node in row_nodes:
|
|
73
|
+
cast_node = row_node.parent
|
|
74
|
+
if not isinstance(cast_node, exp.Cast):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
alias_node = cast_node.parent
|
|
78
|
+
if not isinstance(alias_node, exp.Alias):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if _is_inside_unnest(alias_node):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
if not _is_direct_select_expression(alias_node):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
to_type = cast_node.to.find(exp.Identifier)
|
|
88
|
+
if to_type is None:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
fields = _lookup_type_fields(to_type.name, query)
|
|
92
|
+
if not fields:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
row_exprs = cast_node.this.expressions
|
|
96
|
+
|
|
97
|
+
_rewrite_field_references(alias_node, row_exprs, fields, statement)
|
|
98
|
+
_expand_to_select_list(alias_node, row_exprs, fields)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _simplify_field_access(row_nodes: t.Set[exp.Anonymous], statement: exp.Expr, query: Q) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Simplify field access patterns on ROW casts.
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
`(ROW(a, b, c)::my_type).a` -> `a`
|
|
107
|
+
`(ROW(a, b, c)::my_type).*` -> `a AS a, b AS b, c AS c`
|
|
108
|
+
`(CASE WHEN x THEN ROW(a, b)::t ELSE ROW(0, 0)::t END).a` -> `CASE WHEN x THEN a ELSE 0 END`
|
|
109
|
+
"""
|
|
110
|
+
for dot in statement.find_all(exp.Dot):
|
|
111
|
+
if dot.parent is None and dot is not statement:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
left = dot.this.unnest()
|
|
115
|
+
right = dot.expression
|
|
116
|
+
|
|
117
|
+
cast_node = _get_row_cast(left, row_nodes)
|
|
118
|
+
if cast_node is not None:
|
|
119
|
+
_simplify_field_of_row(dot, cast_node, right, query)
|
|
120
|
+
elif left in row_nodes and isinstance(right, exp.Star):
|
|
121
|
+
_simplify_star_of_uncast_row(dot, left)
|
|
122
|
+
elif isinstance(left, exp.Case) and isinstance(right, exp.Identifier):
|
|
123
|
+
_simplify_field_of_case_row(dot, left, right, query, row_nodes)
|
|
124
|
+
elif isinstance(left, exp.Column) and isinstance(right, exp.Identifier):
|
|
125
|
+
_simplify_column_field_access(dot, left, right)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _simplify_star_of_uncast_row(dot: exp.Dot, row_node: exp.Anonymous) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Handle (ROW(e1, e2, e3)).* pattern without a typecast.
|
|
131
|
+
|
|
132
|
+
In Postgres, the default aliases for ROW() fields accessed via .* are f1, f2, f3, ...
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
`(ROW('Alice'::text, 25, 10.0)).*` -> `'Alice'::text AS f1, 25 AS f2, 10.0 AS f3`
|
|
136
|
+
"""
|
|
137
|
+
aliases = [f"f{i + 1}" for i in range(len(row_node.expressions))]
|
|
138
|
+
_expand_to_select_list(dot, row_node.expressions, aliases)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _simplify_column_field_access(dot: exp.Dot, column: exp.Column, field: exp.Identifier) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Rewrite `(col).field` into a plain `col.field` Column expression.
|
|
144
|
+
|
|
145
|
+
In Postgres, `(u).a` is composite field access on a column alias; the parentheses
|
|
146
|
+
are syntactic and should be removed before qualification so that the optimizer
|
|
147
|
+
does not double-qualify the table name (e.g. `u.u.a`).
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
`(u).a` -> `u.a`
|
|
151
|
+
`(sub.r).b` -> `sub.r.b` (handled by sqlglot as Dot)
|
|
152
|
+
"""
|
|
153
|
+
table = column.args.get("table")
|
|
154
|
+
if table is not None:
|
|
155
|
+
# col already has a table qualifier: keep as Dot(column, field)
|
|
156
|
+
new_col = exp.Dot(this=column.copy(), expression=field.copy())
|
|
157
|
+
else:
|
|
158
|
+
new_col = exp.column(field.name, table=column.name)
|
|
159
|
+
dot.replace(new_col)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _resolve_field_index(cast_node: exp.Cast, field_name: str, query: Q) -> t.Optional[t.Tuple[t.List[str], int]]:
|
|
163
|
+
"""
|
|
164
|
+
Look up the field index for `field_name` in the composite type of `cast_node`.
|
|
165
|
+
Returns (fields, index) or None if the type or field is unknown.
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
Given `ROW(a, b)::my_type` where `my_type` has fields `(x, y)`:
|
|
169
|
+
`_resolve_field_index(cast_node, "y", query)` -> `(["x", "y"], 1)`
|
|
170
|
+
`_resolve_field_index(cast_node, "z", query)` -> `None`
|
|
171
|
+
"""
|
|
172
|
+
type_id = cast_node.to.find(exp.Identifier)
|
|
173
|
+
if type_id is None:
|
|
174
|
+
return None
|
|
175
|
+
fields = _lookup_type_fields(type_id.name, query)
|
|
176
|
+
if not fields or field_name not in fields:
|
|
177
|
+
return None
|
|
178
|
+
return fields, fields.index(field_name)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _simplify_field_of_row(dot: exp.Dot, cast_node: exp.Cast, right: exp.Expr, query: Q) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Handle (ROW(...)::my_type).field or (ROW(...)::my_type).* patterns.
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
`(ROW(a, b, c)::my_type).b` -> `b`
|
|
187
|
+
`(ROW(a, b, c)::my_type).*` -> `a AS a, b AS b, c AS c`
|
|
188
|
+
"""
|
|
189
|
+
if isinstance(right, exp.Star):
|
|
190
|
+
type_id = cast_node.to.find(exp.Identifier)
|
|
191
|
+
if type_id is None:
|
|
192
|
+
return
|
|
193
|
+
fields = _lookup_type_fields(type_id.name, query)
|
|
194
|
+
if fields:
|
|
195
|
+
_expand_to_select_list(dot, cast_node.this.expressions, fields)
|
|
196
|
+
else:
|
|
197
|
+
if resolved := _resolve_field_index(cast_node, right.name, query):
|
|
198
|
+
_, idx = resolved
|
|
199
|
+
dot.replace(_extract_field(cast_node, idx))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _simplify_field_of_case_row(
|
|
203
|
+
dot: exp.Dot,
|
|
204
|
+
case_node: exp.Case,
|
|
205
|
+
right: exp.Identifier,
|
|
206
|
+
query: Q,
|
|
207
|
+
row_node_set: t.Set[exp.Anonymous],
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Simplify CASE statements when a field selector is used on its result.
|
|
210
|
+
|
|
211
|
+
Examples:
|
|
212
|
+
`(CASE WHEN x > 1 THEN ROW(a, b)::t ELSE ROW(0, '')::t END).a` -> `CASE WHEN x > 1 THEN a ELSE 0 END`
|
|
213
|
+
"""
|
|
214
|
+
branch_casts: t.List[exp.Cast] = []
|
|
215
|
+
for if_node in case_node.args.get("ifs", []):
|
|
216
|
+
c = _get_row_cast(if_node.args.get("true").unnest(), row_node_set)
|
|
217
|
+
if c is None:
|
|
218
|
+
return
|
|
219
|
+
branch_casts.append(c)
|
|
220
|
+
|
|
221
|
+
default_cast = _get_row_cast(case_node.args.get("default").unnest(), row_node_set)
|
|
222
|
+
if not branch_casts or default_cast is None:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if _is_inside_unnest(dot):
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if resolved := _resolve_field_index(branch_casts[0], right.name, query):
|
|
229
|
+
_, field_index = resolved
|
|
230
|
+
dot.replace(_replace_row_casts_in_case(case_node, field_index))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_direct_select_expression(node: exp.Expr) -> bool:
|
|
234
|
+
"""Check if a node is a direct child of a SELECT expressions list.
|
|
235
|
+
|
|
236
|
+
`SELECT <node>, ...` -> True
|
|
237
|
+
`SELECT func(<node>)` -> False
|
|
238
|
+
"""
|
|
239
|
+
return isinstance(node.parent, exp.Select) and node in node.parent.expressions
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _find_subquery_table_alias(select_parent: exp.Expr) -> t.Optional[str]:
|
|
243
|
+
"""
|
|
244
|
+
Walk up from a SELECT to find a Subquery or Lateral with an alias.
|
|
245
|
+
"""
|
|
246
|
+
p = select_parent.parent
|
|
247
|
+
while p is not None:
|
|
248
|
+
if isinstance(p, (exp.Subquery, exp.Lateral)) and p.alias:
|
|
249
|
+
return p.alias
|
|
250
|
+
if isinstance(p, exp.CTE):
|
|
251
|
+
break
|
|
252
|
+
p = p.parent
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _rewrite_field_references(
|
|
257
|
+
alias_node: exp.Alias, row_exprs: t.List[exp.Expr], fields: t.List[str], statement: exp.Expr
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Rewrite (alias_name).field references to the associated column or expression.
|
|
260
|
+
|
|
261
|
+
There are two cases to consider:
|
|
262
|
+
1. When the ROW cast alias lives inside a subquery, dot references in the outer query
|
|
263
|
+
are rewritten to qualified column references using the subquery's table alias.
|
|
264
|
+
Given type `t` with fields (x, y):
|
|
265
|
+
`SELECT (r).x FROM (SELECT ROW(a, b)::t AS r FROM t1) AS sub`
|
|
266
|
+
-> `SELECT sub.x AS x FROM (SELECT a AS x, b AS y FROM t1) AS sub`
|
|
267
|
+
|
|
268
|
+
2. When the ROW cast alias lives in a CTE or top-level query (no subquery alias),
|
|
269
|
+
dot references are inlined with the original ROW expressions.
|
|
270
|
+
Given type `t` with fields (x, y):
|
|
271
|
+
`SELECT (r).x FROM (SELECT ROW(a + 1, b)::t AS r FROM t1)`
|
|
272
|
+
-> `SELECT a + 1 AS x FROM (SELECT a + 1 AS x, b AS y FROM t1)`
|
|
273
|
+
"""
|
|
274
|
+
subquery_table_alias = _find_subquery_table_alias(alias_node)
|
|
275
|
+
|
|
276
|
+
for dot in statement.find_all(exp.Dot):
|
|
277
|
+
inner_col = dot.this.unnest()
|
|
278
|
+
dot_right = dot.expression
|
|
279
|
+
if not (
|
|
280
|
+
isinstance(inner_col, exp.Column)
|
|
281
|
+
and inner_col.name == alias_node.alias
|
|
282
|
+
and isinstance(dot_right, exp.Identifier)
|
|
283
|
+
and dot_right.name in fields
|
|
284
|
+
):
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
field_name = dot_right.name
|
|
288
|
+
if subquery_table_alias:
|
|
289
|
+
replacement = exp.alias_(exp.column(field_name, table=subquery_table_alias), field_name)
|
|
290
|
+
else:
|
|
291
|
+
field_idx = fields.index(field_name)
|
|
292
|
+
replacement = exp.alias_(row_exprs[field_idx].copy(), field_name)
|
|
293
|
+
dot.replace(replacement)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _expand_to_select_list(node: exp.Expr, expressions: t.List[exp.Expr], aliases: t.List[str]) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Expand a node into multiple aliased expressions in its parent SELECT list.
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
`SELECT ROW(a, b)::my_type AS r` with expressions=[a, b] and aliases=["x", "y"]:
|
|
302
|
+
`node` (the alias `r`) is replaced in-place -> `SELECT a AS x, b AS y`
|
|
303
|
+
"""
|
|
304
|
+
if not _is_direct_select_expression(node):
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
select_node = node.parent
|
|
308
|
+
idx = select_node.expressions.index(node)
|
|
309
|
+
new_exprs = [exp.alias_(expressions[i].copy(), aliases[i]) for i in range(len(aliases))]
|
|
310
|
+
select_node.set(
|
|
311
|
+
"expressions",
|
|
312
|
+
select_node.expressions[:idx] + new_exprs + select_node.expressions[idx + 1 :],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _is_row_cast(node: exp.Expr) -> bool:
|
|
317
|
+
"""
|
|
318
|
+
Check if a node is a Cast of a ROW() to a USERDEFINED type.
|
|
319
|
+
"""
|
|
320
|
+
return (
|
|
321
|
+
isinstance(node, exp.Cast) and util.is_row_function(node.this) and node.to.this == exp.DataType.Type.USERDEFINED
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _get_row_cast(inner: exp.Expr, row_node_set: t.Set[exp.Anonymous]) -> t.Optional[exp.Cast]:
|
|
326
|
+
"""
|
|
327
|
+
Return the Cast node if it wraps a ROW().
|
|
328
|
+
|
|
329
|
+
Examples:
|
|
330
|
+
`(ROW(a, b)::my_type)` -> Cast node
|
|
331
|
+
"""
|
|
332
|
+
return inner if _is_row_cast(inner) and inner.this in row_node_set else None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _is_inside_unnest(node: exp.Expr) -> bool:
|
|
336
|
+
"""
|
|
337
|
+
Check if a node is nested inside an UNNEST or ARRAY expression.
|
|
338
|
+
|
|
339
|
+
Examples:
|
|
340
|
+
`UNNEST(ARRAY(SELECT ROW(a)::t FROM t1))` -> True
|
|
341
|
+
"""
|
|
342
|
+
return node.find_ancestor(exp.Unnest, exp.Array) is not None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _lookup_type_fields(type_name: str, query: Q) -> t.List[str]:
|
|
346
|
+
"""
|
|
347
|
+
Return ordered field names for a composite type.
|
|
348
|
+
|
|
349
|
+
Examples:
|
|
350
|
+
Given a type `my_type` defined as `CREATE TYPE my_type AS (x INT, y TEXT)`:
|
|
351
|
+
`_lookup_type_fields("my_type", query)` -> `["x", "y"]`
|
|
352
|
+
`_lookup_type_fields("unknown_type", query)` -> `[]`
|
|
353
|
+
"""
|
|
354
|
+
type_table = exp.Table(this=exp.to_identifier(type_name))
|
|
355
|
+
type_query = query.object_mapping.lookup_type_query(table=type_table, raise_on_missing=False)
|
|
356
|
+
if type_query is None:
|
|
357
|
+
return []
|
|
358
|
+
return [c.name for c in type_query.get_column_defs()]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _extract_field(cast_node: exp.Cast, field_index: int) -> exp.Expr:
|
|
362
|
+
"""
|
|
363
|
+
Return the k-th expression from the ROW() constructor.
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
`CAST(ROW(a, b, c) AS my_type), field_index=1` -> `b`
|
|
367
|
+
"""
|
|
368
|
+
return cast_node.this.expressions[field_index].copy()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _replace_row_casts_in_case(case_node: exp.Case, field_index: int) -> exp.Case:
|
|
372
|
+
"""
|
|
373
|
+
Replace every Cast(ROW(...), my_type) inside a Case expression with
|
|
374
|
+
the field_index-th ROW argument.
|
|
375
|
+
|
|
376
|
+
Examples:
|
|
377
|
+
`CASE WHEN x THEN ROW(a, b)::t ELSE ROW(0, '')::t END, field_index=0` -> `CASE WHEN x THEN a ELSE 0 END`
|
|
378
|
+
"""
|
|
379
|
+
new_case = case_node.copy()
|
|
380
|
+
for cast in new_case.find_all(exp.Cast):
|
|
381
|
+
if _is_row_cast(cast):
|
|
382
|
+
cast.replace(_extract_field(cast, field_index))
|
|
383
|
+
return new_case
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def transform_row_function_to_subquery(replacement_expr: exp.Expr, query: UserDefinedFunctionQuery) -> exp.Expr:
|
|
387
|
+
"""Transform a ROW() function into a subquery."""
|
|
388
|
+
transformed = False
|
|
389
|
+
|
|
390
|
+
for cast_node in list(replacement_expr.find_all(exp.Cast)):
|
|
391
|
+
row_expr = cast_node.this
|
|
392
|
+
if util.is_row_function(row_expr):
|
|
393
|
+
# Don't replace if it's being used for field access, as it's likely
|
|
394
|
+
# already been handled or needs to be preserved.
|
|
395
|
+
if not (isinstance(cast_node.parent, exp.Paren) and isinstance(cast_node.parent.parent, exp.Dot)):
|
|
396
|
+
if cast_node is not replacement_expr:
|
|
397
|
+
new_expr = transform_row_to_subquery(cast_node, replacement_expr, query)
|
|
398
|
+
if new_expr:
|
|
399
|
+
transformed = True
|
|
400
|
+
replacement_expr = new_expr
|
|
401
|
+
if transformed:
|
|
402
|
+
logger.debug(f"Replaced ROW() to subquery: {replacement_expr.sql(dialect='postgres')}")
|
|
403
|
+
return replacement_expr
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def transform_row_to_subquery(
|
|
407
|
+
node: exp.Cast, replacement_expr: exp.Expr, query: UserDefinedFunctionQuery
|
|
408
|
+
) -> t.Optional[exp.Expr]:
|
|
409
|
+
"""
|
|
410
|
+
Transforms the ROW(...)::table_name pattern into a SELECT subquery.
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
Query: `SELECT ROW(1, 'abc')::my_table` where `my_table` has two columns.
|
|
414
|
+
Result: `ROW(1, 'abc')::my_table` -> `(SELECT 1, 'abc')`
|
|
415
|
+
"""
|
|
416
|
+
# 1. Check if this is a ROW(...) cast to a known table
|
|
417
|
+
data_type = node.args.get("to")
|
|
418
|
+
if not isinstance(data_type, exp.DataType):
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
type_name = data_type.sql().lower()
|
|
422
|
+
table = exp.to_table(type_name)
|
|
423
|
+
object_query = query.object_mapping.lookup_table_query(table=table, raise_on_missing=False)
|
|
424
|
+
if not object_query:
|
|
425
|
+
object_query = query.object_mapping.lookup_type_query(table=table, raise_on_missing=False)
|
|
426
|
+
if not object_query:
|
|
427
|
+
raise exception.MappingError(message=f"Unknown table or type in cast to ROW(): {type_name}")
|
|
428
|
+
|
|
429
|
+
row_expr = node.this
|
|
430
|
+
|
|
431
|
+
# Ensure column count matches
|
|
432
|
+
columns = object_query.get_column_defs()
|
|
433
|
+
if len(row_expr.expressions) != len(columns):
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
# 2. Convert to SELECT
|
|
437
|
+
new_node = exp.select(*(val.copy() for val in row_expr.expressions))
|
|
438
|
+
|
|
439
|
+
# 3. Apply replacement, lifting if inside a single-expression SELECT
|
|
440
|
+
parent = node.parent
|
|
441
|
+
if not parent:
|
|
442
|
+
return new_node if node is replacement_expr else None
|
|
443
|
+
|
|
444
|
+
if isinstance(parent, exp.Select) and len(parent.expressions) == 1:
|
|
445
|
+
if parent is replacement_expr:
|
|
446
|
+
return new_node
|
|
447
|
+
parent.replace(new_node)
|
|
448
|
+
else:
|
|
449
|
+
node.replace(exp.Paren(this=new_node))
|
|
450
|
+
|
|
451
|
+
return None
|