sqlleaf 0.0.1__py3-none-any.whl → 0.0.2__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 (85) hide show
  1. sqlleaf/__init__.py +1 -0
  2. sqlleaf/dialects/__init__.py +1 -0
  3. sqlleaf/dialects/plpgsql/__init__.py +1 -0
  4. sqlleaf/dialects/plpgsql/p_classes.py +1 -1
  5. sqlleaf/dialects/plpgsql/p_generator.py +2 -2
  6. sqlleaf/dialects/plpgsql/p_parser.py +4 -4
  7. sqlleaf/exception.py +2 -2
  8. sqlleaf/lineage.py +8 -65
  9. sqlleaf/mappings.py +94 -34
  10. sqlleaf/models/context.py +1 -1
  11. sqlleaf/models/node/base.py +1 -1
  12. sqlleaf/models/query/__init__.py +51 -0
  13. sqlleaf/models/query/base.py +12 -21
  14. sqlleaf/models/query/block.py +35 -0
  15. sqlleaf/models/query/call.py +4 -4
  16. sqlleaf/models/query/copy.py +2 -2
  17. sqlleaf/models/query/ctas.py +6 -6
  18. sqlleaf/models/query/declare_item.py +33 -0
  19. sqlleaf/models/query/execute.py +2 -2
  20. sqlleaf/models/query/execute_dynamic.py +75 -0
  21. sqlleaf/models/query/fetch.py +25 -0
  22. sqlleaf/models/query/for_in.py +31 -0
  23. sqlleaf/models/query/holder.py +2 -2
  24. sqlleaf/models/query/if_.py +35 -0
  25. sqlleaf/models/query/loop.py +31 -0
  26. sqlleaf/models/query/multitable_insert.py +1 -1
  27. sqlleaf/models/query/open.py +54 -0
  28. sqlleaf/models/query/perform.py +12 -0
  29. sqlleaf/models/query/prepare.py +1 -1
  30. sqlleaf/models/query/procedure.py +76 -24
  31. sqlleaf/models/query/set.py +43 -11
  32. sqlleaf/models/query/stage.py +1 -1
  33. sqlleaf/models/query/table.py +10 -10
  34. sqlleaf/models/query/type.py +1 -1
  35. sqlleaf/models/query/unload.py +1 -1
  36. sqlleaf/models/query/user_defined_function.py +20 -28
  37. sqlleaf/models/query/view.py +3 -3
  38. sqlleaf/models/query/while_.py +30 -0
  39. sqlleaf/path.py +8 -8
  40. sqlleaf/processors/collector/classify.py +115 -0
  41. sqlleaf/processors/collector/collector.py +178 -284
  42. sqlleaf/processors/generator/dialects/base.py +50 -5
  43. sqlleaf/processors/generator/generator.py +61 -8
  44. sqlleaf/processors/substitutor/substitute.py +781 -0
  45. sqlleaf/processors/transformer/__init__.py +6 -2
  46. sqlleaf/processors/transformer/assignment.py +18 -0
  47. sqlleaf/processors/transformer/base.py +87 -88
  48. sqlleaf/processors/transformer/call.py +2 -1
  49. sqlleaf/processors/transformer/copy.py +2 -0
  50. sqlleaf/processors/transformer/ctas.py +2 -0
  51. sqlleaf/processors/transformer/delete.py +2 -0
  52. sqlleaf/processors/transformer/execute.py +2 -1
  53. sqlleaf/processors/transformer/execute_dynamic.py +10 -0
  54. sqlleaf/processors/transformer/expressions/__init__.py +5 -1
  55. sqlleaf/processors/transformer/expressions/format.py +251 -0
  56. sqlleaf/processors/transformer/expressions/row.py +12 -12
  57. sqlleaf/processors/transformer/expressions/values.py +115 -89
  58. sqlleaf/processors/transformer/insert.py +3 -1
  59. sqlleaf/processors/transformer/merge.py +2 -0
  60. sqlleaf/processors/transformer/multitable_insert.py +2 -0
  61. sqlleaf/processors/transformer/open.py +78 -0
  62. sqlleaf/processors/transformer/replace.py +3 -1
  63. sqlleaf/processors/transformer/transformer.py +90 -48
  64. sqlleaf/processors/transformer/unload.py +2 -0
  65. sqlleaf/processors/transformer/update.py +2 -0
  66. sqlleaf/processors/transformer/values.py +3 -2
  67. sqlleaf/settings/__init__.py +2 -2
  68. sqlleaf/settings/athena.py +1 -1
  69. sqlleaf/settings/base.py +2 -2
  70. sqlleaf/settings/postgres.py +2 -2
  71. sqlleaf/util/__init__.py +2 -0
  72. sqlleaf/util/expression.py +25 -4
  73. sqlleaf/util/graph.py +3 -3
  74. sqlleaf/util/helpers.py +2 -2
  75. sqlleaf/util/iterators.py +17 -6
  76. sqlleaf-0.0.2.dist-info/METADATA +229 -0
  77. sqlleaf-0.0.2.dist-info/RECORD +125 -0
  78. sqlleaf/processors/collector/substitute.py +0 -396
  79. sqlleaf/processors/transformer/udf.py +0 -235
  80. sqlleaf-0.0.1.dist-info/METADATA +0 -46
  81. sqlleaf-0.0.1.dist-info/RECORD +0 -111
  82. /sqlleaf/{system_tables/load.py → processors/substitutor/__init__.py} +0 -0
  83. {sqlleaf-0.0.1.dist-info → sqlleaf-0.0.2.dist-info}/WHEEL +0 -0
  84. {sqlleaf-0.0.1.dist-info → sqlleaf-0.0.2.dist-info}/licenses/LICENSE +0 -0
  85. {sqlleaf-0.0.1.dist-info → sqlleaf-0.0.2.dist-info}/top_level.txt +0 -0
sqlleaf/__init__.py CHANGED
@@ -2,3 +2,4 @@ from sqlleaf.lineage import Lineage as Lineage
2
2
  from sqlleaf.processors.collector.collector import collect_queries as collect_queries
3
3
  from sqlleaf.processors.generator.generator import generate_lineage_for_query as generate_lineage_for_query
4
4
  from sqlleaf.processors.transformer.transformer import transform_query as transform_query
5
+ from sqlleaf.dialects import PlPgSQL
@@ -0,0 +1 @@
1
+ from sqlleaf.dialects.plpgsql.p_dialect import PlPgSQL
@@ -0,0 +1 @@
1
+ from sqlleaf.dialects.plpgsql import p_classes as pgexp
@@ -146,7 +146,7 @@ class PGGetDiagnostics(exp.Expression):
146
146
  arg_types = {"current": False, "stacked": False, "expressions": True}
147
147
 
148
148
 
149
- class PGOpenCursor(exp.Expression):
149
+ class PGOpen(exp.Expression):
150
150
  """An OPEN cursor statement."""
151
151
  arg_types = {"this": True, "expression": False, "scroll": False, "expressions": False}
152
152
 
@@ -25,7 +25,7 @@ class PlPgSQLGenerator(PostgresGenerator):
25
25
  PGContinue: lambda self, e: self.pgcontinue_sql(e),
26
26
  PGRaise: lambda self, e: self.pgraise_sql(e),
27
27
  PGSqlState: lambda self, e: self.pgsqlstate_sql(e),
28
- PGOpenCursor: lambda self, e: self.pgopencursor_sql(e),
28
+ PGOpen: lambda self, e: self.pgopen_sql(e),
29
29
  PGFetch: lambda self, e: self.pgfetch_sql(e),
30
30
  PGMove: lambda self, e: self.pgmove_sql(e),
31
31
  PGClose: lambda self, e: self.pgclose_sql(e),
@@ -419,7 +419,7 @@ class PlPgSQLGenerator(PostgresGenerator):
419
419
  return f"{self.sql(dir_expr)} {prep} {self.sql(expression.this)}"
420
420
  return self.sql(expression.this)
421
421
 
422
- def pgopencursor_sql(self, expression: PGOpenCursor) -> str:
422
+ def pgopen_sql(self, expression: PGOpen) -> str:
423
423
  # Unbound form: OPEN c [NO|SCROLL] FOR <query>
424
424
  if expression.args.get("expression") is not None:
425
425
  parts: list[str] = ["OPEN", self.sql(expression.this)]
@@ -1053,7 +1053,7 @@ class PlPgSQLParser(PostgresParser):
1053
1053
 
1054
1054
  return args
1055
1055
 
1056
- def _parse_pgopen(self) -> PGOpenCursor:
1056
+ def _parse_pgopen(self) -> PGOpen:
1057
1057
  # Cursor variable identifier
1058
1058
  cursor = self._parse_id_var()
1059
1059
 
@@ -1062,7 +1062,7 @@ class PlPgSQLParser(PostgresParser):
1062
1062
  if self._match(TokenType.L_PAREN, advance=False):
1063
1063
  # Parse cursor call arguments directly (positional, name := value, name => value)
1064
1064
  args = self._parse_cursor_call_args()
1065
- return self.expression(PGOpenCursor(this=cursor, expressions=args))
1065
+ return self.expression(PGOpen(this=cursor, expressions=args))
1066
1066
 
1067
1067
  # Optional [[NO] SCROLL]
1068
1068
  scroll: bool | None = None
@@ -1075,7 +1075,7 @@ class PlPgSQLParser(PostgresParser):
1075
1075
  # If there's no FOR and no SCROLL/NO SCROLL, treat as bound cursor with zero args: OPEN c;
1076
1076
  if not self._match(TokenType.FOR):
1077
1077
  if scroll is None:
1078
- return self.expression(PGOpenCursor(this=cursor))
1078
+ return self.expression(PGOpen(this=cursor))
1079
1079
  # If SCROLL/NO SCROLL was provided, FOR is required
1080
1080
  self._match_expect(TokenType.FOR)
1081
1081
 
@@ -1088,7 +1088,7 @@ class PlPgSQLParser(PostgresParser):
1088
1088
  if query is None:
1089
1089
  self.raise_error("Expected query after OPEN ... FOR")
1090
1090
 
1091
- return self.expression(PGOpenCursor(this=cursor, expression=query, scroll=scroll))
1091
+ return self.expression(PGOpen(this=cursor, expression=query, scroll=scroll))
1092
1092
 
1093
1093
  def _parse_pgfetch(self) -> PGFetch:
1094
1094
  # Parse optional direction, optional preposition (required when direction given), and cursor
sqlleaf/exception.py CHANGED
@@ -6,12 +6,12 @@ ERROR_LEVEL = "stop"
6
6
  logger = logging.getLogger("sqlleaf")
7
7
 
8
8
 
9
- def raise_error(clazz: type, message: str):
9
+ def raise_error(cls: type, message: str):
10
10
  """
11
11
  Raise a custom exception based on the error level.
12
12
  """
13
13
  if ERROR_LEVEL == "stop":
14
- raise clazz(message)
14
+ raise cls(message)
15
15
  elif ERROR_LEVEL == "continue":
16
16
  logger.error(message)
17
17
 
sqlleaf/lineage.py CHANGED
@@ -8,21 +8,11 @@ from sqlleaf import exception, mappings, path, util
8
8
  from sqlleaf.models.hook import Hook
9
9
  from sqlleaf.models.node import EdgeAttributes, GraphAttributes, N
10
10
  from sqlleaf.models.query import (
11
- CopyQuery,
12
- CTASQuery,
13
- InsertQuery,
14
- PutQuery,
15
11
  Q,
16
- TableQuery,
17
- UnloadQuery,
18
- UpdateQuery,
19
- ViewQuery,
20
12
  )
21
13
  from sqlleaf.path import LineagePath
22
14
  from sqlleaf.processors.collector import collector
23
15
  from sqlleaf.processors.generator import generator
24
- from sqlleaf.processors.transformer import transformer
25
- from sqlleaf.typing import SqlObjectType
26
16
 
27
17
  logging.getLogger("sqlglot").setLevel(logging.WARNING)
28
18
  logger = logging.getLogger("sqlleaf")
@@ -35,8 +25,8 @@ class Lineage:
35
25
 
36
26
  def __init__(self):
37
27
  self.graph = new_graph() # The graph that contains all lineage
38
- self.subgraphs: t.List[nx.MultiDiGraph] = [] # The subgraphs that make up the main graph
39
- self.paths: t.Dict[str, t.List[LineagePath]] = {} # The paths throughout the graph
28
+ self.subgraphs: list[nx.MultiDiGraph] = [] # The subgraphs that make up the main graph
29
+ self.paths: dict[str, list[LineagePath]] = {} # The paths throughout the graph
40
30
  self.collected_queries: collector.CollectQueryResult | None = None
41
31
  self.user_defined_hooks: list[Hook] = []
42
32
  self.object_mapping: mappings.ObjectMapping | None = None
@@ -60,11 +50,8 @@ class Lineage:
60
50
  query_holders = parent_holder.get_all_holders()
61
51
 
62
52
  for holder in query_holders:
63
- transformer.transform_query(holder)
64
-
65
- for holder in query_holders:
66
- # Transform and produce lineage only for certain queries
67
- if query_has_lineage(holder.original):
53
+ # Produce lineage only for certain queries
54
+ if generator.query_has_lineage(holder.original):
68
55
  generator.generate_lineage_for_query(holder, graph, self.user_defined_hooks)
69
56
 
70
57
  # Associate the query with the graph even if it has no lineage
@@ -96,14 +83,14 @@ class Lineage:
96
83
 
97
84
  self.graph.add_edges_from(subgraph.edges(data=True))
98
85
 
99
- def get_edges(self) -> t.List[EdgeAttributes]:
86
+ def get_edges(self) -> list[EdgeAttributes]:
100
87
  edges = [data["attrs"] for par, chi, data in self.graph.edges(data=True)]
101
88
  edges = sorted(edges, key=lambda e: (e.parent.full_name, e.child.full_name))
102
89
  for edge in edges:
103
90
  logger.debug(f"Found edge: {edge.parent.friendly_name} -> {edge.child.friendly_name}")
104
91
  return edges
105
92
 
106
- def get_nodes(self) -> t.List[N]:
93
+ def get_nodes(self) -> list[N]:
107
94
  nodes = [data["attrs"] for n, data in self.graph.nodes(data=True)]
108
95
  # TODO: sort on selected index?
109
96
  nodes = sorted(
@@ -111,13 +98,13 @@ class Lineage:
111
98
  )
112
99
  return nodes
113
100
 
114
- def get_original_queries(self) -> t.List[Q]:
101
+ def get_original_queries(self) -> list[Q]:
115
102
  """
116
103
  Get the original queries from the graph.
117
104
  """
118
105
  return [q.original for q in self.graph.graph["attrs"].queries]
119
106
 
120
- def get_transformed_queries(self) -> t.List[Q]:
107
+ def get_transformed_queries(self) -> list[Q]:
121
108
  """
122
109
  Get the transformed queries from the graph.
123
110
  """
@@ -280,47 +267,3 @@ def new_graph() -> nx.MultiDiGraph:
280
267
  A graph has attributes along with its node and edges.
281
268
  """
282
269
  return nx.MultiDiGraph(attrs=GraphAttributes())
283
-
284
-
285
- QUERIES_WITH_LINEAGE = (
286
- CTASQuery,
287
- CopyQuery,
288
- InsertQuery,
289
- PutQuery,
290
- TableQuery,
291
- UnloadQuery,
292
- UpdateQuery,
293
- ViewQuery,
294
- )
295
-
296
-
297
- def query_has_lineage(query: Q) -> bool:
298
- """
299
- Check if a query has lineage within its expressions.
300
-
301
- We distinguish between a query's definition and a query that is called.
302
- That is, in order for a `CREATE FUNCTION` or `CREATE PROCEDURE` to have
303
- lineage, it must be executed by an invoking statement, e.g. `CALL()` or `SELECT UDF()`
304
- Simply having statements inside its definition is not sufficient to produce lineage.
305
- """
306
- has_lineage = True
307
- if not isinstance(query, QUERIES_WITH_LINEAGE):
308
- has_lineage = False
309
- elif isinstance(query, CopyQuery) and query.source_info.type == SqlObjectType.VALUES:
310
- # COPY TO STDOUT VALUES (..) # TODO: this should have lineage
311
- has_lineage = False
312
- elif isinstance(query, CopyQuery) and not query.is_query_active():
313
- has_lineage = False
314
- elif isinstance(query, CTASQuery) and not query.load_data:
315
- # CREATE TABLE WITH NO DATA
316
- has_lineage = False
317
- elif isinstance(query, CTASQuery) and query.source_info.type == SqlObjectType.PREPARED_STATEMENT:
318
- # CREATE TABLE AS EXECUTE
319
- has_lineage = False
320
- elif isinstance(query, TableQuery) and query.property != "external":
321
- # CREATE EXTERNAL TABLE
322
- has_lineage = False
323
-
324
- if not has_lineage:
325
- logger.debug(f"Query type '{query.__class__.__name__}' does NOT have lineage. Skipping.")
326
- return has_lineage
sqlleaf/mappings.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import typing as t
4
5
 
5
6
  from sqlglot import MappingSchema, exp
@@ -24,68 +25,119 @@ from sqlleaf.models.query import (
24
25
  ViewQuery,
25
26
  )
26
27
 
27
- ColumnMapping = t.Union[t.Dict, str, t.List]
28
+ ColumnMapping = t.Union[dict, str, list]
29
+
30
+ logger = logging.getLogger("sqlleaf")
28
31
 
29
32
 
30
33
  class ObjectMapping(MappingSchema):
31
34
  """
32
- Extends sqlglot.MappingSchema to provide additional functionality related to tracking exp.Table
35
+ Contains all the objects that get created, such as tables, procedures, functions, and sequences.
33
36
 
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.
37
+ We extends sqlglot.MappingSchema to provide additional functionality related to tracking these objects,
38
+ while still allowing optimizer inside sqlglot to work (as it needs a mapping of objects to perform functions
39
+ like qualification and simplification).
36
40
  """
37
41
 
38
42
  def __init__(self, dialect: str):
39
- """
40
- Initialize a mapping of tables parts to exp.Table
41
- """
42
43
  super().__init__(
43
44
  dialect=dialect, normalize=False
44
45
  ) # Set `normalize=False` to prevent an unnecessary second parse.
45
46
  self.kind_mapping = {}
46
47
  self.kind_mapping_trie = {}
48
+ # Session/global variables (e.g., SET var = ...)
47
49
  self.session_variables: dict[str, exp.Expr] = {}
50
+ # Block-scoped variables: one dict per active block, innermost scope at the end
51
+ self.variable_scopes: list[dict[str, exp.Expr]] = []
52
+
53
+ # ---- Variable scope API ----
54
+ def push_variable_scope(self) -> None:
55
+ """Enter a new block scope."""
56
+ self.variable_scopes.append({})
57
+
58
+ def pop_variable_scope(self) -> None:
59
+ """Leave the current block scope."""
60
+ if self.variable_scopes:
61
+ self.variable_scopes.pop()
62
+
63
+ def set_variable(self, name: str, value: exp.Expr) -> None:
64
+ """Assign or declare a variable in the current scope."""
65
+ self.variable_scopes[-1][name] = value
66
+ logger.debug(f"Set variable value: {name} := {value.sql()}")
67
+
68
+ def get_variable(self, name: str, raise_on_missing: bool = True) -> exp.Expr | None:
69
+ """Resolve a variable by searching from innermost to outermost scopes, then session vars."""
70
+ for scope in reversed(self.variable_scopes):
71
+ if name in scope:
72
+ return scope[name]
73
+ if raise_on_missing:
74
+ raise exception.MappingError(f"Could not find value for variable: {name}")
75
+ return None
76
+
77
+ def get_variables_for(self, expressions: list[exp.Expr]) -> list[exp.Expr]:
78
+ """
79
+ Given a list of expressions as keys, fetch their values from the variable scope if they exist.
80
+
81
+ Example:
82
+ expressions = [ Literal[42], Column[name] ]
83
+ variable scope = { Column[name] -> Literal['john'] }
84
+ ->
85
+ [ Literal['john'] ]
86
+ """
87
+ resolved = []
88
+ for expr in expressions:
89
+ if isinstance(expr, exp.Column) and not expr.table:
90
+ variable = self.get_variable(expr.name)
91
+ if variable is not None:
92
+ resolved.append(variable.copy())
93
+ continue
94
+ resolved.append(expr)
95
+ return resolved
96
+
97
+ def get_scope(self) -> dict[str, exp.Expr]:
98
+ """Return the current scope dict (innermost)."""
99
+ return self.variable_scopes[-1]
48
100
 
49
101
  def add_database_query(self, query: DatabaseQuery) -> None:
50
- self._add_query(kind="database", query=query, dialect=query.dialect)
102
+ self._add_query(query=query, dialect=query.dialect)
51
103
 
52
104
  def add_prepare_query(self, query: PrepareQuery) -> None:
53
- self._add_query(kind="prepare", query=query, dialect=query.dialect)
105
+ self._add_query(query=query, dialect=query.dialect)
54
106
 
55
107
  def add_procedure_query(self, query: ProcedureQuery) -> None:
56
- self._add_query(kind="procedure", query=query, dialect=query.dialect)
108
+ self._add_query(query=query, dialect=query.dialect)
57
109
 
58
110
  def add_schema_query(self, query: SchemaQuery) -> None:
59
- self._add_query(kind="schema", query=query, dialect=query.dialect)
111
+ self._add_query(query=query, dialect=query.dialect)
60
112
 
61
113
  def add_sequence_query(self, query: SequenceQuery) -> None:
62
- self._add_query(kind="sequence", query=query, dialect=query.dialect)
114
+ self._add_query(query=query, dialect=query.dialect)
63
115
 
64
116
  def add_stage_query(self, query: StageQuery) -> None:
65
- self._add_query(kind="stage", query=query, dialect=query.dialect)
117
+ self._add_query(query=query, dialect=query.dialect)
66
118
 
67
119
  def add_table_query(
68
- self, query: TableQuery | ViewQuery | CTASQuery, column_mapping: t.Optional[ColumnMapping] = None
120
+ self, query: TableQuery | ViewQuery | CTASQuery, column_mapping: ColumnMapping | None = None
69
121
  ) -> None:
70
- self._add_query(kind="table", query=query, column_mapping=column_mapping, dialect=query.dialect)
122
+ self._add_query(query=query, column_mapping=column_mapping, dialect=query.dialect, kind="table")
71
123
 
72
124
  def add_trigger_query(self, query: TriggerQuery) -> None:
73
- self._add_query(kind="trigger", query=query, dialect=query.dialect)
125
+ self._add_query(query=query, dialect=query.dialect)
74
126
 
75
127
  def add_type_query(self, query: TypeQuery) -> None:
76
- self._add_query(kind="type", query=query, dialect=query.dialect)
128
+ self._add_query( query=query, dialect=query.dialect)
77
129
 
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)
130
+ def add_udf_query(self, query: UserDefinedFunctionQuery, column_mapping: ColumnMapping | None = None) -> None:
131
+ self._add_query(query=query, column_mapping=column_mapping, dialect=query.dialect)
80
132
 
81
133
  def _add_query(
82
134
  self,
83
- kind: str,
84
135
  query: Q,
85
- column_mapping: t.Optional[ColumnMapping] = None,
136
+ column_mapping: ColumnMapping | None = None,
86
137
  dialect: DialectType = None,
87
- normalize: t.Optional[bool] = None,
138
+ normalize: bool | None = None,
88
139
  match_depth: bool = False,
140
+ kind: str = ""
89
141
  ) -> None:
90
142
  """
91
143
  Register or update a table. Updates are only performed if a new column mapping is provided.
@@ -100,6 +152,9 @@ class ObjectMapping(MappingSchema):
100
152
  match_depth: whether to enforce that the table must match the schema's depth or not.
101
153
  """
102
154
  # Initialize the structures. These are required by sqlglot.
155
+ if not kind:
156
+ kind = query.kind
157
+
103
158
  if kind not in self.kind_mapping:
104
159
  self.kind_mapping[kind] = {}
105
160
  self.kind_mapping_trie[kind] = new_trie({})
@@ -133,11 +188,11 @@ class ObjectMapping(MappingSchema):
133
188
  def _add_columns_for_table(
134
189
  self,
135
190
  table: exp.Table,
136
- column_mapping: t.Optional[ColumnMapping] = None,
191
+ column_mapping: ColumnMapping | None = None,
137
192
  dialect: DialectType = None,
138
- normalize: t.Optional[bool] = None,
193
+ normalize: bool | None = None,
139
194
  match_depth: bool = False,
140
- ):
195
+ ) -> None:
141
196
  super().add_table(
142
197
  table=table,
143
198
  column_mapping=column_mapping,
@@ -151,7 +206,7 @@ class ObjectMapping(MappingSchema):
151
206
  table: exp.Table,
152
207
  raise_on_missing: bool = True,
153
208
  ensure_data_types: bool = False,
154
- ):
209
+ ) -> None:
155
210
  """
156
211
  A nicer name for the parent's function.
157
212
  """
@@ -184,7 +239,7 @@ class ObjectMapping(MappingSchema):
184
239
 
185
240
  def lookup_udf_query(
186
241
  self, table: exp.Table, raise_on_missing: bool = True
187
- ) -> t.List[UserDefinedFunctionQuery] | None:
242
+ ) -> list[UserDefinedFunctionQuery] | None:
188
243
  # A UDF can have multiple names but different types, thus we return a list
189
244
  return self._lookup_query(kind="udf", object=table, raise_on_missing=raise_on_missing)
190
245
 
@@ -210,7 +265,12 @@ class ObjectMapping(MappingSchema):
210
265
  """
211
266
  if kind not in self.kind_mapping:
212
267
  if raise_on_missing:
213
- raise exception.MappingError(f"Could not find '{object.this}' of type '{kind}' in mapping.")
268
+ if kind == "prepare":
269
+ knd = "PREPARE"
270
+ else:
271
+ knd = "CREATE " + {"udf": "FUNCTION"}.get(kind, kind).upper()
272
+
273
+ raise exception.MappingError(f"Could not find '{object.this}' of type '{kind}' in mapping. Did you forget to define a '{knd}' statement?")
214
274
  return None
215
275
 
216
276
  parts = self.table_parts(object)[0: len(self.supported_table_args)]
@@ -234,7 +294,7 @@ class ObjectMapping(MappingSchema):
234
294
 
235
295
  # Override sqlglot's property. It seems to be buggy when using different dict sizes (catalog, schema, etc)
236
296
  @property
237
- def supported_table_args(self) -> t.Tuple[str, ...]:
297
+ def supported_table_args(self) -> tuple[str, ...]:
238
298
  return exp.TABLE_PARTS
239
299
 
240
300
  def get_table_or_stage(
@@ -248,7 +308,7 @@ class ObjectMapping(MappingSchema):
248
308
  table_expr = exp.Table(this=table)
249
309
 
250
310
  if str(table_expr).startswith("@"):
251
- child_object_query = self.lookup_stage_query(table=table_expr, raise_on_missing=raise_on_missing)
311
+ child_object_query = self.lookup_stage_query(table=table_expr, raise_on_missing=False)
252
312
  else:
253
313
  child_object_query = self.lookup_table_query(table=table_expr, raise_on_missing=raise_on_missing)
254
314
 
@@ -260,7 +320,7 @@ class ObjectMapping(MappingSchema):
260
320
  def lookup_udf_call(
261
321
  self,
262
322
  node: exp.Anonymous,
263
- ) -> t.Optional[UserDefinedFunctionQuery]:
323
+ ) -> UserDefinedFunctionQuery | None:
264
324
  """
265
325
  Looks up the UDF definition for a single exp.Anonymous node.
266
326
  Returns the matched UDF definition, or None if not found.
@@ -275,8 +335,8 @@ class ObjectMapping(MappingSchema):
275
335
 
276
336
 
277
337
  def resolve_overloaded_function(
278
- node: exp.Anonymous, candidates: t.List[UserDefinedFunctionQuery]
279
- ) -> t.Optional[UserDefinedFunctionQuery]:
338
+ node: exp.Anonymous, candidates: list[UserDefinedFunctionQuery]
339
+ ) -> UserDefinedFunctionQuery | None:
280
340
  """
281
341
  Resolves the best function candidate for an overloaded function call.
282
342
  Applies some precedence rules (e.g., type matching, or non-variadic > variadic) but many are currently
@@ -323,7 +383,7 @@ def resolve_overloaded_function(
323
383
  return matches[0]
324
384
 
325
385
 
326
- def match_function_arguments(args: t.List[exp.Expr], candidate: UserDefinedFunctionQuery) -> bool:
386
+ def match_function_arguments(args: list[exp.Expr], candidate: UserDefinedFunctionQuery) -> bool:
327
387
  """
328
388
  Checks if the provided arguments match the function candidate's parameters.
329
389
  Handles exact types, polymorphic types (anyelement, anyarray), and VARIADIC parameters.
sqlleaf/models/context.py CHANGED
@@ -17,7 +17,7 @@ logger = logging.getLogger("sqlleaf")
17
17
 
18
18
  class ScopePositions:
19
19
  def __init__(self):
20
- self.positions: t.Dict[int, t.Dict[int, int]] = {}
20
+ self.positions: dict[int, dict[int, int]] = {}
21
21
 
22
22
  def get_position_for_expr(self, expr: exp.Expr) -> dict[int, int]:
23
23
  return self.positions[id(expr)]
@@ -142,7 +142,7 @@ class EdgeAttributes:
142
142
 
143
143
  class GraphAttributes:
144
144
  def __init__(self):
145
- self.queries: t.List[QueryHolder] = []
145
+ self.queries: list[QueryHolder] = []
146
146
 
147
147
  def add_query_to_graph(self, holder: QueryHolder):
148
148
  self.queries.append(holder)
@@ -1,22 +1,30 @@
1
1
  import typing as t
2
2
 
3
3
  from sqlleaf.models.query.base import Query as Query
4
+ from sqlleaf.models.query.block import BlockQuery as BlockQuery
4
5
  from sqlleaf.models.query.call import CallQuery as CallQuery
5
6
  from sqlleaf.models.query.copy import CopyQuery as CopyQuery
6
7
  from sqlleaf.models.query.ctas import CTASQuery as CTASQuery
7
8
  from sqlleaf.models.query.database import DatabaseQuery as DatabaseQuery
9
+ from sqlleaf.models.query.declare_item import AssignmentQuery as AssignmentQuery
8
10
  from sqlleaf.models.query.delete import DeleteQuery as DeleteQuery
9
11
  from sqlleaf.models.query.execute import ExecuteQuery as ExecuteQuery
12
+ from sqlleaf.models.query.execute_dynamic import ExecuteDynamicQuery as ExecuteDynamicQuery
13
+ from sqlleaf.models.query.fetch import FetchQuery as FetchQuery
14
+ from sqlleaf.models.query.for_in import ForInQuery as ForInQuery
10
15
  from sqlleaf.models.query.holder import QueryHolder as QueryHolder
11
16
  from sqlleaf.models.query.insert import InsertQuery as InsertQuery
17
+ from sqlleaf.models.query.loop import LoopQuery as LoopQuery
12
18
  from sqlleaf.models.query.merge import MergeQuery as MergeQuery
13
19
  from sqlleaf.models.query.multitable_insert import MultitableInsertQuery as MultitableInsertQuery
20
+ from sqlleaf.models.query.open import OpenQuery as OpenQuery
14
21
  from sqlleaf.models.query.prepare import PrepareQuery as PrepareQuery
15
22
  from sqlleaf.models.query.procedure import ProcedureQuery as ProcedureQuery
16
23
  from sqlleaf.models.query.put import PutQuery as PutQuery
17
24
  from sqlleaf.models.query.replace import ReplaceQuery as ReplaceQuery
18
25
  from sqlleaf.models.query.schema import SchemaQuery as SchemaQuery
19
26
  from sqlleaf.models.query.select import SelectQuery as SelectQuery
27
+ from sqlleaf.models.query.perform import PerformQuery as PerformQuery
20
28
  from sqlleaf.models.query.sequence import SequenceQuery as SequenceQuery
21
29
  from sqlleaf.models.query.set import SetQuery as SetQuery
22
30
  from sqlleaf.models.query.stage import StageQuery as StageQuery
@@ -29,5 +37,48 @@ from sqlleaf.models.query.user_defined_function import FunctionParam as Function
29
37
  from sqlleaf.models.query.user_defined_function import UserDefinedFunctionQuery as UserDefinedFunctionQuery
30
38
  from sqlleaf.models.query.values import ValuesQuery as ValuesQuery
31
39
  from sqlleaf.models.query.view import ViewQuery as ViewQuery
40
+ from sqlleaf.models.query.while_ import WhileQuery as WhileQuery
32
41
 
33
42
  Q = t.TypeVar("Q", bound=Query)
43
+
44
+ __all__ = [
45
+ "AssignmentQuery",
46
+ "BlockQuery",
47
+ "CallQuery",
48
+ "CopyQuery",
49
+ "CTASQuery",
50
+ "DatabaseQuery",
51
+ "DeleteQuery",
52
+ "ExecuteDynamicQuery",
53
+ "ExecuteQuery",
54
+ "FetchQuery",
55
+ "ForInQuery",
56
+ "FunctionParam",
57
+ "InsertQuery",
58
+ "LoopQuery",
59
+ "MergeQuery",
60
+ "MultitableInsertQuery",
61
+ "OpenQuery",
62
+ "PerformQuery",
63
+ "PrepareQuery",
64
+ "ProcedureQuery",
65
+ "PutQuery",
66
+ "Q",
67
+ "Query",
68
+ "QueryHolder",
69
+ "ReplaceQuery",
70
+ "SchemaQuery",
71
+ "SelectQuery",
72
+ "SequenceQuery",
73
+ "SetQuery",
74
+ "StageQuery",
75
+ "TableQuery",
76
+ "TriggerQuery",
77
+ "TypeQuery",
78
+ "UnloadQuery",
79
+ "UpdateQuery",
80
+ "UserDefinedFunctionQuery",
81
+ "ValuesQuery",
82
+ "ViewQuery",
83
+ "WhileQuery",
84
+ ]
@@ -25,15 +25,14 @@ class Query:
25
25
  statement_index: int,
26
26
  object_mapping: mappings.ObjectMapping,
27
27
  source_info: SourceInfo | None,
28
- target_info: TargetInfo,
28
+ target_info: TargetInfo | None,
29
29
  skip_type_annotation: bool = False,
30
30
  ):
31
31
  self.kind = self.KIND
32
32
  self.dialect = dialect
33
33
  self.object_mapping = object_mapping
34
- self.parent_query = None
35
34
  self.child_queries = []
36
- self.column_defs: t.List[exp.ColumnDef] = []
35
+ self.column_defs: list[exp.ColumnDef] = []
37
36
  self.property = ""
38
37
 
39
38
  # Remove comments at initialization
@@ -54,10 +53,10 @@ class Query:
54
53
  f"Created new query. Query => {self.__class__.__name__} | SourceType => {self.source_info and self.source_info.type.name} | TargetType => {self.target_info and self.target_info.type.name}"
55
54
  )
56
55
 
57
- def set_holder(self, holder: QueryHolder):
56
+ def set_holder(self, holder: QueryHolder) -> None:
58
57
  self.holder = holder
59
58
 
60
- def _determine_expression_type(self, expr: exp.Expr | t.List[exp.Expr], dialect: str) -> SqlObjectType:
59
+ def _determine_expression_type(self, expr: exp.Expr | list[exp.Expr], dialect: str) -> SqlObjectType:
61
60
  """
62
61
  Determine the type of object that an expression represents.
63
62
  For example, the literal "/tmp/data.csv" is of type FILE; identifier "STDIN" is of type STREAM.
@@ -126,7 +125,7 @@ class Query:
126
125
  def get_transformed_self(self) -> Query | None:
127
126
  return self.holder.transformed
128
127
 
129
- def get_columns_from_target(self) -> t.List[exp.ColumnDef]:
128
+ def get_columns_from_target(self) -> list[exp.ColumnDef]:
130
129
  """
131
130
  Given a query, figure out its target object, including its columns.
132
131
 
@@ -160,7 +159,7 @@ class Query:
160
159
  def _get_column_defs(
161
160
  self,
162
161
  expr: SourceExprType | TargetExprType,
163
- ) -> t.List[exp.ColumnDef]:
162
+ ) -> list[exp.ColumnDef]:
164
163
  """
165
164
  Most of the time, the sources and target are tables.
166
165
  However, with COPY/UNLOAD, they can be files or streams.
@@ -198,19 +197,15 @@ class Query:
198
197
  """
199
198
  Get the statement index for this query (including its parents).
200
199
  """
201
- if self.parent_query:
202
- index = self.parent_query.get_statement_index()
203
- return index + ":" + str(self.statement_index)
204
- else:
205
- return str(self.statement_index)
200
+ return str(self.statement_index)
206
201
 
207
- def get_ctes(self) -> t.List:
202
+ def get_ctes(self) -> list:
208
203
  return []
209
204
 
210
- def get_column_defs(self, include_system: bool = False) -> t.List[exp.ColumnDef]:
205
+ def get_column_defs(self, include_system: bool = False) -> list[exp.ColumnDef]:
211
206
  return self.column_defs
212
207
 
213
- def get_column_names_with_types(self, include_system: bool = False) -> t.Dict[str, str]:
208
+ def get_column_names_with_types(self, include_system: bool = False) -> dict[str, str]:
214
209
  """
215
210
  Used by sqlglot's MappingSchema
216
211
  """
@@ -219,13 +214,9 @@ class Query:
219
214
 
220
215
  @property
221
216
  def id(self) -> str:
222
- return "query:" + util.short_sha256_hash(self.statement.sql() + ":" + str(self.statement_index))
223
-
224
- def add_child_query(self, child_query):
225
- child_query.parent_query = self
226
- self.child_queries.append(child_query)
217
+ return "query:" + util.short_sha256_hash(self.statement.sql(dialect=self.dialect) + ":" + str(self.statement_index))
227
218
 
228
- def get_all_queries(self, types: t.Tuple | None = None):
219
+ def get_all_queries(self, types: tuple | None = None):
229
220
  """
230
221
  Collect all queries (self + children recursively), optionally filtered by type.
231
222
  """