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.
Files changed (111) hide show
  1. sqlleaf/__init__.py +4 -0
  2. sqlleaf/dialects/__init__.py +0 -0
  3. sqlleaf/dialects/plpgsql/__init__.py +0 -0
  4. sqlleaf/dialects/plpgsql/p_classes.py +275 -0
  5. sqlleaf/dialects/plpgsql/p_dialect.py +148 -0
  6. sqlleaf/dialects/plpgsql/p_generator.py +538 -0
  7. sqlleaf/dialects/plpgsql/p_parser.py +1244 -0
  8. sqlleaf/dialects/plpgsql/p_tokenizer.py +41 -0
  9. sqlleaf/exception.py +67 -0
  10. sqlleaf/lineage.py +326 -0
  11. sqlleaf/mappings.py +376 -0
  12. sqlleaf/models/__init__.py +0 -0
  13. sqlleaf/models/context.py +153 -0
  14. sqlleaf/models/hook.py +9 -0
  15. sqlleaf/models/node/__init__.py +27 -0
  16. sqlleaf/models/node/base.py +148 -0
  17. sqlleaf/models/node/column.py +345 -0
  18. sqlleaf/models/node/dynamodb.py +39 -0
  19. sqlleaf/models/node/function.py +33 -0
  20. sqlleaf/models/node/interval.py +21 -0
  21. sqlleaf/models/node/json.py +57 -0
  22. sqlleaf/models/node/literal.py +17 -0
  23. sqlleaf/models/node/null.py +14 -0
  24. sqlleaf/models/node/pivot.py +34 -0
  25. sqlleaf/models/node/program.py +27 -0
  26. sqlleaf/models/node/sequence.py +18 -0
  27. sqlleaf/models/node/star.py +11 -0
  28. sqlleaf/models/node/stream.py +14 -0
  29. sqlleaf/models/node/user_defined_function.py +33 -0
  30. sqlleaf/models/node/var.py +7 -0
  31. sqlleaf/models/node/variable.py +8 -0
  32. sqlleaf/models/node/window.py +22 -0
  33. sqlleaf/models/query/__init__.py +33 -0
  34. sqlleaf/models/query/base.py +248 -0
  35. sqlleaf/models/query/call.py +111 -0
  36. sqlleaf/models/query/copy.py +144 -0
  37. sqlleaf/models/query/ctas.py +61 -0
  38. sqlleaf/models/query/database.py +48 -0
  39. sqlleaf/models/query/delete.py +26 -0
  40. sqlleaf/models/query/execute.py +66 -0
  41. sqlleaf/models/query/holder.py +49 -0
  42. sqlleaf/models/query/insert.py +69 -0
  43. sqlleaf/models/query/merge.py +29 -0
  44. sqlleaf/models/query/multitable_insert.py +42 -0
  45. sqlleaf/models/query/prepare.py +61 -0
  46. sqlleaf/models/query/procedure.py +72 -0
  47. sqlleaf/models/query/put.py +38 -0
  48. sqlleaf/models/query/replace.py +32 -0
  49. sqlleaf/models/query/schema.py +47 -0
  50. sqlleaf/models/query/select.py +27 -0
  51. sqlleaf/models/query/sequence.py +33 -0
  52. sqlleaf/models/query/set.py +74 -0
  53. sqlleaf/models/query/stage.py +78 -0
  54. sqlleaf/models/query/table.py +220 -0
  55. sqlleaf/models/query/trigger.py +42 -0
  56. sqlleaf/models/query/type.py +42 -0
  57. sqlleaf/models/query/unload.py +64 -0
  58. sqlleaf/models/query/update.py +45 -0
  59. sqlleaf/models/query/user_defined_function.py +297 -0
  60. sqlleaf/models/query/values.py +20 -0
  61. sqlleaf/models/query/view.py +41 -0
  62. sqlleaf/path.py +194 -0
  63. sqlleaf/processors/__init__.py +0 -0
  64. sqlleaf/processors/collector/__init__.py +0 -0
  65. sqlleaf/processors/collector/collector.py +827 -0
  66. sqlleaf/processors/collector/substitute.py +396 -0
  67. sqlleaf/processors/generator/__init__.py +0 -0
  68. sqlleaf/processors/generator/dialects/__init__.py +5 -0
  69. sqlleaf/processors/generator/dialects/athena.py +45 -0
  70. sqlleaf/processors/generator/dialects/base.py +572 -0
  71. sqlleaf/processors/generator/dialects/mysql.py +19 -0
  72. sqlleaf/processors/generator/dialects/postgres.py +182 -0
  73. sqlleaf/processors/generator/dialects/redshift.py +192 -0
  74. sqlleaf/processors/generator/dialects/snowflake.py +90 -0
  75. sqlleaf/processors/generator/generator.py +495 -0
  76. sqlleaf/processors/transformer/__init__.py +31 -0
  77. sqlleaf/processors/transformer/base.py +899 -0
  78. sqlleaf/processors/transformer/call.py +9 -0
  79. sqlleaf/processors/transformer/copy.py +63 -0
  80. sqlleaf/processors/transformer/ctas.py +39 -0
  81. sqlleaf/processors/transformer/delete.py +14 -0
  82. sqlleaf/processors/transformer/execute.py +9 -0
  83. sqlleaf/processors/transformer/expressions/__init__.py +11 -0
  84. sqlleaf/processors/transformer/expressions/functional_notation.py +22 -0
  85. sqlleaf/processors/transformer/expressions/row.py +451 -0
  86. sqlleaf/processors/transformer/expressions/values.py +318 -0
  87. sqlleaf/processors/transformer/insert.py +125 -0
  88. sqlleaf/processors/transformer/merge.py +14 -0
  89. sqlleaf/processors/transformer/multitable_insert.py +14 -0
  90. sqlleaf/processors/transformer/replace.py +23 -0
  91. sqlleaf/processors/transformer/transformer.py +141 -0
  92. sqlleaf/processors/transformer/udf.py +235 -0
  93. sqlleaf/processors/transformer/unload.py +30 -0
  94. sqlleaf/processors/transformer/update.py +174 -0
  95. sqlleaf/processors/transformer/values.py +10 -0
  96. sqlleaf/settings/__init__.py +19 -0
  97. sqlleaf/settings/athena.py +23 -0
  98. sqlleaf/settings/base.py +37 -0
  99. sqlleaf/settings/postgres.py +52 -0
  100. sqlleaf/system_tables/load.py +0 -0
  101. sqlleaf/typing.py +93 -0
  102. sqlleaf/util/__init__.py +34 -0
  103. sqlleaf/util/expression.py +282 -0
  104. sqlleaf/util/graph.py +116 -0
  105. sqlleaf/util/helpers.py +42 -0
  106. sqlleaf/util/iterators.py +37 -0
  107. sqlleaf-0.0.1.dist-info/METADATA +46 -0
  108. sqlleaf-0.0.1.dist-info/RECORD +111 -0
  109. sqlleaf-0.0.1.dist-info/WHEEL +5 -0
  110. sqlleaf-0.0.1.dist-info/licenses/LICENSE +21 -0
  111. sqlleaf-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,9 @@
1
+ import logging
2
+
3
+ from sqlleaf.processors.transformer import base
4
+
5
+ logger = logging.getLogger("sqlleaf")
6
+
7
+
8
+ class CallTransformer(base.BaseQueryTransformer):
9
+ pass
@@ -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,9 @@
1
+ import logging
2
+
3
+ from sqlleaf.processors.transformer import base
4
+
5
+ logger = logging.getLogger("sqlleaf")
6
+
7
+
8
+ class ExecuteTransformer(base.BaseQueryTransformer):
9
+ pass
@@ -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