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
sqlleaf/mappings.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from sqlglot import MappingSchema, exp
|
|
6
|
+
from sqlglot.dialects.dialect import DialectType
|
|
7
|
+
from sqlglot.schema import nested_set
|
|
8
|
+
from sqlglot.trie import new_trie
|
|
9
|
+
|
|
10
|
+
from sqlleaf import exception, util
|
|
11
|
+
from sqlleaf.models.query import (
|
|
12
|
+
CTASQuery,
|
|
13
|
+
DatabaseQuery,
|
|
14
|
+
PrepareQuery,
|
|
15
|
+
ProcedureQuery,
|
|
16
|
+
Q,
|
|
17
|
+
SchemaQuery,
|
|
18
|
+
SequenceQuery,
|
|
19
|
+
StageQuery,
|
|
20
|
+
TableQuery,
|
|
21
|
+
TriggerQuery,
|
|
22
|
+
TypeQuery,
|
|
23
|
+
UserDefinedFunctionQuery,
|
|
24
|
+
ViewQuery,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
ColumnMapping = t.Union[t.Dict, str, t.List]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ObjectMapping(MappingSchema):
|
|
31
|
+
"""
|
|
32
|
+
Extends sqlglot.MappingSchema to provide additional functionality related to tracking exp.Table
|
|
33
|
+
|
|
34
|
+
Specifically, we need to track the exp.Table inside the exp.Create statements, as they contain more information
|
|
35
|
+
than the exp.Table that we encounter later when parsing INSERT statements.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, dialect: str):
|
|
39
|
+
"""
|
|
40
|
+
Initialize a mapping of tables parts to exp.Table
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(
|
|
43
|
+
dialect=dialect, normalize=False
|
|
44
|
+
) # Set `normalize=False` to prevent an unnecessary second parse.
|
|
45
|
+
self.kind_mapping = {}
|
|
46
|
+
self.kind_mapping_trie = {}
|
|
47
|
+
self.session_variables: dict[str, exp.Expr] = {}
|
|
48
|
+
|
|
49
|
+
def add_database_query(self, query: DatabaseQuery) -> None:
|
|
50
|
+
self._add_query(kind="database", query=query, dialect=query.dialect)
|
|
51
|
+
|
|
52
|
+
def add_prepare_query(self, query: PrepareQuery) -> None:
|
|
53
|
+
self._add_query(kind="prepare", query=query, dialect=query.dialect)
|
|
54
|
+
|
|
55
|
+
def add_procedure_query(self, query: ProcedureQuery) -> None:
|
|
56
|
+
self._add_query(kind="procedure", query=query, dialect=query.dialect)
|
|
57
|
+
|
|
58
|
+
def add_schema_query(self, query: SchemaQuery) -> None:
|
|
59
|
+
self._add_query(kind="schema", query=query, dialect=query.dialect)
|
|
60
|
+
|
|
61
|
+
def add_sequence_query(self, query: SequenceQuery) -> None:
|
|
62
|
+
self._add_query(kind="sequence", query=query, dialect=query.dialect)
|
|
63
|
+
|
|
64
|
+
def add_stage_query(self, query: StageQuery) -> None:
|
|
65
|
+
self._add_query(kind="stage", query=query, dialect=query.dialect)
|
|
66
|
+
|
|
67
|
+
def add_table_query(
|
|
68
|
+
self, query: TableQuery | ViewQuery | CTASQuery, column_mapping: t.Optional[ColumnMapping] = None
|
|
69
|
+
) -> None:
|
|
70
|
+
self._add_query(kind="table", query=query, column_mapping=column_mapping, dialect=query.dialect)
|
|
71
|
+
|
|
72
|
+
def add_trigger_query(self, query: TriggerQuery) -> None:
|
|
73
|
+
self._add_query(kind="trigger", query=query, dialect=query.dialect)
|
|
74
|
+
|
|
75
|
+
def add_type_query(self, query: TypeQuery) -> None:
|
|
76
|
+
self._add_query(kind="type", query=query, dialect=query.dialect)
|
|
77
|
+
|
|
78
|
+
def add_udf_query(self, query: UserDefinedFunctionQuery, column_mapping: t.Optional[ColumnMapping] = None) -> None:
|
|
79
|
+
self._add_query(kind="udf", query=query, column_mapping=column_mapping, dialect=query.dialect)
|
|
80
|
+
|
|
81
|
+
def _add_query(
|
|
82
|
+
self,
|
|
83
|
+
kind: str,
|
|
84
|
+
query: Q,
|
|
85
|
+
column_mapping: t.Optional[ColumnMapping] = None,
|
|
86
|
+
dialect: DialectType = None,
|
|
87
|
+
normalize: t.Optional[bool] = None,
|
|
88
|
+
match_depth: bool = False,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Register or update a table. Updates are only performed if a new column mapping is provided.
|
|
92
|
+
The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
kind: the expression's kind
|
|
96
|
+
query: the query to store
|
|
97
|
+
column_mapping: a column mapping that describes the structure of the table.
|
|
98
|
+
dialect: the SQL dialect that will be used to parse `table` if it's a string.
|
|
99
|
+
normalize: whether to normalize identifiers according to the dialect of interest.
|
|
100
|
+
match_depth: whether to enforce that the table must match the schema's depth or not.
|
|
101
|
+
"""
|
|
102
|
+
# Initialize the structures. These are required by sqlglot.
|
|
103
|
+
if kind not in self.kind_mapping:
|
|
104
|
+
self.kind_mapping[kind] = {}
|
|
105
|
+
self.kind_mapping_trie[kind] = new_trie({})
|
|
106
|
+
|
|
107
|
+
# Normalize the table parts (catalog.db.table.column)
|
|
108
|
+
table = query.get_target_as_table()
|
|
109
|
+
normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
|
|
110
|
+
parts = self.table_parts(normalized_table)
|
|
111
|
+
|
|
112
|
+
if kind == "udf":
|
|
113
|
+
# Store the UDF with other UDFs of the same name
|
|
114
|
+
udfs_with_same_name = self.lookup_udf_query(table, raise_on_missing=False) or []
|
|
115
|
+
query = [query] + udfs_with_same_name
|
|
116
|
+
|
|
117
|
+
# Store the object
|
|
118
|
+
nested_set(self.kind_mapping[kind], tuple(reversed(parts)), query)
|
|
119
|
+
new_trie([parts], self.kind_mapping_trie[kind])
|
|
120
|
+
|
|
121
|
+
if kind in ["table", "udf"] and column_mapping is not None:
|
|
122
|
+
# Track the table's columns. UDFs are supported by sqlglot when qualifying columns
|
|
123
|
+
# TODO: there is a bug here where multiple UDFs with the same name that have different return columns
|
|
124
|
+
# will have their column names overwritten (non-merged)
|
|
125
|
+
self._add_columns_for_table(
|
|
126
|
+
table=table,
|
|
127
|
+
column_mapping=column_mapping,
|
|
128
|
+
dialect=dialect,
|
|
129
|
+
normalize=normalize,
|
|
130
|
+
match_depth=match_depth,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _add_columns_for_table(
|
|
134
|
+
self,
|
|
135
|
+
table: exp.Table,
|
|
136
|
+
column_mapping: t.Optional[ColumnMapping] = None,
|
|
137
|
+
dialect: DialectType = None,
|
|
138
|
+
normalize: t.Optional[bool] = None,
|
|
139
|
+
match_depth: bool = False,
|
|
140
|
+
):
|
|
141
|
+
super().add_table(
|
|
142
|
+
table=table,
|
|
143
|
+
column_mapping=column_mapping,
|
|
144
|
+
dialect=dialect,
|
|
145
|
+
normalize=normalize,
|
|
146
|
+
match_depth=match_depth,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def find_columns_for_table(
|
|
150
|
+
self,
|
|
151
|
+
table: exp.Table,
|
|
152
|
+
raise_on_missing: bool = True,
|
|
153
|
+
ensure_data_types: bool = False,
|
|
154
|
+
):
|
|
155
|
+
"""
|
|
156
|
+
A nicer name for the parent's function.
|
|
157
|
+
"""
|
|
158
|
+
return super().find(
|
|
159
|
+
table,
|
|
160
|
+
raise_on_missing=raise_on_missing,
|
|
161
|
+
ensure_data_types=ensure_data_types,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def lookup_prepare_query(self, table: exp.Table, raise_on_missing: bool = True) -> PrepareQuery | None:
|
|
165
|
+
return self._lookup_query(kind="prepare", object=table, raise_on_missing=raise_on_missing)
|
|
166
|
+
|
|
167
|
+
def lookup_procedure_query(self, table: exp.Table, raise_on_missing: bool = True) -> ProcedureQuery | None:
|
|
168
|
+
return self._lookup_query(kind="procedure", object=table, raise_on_missing=raise_on_missing)
|
|
169
|
+
|
|
170
|
+
def lookup_sequence_query(self, table: exp.Table, raise_on_missing: bool = True) -> SequenceQuery | None:
|
|
171
|
+
return self._lookup_query(kind="sequence", object=table, raise_on_missing=raise_on_missing)
|
|
172
|
+
|
|
173
|
+
def lookup_stage_query(self, table: exp.Table, raise_on_missing: bool = True) -> StageQuery | None:
|
|
174
|
+
return self._lookup_query(kind="stage", object=table, raise_on_missing=raise_on_missing)
|
|
175
|
+
|
|
176
|
+
def lookup_table_query(self, table: exp.Table, raise_on_missing: bool = True) -> TableQuery | None:
|
|
177
|
+
return self._lookup_query(kind="table", object=table, raise_on_missing=raise_on_missing)
|
|
178
|
+
|
|
179
|
+
def lookup_trigger_query(self, table: exp.Table, raise_on_missing: bool = True) -> TriggerQuery | None:
|
|
180
|
+
return self._lookup_query(kind="trigger", object=table, raise_on_missing=raise_on_missing)
|
|
181
|
+
|
|
182
|
+
def lookup_type_query(self, table: exp.Table, raise_on_missing: bool = True) -> TypeQuery | None:
|
|
183
|
+
return self._lookup_query(kind="type", object=table, raise_on_missing=raise_on_missing)
|
|
184
|
+
|
|
185
|
+
def lookup_udf_query(
|
|
186
|
+
self, table: exp.Table, raise_on_missing: bool = True
|
|
187
|
+
) -> t.List[UserDefinedFunctionQuery] | None:
|
|
188
|
+
# A UDF can have multiple names but different types, thus we return a list
|
|
189
|
+
return self._lookup_query(kind="udf", object=table, raise_on_missing=raise_on_missing)
|
|
190
|
+
|
|
191
|
+
def _lookup_query(
|
|
192
|
+
self,
|
|
193
|
+
kind: str,
|
|
194
|
+
object: exp.Table,
|
|
195
|
+
raise_on_missing: bool,
|
|
196
|
+
) -> Q | None:
|
|
197
|
+
"""
|
|
198
|
+
Returns the Query for a given object kind and exp.Table.
|
|
199
|
+
|
|
200
|
+
This is different from the MappingSchema's find(), which returns the column mappings.
|
|
201
|
+
This returns an exp.Table.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
kind: the expression's kind
|
|
205
|
+
object: the target object.
|
|
206
|
+
raise_on_missing: whether to raise in case the schema is not found.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The schema of the target object.
|
|
210
|
+
"""
|
|
211
|
+
if kind not in self.kind_mapping:
|
|
212
|
+
if raise_on_missing:
|
|
213
|
+
raise exception.MappingError(f"Could not find '{object.this}' of type '{kind}' in mapping.")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
parts = self.table_parts(object)[0: len(self.supported_table_args)]
|
|
217
|
+
resolved_parts = self._find_in_trie(parts, self.kind_mapping_trie[kind], raise_on_missing)
|
|
218
|
+
|
|
219
|
+
if resolved_parts is None:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
result = self.nested_get(resolved_parts, self.kind_mapping[kind], raise_on_missing=raise_on_missing)
|
|
223
|
+
if not result:
|
|
224
|
+
return None
|
|
225
|
+
elif isinstance(result, dict):
|
|
226
|
+
# The mapping object has varying depth if some objects use a catalog and others don't
|
|
227
|
+
if object.name in result:
|
|
228
|
+
return result[object.name]
|
|
229
|
+
else:
|
|
230
|
+
return None
|
|
231
|
+
else:
|
|
232
|
+
# Must be a Query
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
# Override sqlglot's property. It seems to be buggy when using different dict sizes (catalog, schema, etc)
|
|
236
|
+
@property
|
|
237
|
+
def supported_table_args(self) -> t.Tuple[str, ...]:
|
|
238
|
+
return exp.TABLE_PARTS
|
|
239
|
+
|
|
240
|
+
def get_table_or_stage(
|
|
241
|
+
self, table: exp.Table | exp.Var, raise_on_missing: bool = True
|
|
242
|
+
) -> TableQuery | StageQuery | None:
|
|
243
|
+
"""
|
|
244
|
+
Get the 'CREATE' query for a table or stage.
|
|
245
|
+
"""
|
|
246
|
+
table_expr = table
|
|
247
|
+
if isinstance(table_expr, exp.Var):
|
|
248
|
+
table_expr = exp.Table(this=table)
|
|
249
|
+
|
|
250
|
+
if str(table_expr).startswith("@"):
|
|
251
|
+
child_object_query = self.lookup_stage_query(table=table_expr, raise_on_missing=raise_on_missing)
|
|
252
|
+
else:
|
|
253
|
+
child_object_query = self.lookup_table_query(table=table_expr, raise_on_missing=raise_on_missing)
|
|
254
|
+
|
|
255
|
+
if not child_object_query and raise_on_missing:
|
|
256
|
+
raise exception.MappingError(message=f"Unknown table: {str(table_expr)}")
|
|
257
|
+
|
|
258
|
+
return child_object_query
|
|
259
|
+
|
|
260
|
+
def lookup_udf_call(
|
|
261
|
+
self,
|
|
262
|
+
node: exp.Anonymous,
|
|
263
|
+
) -> t.Optional[UserDefinedFunctionQuery]:
|
|
264
|
+
"""
|
|
265
|
+
Looks up the UDF definition for a single exp.Anonymous node.
|
|
266
|
+
Returns the matched UDF definition, or None if not found.
|
|
267
|
+
"""
|
|
268
|
+
function_schema, function_name = util.get_udf_name(node)
|
|
269
|
+
udf_object = exp.table_(table=function_name, db=function_schema)
|
|
270
|
+
candidates = self.lookup_udf_query(table=udf_object, raise_on_missing=False)
|
|
271
|
+
if not candidates:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
return resolve_overloaded_function(node, candidates)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def resolve_overloaded_function(
|
|
278
|
+
node: exp.Anonymous, candidates: t.List[UserDefinedFunctionQuery]
|
|
279
|
+
) -> t.Optional[UserDefinedFunctionQuery]:
|
|
280
|
+
"""
|
|
281
|
+
Resolves the best function candidate for an overloaded function call.
|
|
282
|
+
Applies some precedence rules (e.g., type matching, or non-variadic > variadic) but many are currently
|
|
283
|
+
excluded due to the complexity of the rules.
|
|
284
|
+
"""
|
|
285
|
+
if len(candidates) == 1:
|
|
286
|
+
return candidates[0]
|
|
287
|
+
|
|
288
|
+
# Function overloading: find the best match based on arguments
|
|
289
|
+
args = node.expressions
|
|
290
|
+
matches = []
|
|
291
|
+
for candidate in candidates:
|
|
292
|
+
if match_function_arguments(args, candidate):
|
|
293
|
+
matches.append(candidate)
|
|
294
|
+
|
|
295
|
+
if not matches:
|
|
296
|
+
raise exception.MappingError(f"No matching function signatures found for args: {args}")
|
|
297
|
+
|
|
298
|
+
if len(matches) == 1:
|
|
299
|
+
return matches[0]
|
|
300
|
+
|
|
301
|
+
# Preference rule: non-variadic is preferred over variadic
|
|
302
|
+
non_variadic_matches = [m for m in matches if not any(p.is_variadic for p in m.parameters)]
|
|
303
|
+
if non_variadic_matches:
|
|
304
|
+
# Prefer more specific non-variadic matches over polymorphic ones
|
|
305
|
+
specific_matches = [
|
|
306
|
+
m
|
|
307
|
+
for m in non_variadic_matches
|
|
308
|
+
if not any(str(p.type).lower() in ("anyelement", "anyarray") for p in m.parameters)
|
|
309
|
+
]
|
|
310
|
+
if specific_matches:
|
|
311
|
+
return specific_matches[0]
|
|
312
|
+
|
|
313
|
+
# If we only have polymorphic matches, prefer anyarray for array arguments
|
|
314
|
+
if all(arg.type and arg.type.is_type(exp.DataType.Type.ARRAY) for arg in args):
|
|
315
|
+
anyarray_matches = [
|
|
316
|
+
m for m in non_variadic_matches if any(str(p.type).lower() == "anyarray" for p in m.parameters)
|
|
317
|
+
]
|
|
318
|
+
if anyarray_matches:
|
|
319
|
+
return anyarray_matches[0]
|
|
320
|
+
|
|
321
|
+
return non_variadic_matches[0]
|
|
322
|
+
|
|
323
|
+
return matches[0]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def match_function_arguments(args: t.List[exp.Expr], candidate: UserDefinedFunctionQuery) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Checks if the provided arguments match the function candidate's parameters.
|
|
329
|
+
Handles exact types, polymorphic types (anyelement, anyarray), and VARIADIC parameters.
|
|
330
|
+
"""
|
|
331
|
+
params = candidate.parameters
|
|
332
|
+
arg_count = len(args)
|
|
333
|
+
param_count = len(params)
|
|
334
|
+
|
|
335
|
+
has_variadic = any(p.is_variadic for p in params)
|
|
336
|
+
|
|
337
|
+
if not has_variadic:
|
|
338
|
+
if arg_count != param_count:
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
for i, arg in enumerate(args):
|
|
342
|
+
if not match_type(arg, params[i].type):
|
|
343
|
+
return False
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
def match_type(arg: exp.Expr, target_type: exp.DataType) -> bool:
|
|
347
|
+
"""Matches an argument expression to a target data type."""
|
|
348
|
+
arg_type = arg.type
|
|
349
|
+
if not arg_type:
|
|
350
|
+
# If type isn't annotated, we can't be sure, but let's try to be lenient or rely on annotate_types
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
if arg_type.this == exp.DataType.Type.VARCHAR:
|
|
354
|
+
arg_type = exp.DataType.build("TEXT")
|
|
355
|
+
|
|
356
|
+
target_type_name = target_type.sql().lower()
|
|
357
|
+
|
|
358
|
+
if target_type_name == "anyelement":
|
|
359
|
+
return True
|
|
360
|
+
if target_type_name == "anyarray":
|
|
361
|
+
return arg_type.is_type(exp.DataType.Type.ARRAY)
|
|
362
|
+
|
|
363
|
+
# If target is DECIMAL but arg is TEXT, they don't match
|
|
364
|
+
if target_type.is_type(exp.DataType.Type.DECIMAL) and arg_type.is_type(exp.DataType.Type.TEXT):
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
# Handle numeric type matching - Postgres allows implicit cast from literal to numeric
|
|
368
|
+
if target_type.is_type(exp.DataType.Type.DECIMAL):
|
|
369
|
+
if arg_type.is_type(exp.DataType.Type.DOUBLE, exp.DataType.Type.FLOAT, exp.DataType.Type.DECIMAL):
|
|
370
|
+
return True
|
|
371
|
+
# Literals often come as DOUBLE but should match NUMERIC
|
|
372
|
+
if isinstance(arg, (exp.Literal, exp.Cast)):
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
return exp.DataType.is_type(arg_type, target_type)
|
|
376
|
+
|
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
import typing as t
|
|
6
|
+
from dataclasses import InitVar, dataclass
|
|
7
|
+
|
|
8
|
+
import networkx as nx
|
|
9
|
+
from sqlglot import exp
|
|
10
|
+
from sqlglot.optimizer import Scope, traverse_scope
|
|
11
|
+
|
|
12
|
+
from sqlleaf import util
|
|
13
|
+
from sqlleaf.typing import TableOrScopeType
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("sqlleaf")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ScopePositions:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.positions: t.Dict[int, t.Dict[int, int]] = {}
|
|
21
|
+
|
|
22
|
+
def get_position_for_expr(self, expr: exp.Expr) -> dict[int, int]:
|
|
23
|
+
return self.positions[id(expr)]
|
|
24
|
+
|
|
25
|
+
def calculate(self, scope: Scope) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Determine the height and width of every scope (SELECT statement) in the query's expression tree.
|
|
28
|
+
This iterates over every expression in the tree via Depth-First Search, looking for scopes.
|
|
29
|
+
"""
|
|
30
|
+
root_expr = scope.expression.root()
|
|
31
|
+
scopes = {id(scope.expression): scope for scope in list(traverse_scope(root_expr))}
|
|
32
|
+
|
|
33
|
+
# For each height, map to the current width
|
|
34
|
+
heights_to_widths = {}
|
|
35
|
+
expr_ids_to_positions = {}
|
|
36
|
+
stack = [(root_expr, 1)]
|
|
37
|
+
|
|
38
|
+
while stack:
|
|
39
|
+
node, h = stack.pop()
|
|
40
|
+
node_id = id(node)
|
|
41
|
+
|
|
42
|
+
if node_id in scopes:
|
|
43
|
+
logger.debug(f"Found scope expr ({node.__class__.__name__}): {node.sql()}")
|
|
44
|
+
|
|
45
|
+
if not expr_ids_to_positions: # Root node
|
|
46
|
+
expr_ids_to_positions[node_id] = (0, 0)
|
|
47
|
+
heights_to_widths[0] = 0
|
|
48
|
+
else:
|
|
49
|
+
# Track the width across varying heights
|
|
50
|
+
w = heights_to_widths.get(h, 0)
|
|
51
|
+
expr_ids_to_positions[node_id] = (h, w)
|
|
52
|
+
heights_to_widths[h] = w + 1
|
|
53
|
+
logger.debug(f"Set height={h} width={w}")
|
|
54
|
+
h = h + 1
|
|
55
|
+
scopes.pop(node_id)
|
|
56
|
+
|
|
57
|
+
for v in node.iter_expressions(reverse=True):
|
|
58
|
+
stack.append((v, h))
|
|
59
|
+
|
|
60
|
+
self.positions = expr_ids_to_positions
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class GeneratorContext[Q, N]:
|
|
65
|
+
graph: nx.MultiDiGraph
|
|
66
|
+
query: Q
|
|
67
|
+
expr: exp.Expr
|
|
68
|
+
scope: TableOrScopeType
|
|
69
|
+
child_node: N
|
|
70
|
+
scope_positions: ScopePositions = ScopePositions()
|
|
71
|
+
data_type: exp.DataType | None = None
|
|
72
|
+
# Override the data_type if needed
|
|
73
|
+
new_data_type: InitVar[exp.DataType | None] = None
|
|
74
|
+
|
|
75
|
+
def __post_init__(self, new_data_type: exp.DataType | None = None):
|
|
76
|
+
"""
|
|
77
|
+
Called via replace() or if a new object is instantiated
|
|
78
|
+
"""
|
|
79
|
+
expr_type = new_data_type if new_data_type else self.get_data_type_of_expr(self.expr)
|
|
80
|
+
unwrapped_expr = util.unwrap_expression(self.expr)
|
|
81
|
+
|
|
82
|
+
object.__setattr__(self, "data_type", expr_type)
|
|
83
|
+
object.__setattr__(self, "expr", unwrapped_expr)
|
|
84
|
+
|
|
85
|
+
def get_child_node(self) -> N:
|
|
86
|
+
assert self.child_node is not None
|
|
87
|
+
return self.child_node
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def get_data_type_of_expr(expr: exp.Expr) -> exp.DataType:
|
|
91
|
+
"""
|
|
92
|
+
Determine the expression's data type. If it's missing, use an ancestor's data type.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def is_missing_type(x: exp.Expr) -> bool:
|
|
96
|
+
return not (x.type or x.is_type(exp.DType.UNKNOWN.into_expr()))
|
|
97
|
+
|
|
98
|
+
if isinstance(expr, exp.ColumnDef):
|
|
99
|
+
return expr.kind or exp.DType.UNKNOWN.into_expr()
|
|
100
|
+
elif is_missing_type(expr) and expr.parent:
|
|
101
|
+
# Use an ancestor's type
|
|
102
|
+
parent = expr.parent
|
|
103
|
+
while parent:
|
|
104
|
+
if not is_missing_type(parent):
|
|
105
|
+
return t.cast(exp.DataType, parent.type)
|
|
106
|
+
parent = parent.parent
|
|
107
|
+
|
|
108
|
+
return t.cast(exp.DataType, expr.parent.type)
|
|
109
|
+
return expr.type or exp.DType.UNKNOWN.into_expr()
|
|
110
|
+
|
|
111
|
+
def replace(self, **args) -> GeneratorContext[Q, N]:
|
|
112
|
+
"""
|
|
113
|
+
Convenience method to take us to this class definition.
|
|
114
|
+
"""
|
|
115
|
+
return dataclasses.replace(self, **args)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class PositionContext:
|
|
120
|
+
# The position of this query inside a list of queries, e.g. SELECT 'a'; SELECT 'b' -> a=0, b=1
|
|
121
|
+
statement_index: str
|
|
122
|
+
|
|
123
|
+
# The position of this column inside a set of selected columns (e.g. SELECT 'a', 'b') -> a=0, b=1
|
|
124
|
+
select_index: int = 0
|
|
125
|
+
|
|
126
|
+
# The depth of the function: e.g. SELECT UPPER(LOWER('a')) -> LOWER=0, UPPER=1
|
|
127
|
+
function_depth: int = 0
|
|
128
|
+
|
|
129
|
+
# The argument of a function: e.g. SELECT my.func('a', 'b') -> a=0, b=1
|
|
130
|
+
function_arg_index: int = 0
|
|
131
|
+
|
|
132
|
+
# The depth of a subquery, e.g. WITH cte AS (SELECT 'a') SELECT 'a' -> The first a=1, second a=0
|
|
133
|
+
query_depth: int = 0
|
|
134
|
+
|
|
135
|
+
# The width of a subquery, e.g. SELECT 4 + (SELECT 5) + (SELECT 6) -> depth(4)=0, depth(5)=1, depth(6)=1
|
|
136
|
+
query_width: int = 0
|
|
137
|
+
|
|
138
|
+
def as_str(self) -> str:
|
|
139
|
+
parts = [
|
|
140
|
+
f"query_depth={self.query_depth}",
|
|
141
|
+
f"query_width={self.query_width}",
|
|
142
|
+
f"statement={self.statement_index}",
|
|
143
|
+
f"select={self.select_index}",
|
|
144
|
+
f"func_depth={self.function_depth}",
|
|
145
|
+
f"func_arg={self.function_arg_index}",
|
|
146
|
+
]
|
|
147
|
+
return " ".join(parts)
|
|
148
|
+
|
|
149
|
+
def replace(self, **args) -> PositionContext:
|
|
150
|
+
"""
|
|
151
|
+
Convenience method to take us to this class definition.
|
|
152
|
+
"""
|
|
153
|
+
return dataclasses.replace(self, **args)
|
sqlleaf/models/hook.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from sqlleaf.models.node.base import EdgeAttributes as EdgeAttributes
|
|
4
|
+
from sqlleaf.models.node.base import GraphAttributes as GraphAttributes
|
|
5
|
+
from sqlleaf.models.node.base import NodeAttributes as NodeAttributes
|
|
6
|
+
from sqlleaf.models.node.column import ColumnNode as ColumnNode
|
|
7
|
+
from sqlleaf.models.node.column import FileColumnNode as FileColumnNode
|
|
8
|
+
from sqlleaf.models.node.column import StageColumnNode as StageColumnNode
|
|
9
|
+
from sqlleaf.models.node.dynamodb import DynamoDbNode as DynamoDbNode
|
|
10
|
+
from sqlleaf.models.node.function import FunctionNode as FunctionNode
|
|
11
|
+
from sqlleaf.models.node.interval import IntervalNode as IntervalNode
|
|
12
|
+
from sqlleaf.models.node.json import JsonPathNode as JsonPathNode
|
|
13
|
+
from sqlleaf.models.node.literal import LiteralNode as LiteralNode
|
|
14
|
+
from sqlleaf.models.node.null import NullNode as NullNode
|
|
15
|
+
from sqlleaf.models.node.pivot import PivotNode as PivotNode
|
|
16
|
+
from sqlleaf.models.node.pivot import UnpivotNode as UnpivotNode
|
|
17
|
+
from sqlleaf.models.node.program import ProgramNode as ProgramNode
|
|
18
|
+
from sqlleaf.models.node.sequence import SequenceNode as SequenceNode
|
|
19
|
+
from sqlleaf.models.node.star import StarNode as StarNode
|
|
20
|
+
from sqlleaf.models.node.stream import StreamNode as StreamNode
|
|
21
|
+
from sqlleaf.models.node.user_defined_function import UserDefinedFunctionNode as UserDefinedFunctionNode
|
|
22
|
+
from sqlleaf.models.node.var import VarNode as VarNode
|
|
23
|
+
from sqlleaf.models.node.variable import VariableNode as VariableNode
|
|
24
|
+
from sqlleaf.models.node.window import WindowNode as WindowNode
|
|
25
|
+
|
|
26
|
+
N = t.TypeVar("N", bound=NodeAttributes)
|
|
27
|
+
TargetNodeType = ColumnNode | DynamoDbNode | FileColumnNode | StageColumnNode | StreamNode | ProgramNode
|