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
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,9 @@
1
+ import typing as t
2
+
3
+ from sqlleaf.models.node import N
4
+
5
+ class Hook:
6
+ def __init__(self, name: str, kind: N, func: t.Callable[[N], N | None]):
7
+ self.name = name
8
+ self.kind = kind
9
+ self.func = func
@@ -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