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,34 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.context import GeneratorContext, PositionContext
4
+ from sqlleaf.models.node import NodeAttributes
5
+
6
+
7
+ class _PivotNode(NodeAttributes):
8
+ def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
9
+ super().__init__(gen_ctx, pos_ctx)
10
+ self.source: str = ""
11
+ self.target: str = ""
12
+
13
+ def set(self, source: str, target: str):
14
+ self.source = source
15
+ self.target = target
16
+
17
+ def fields(self) -> dict[str, str]:
18
+ f = {}
19
+ # Keep empty strings as values to match expected test output
20
+ f["source"] = self.source
21
+ f["target"] = self.target
22
+ f["statement"] = str(self.ctx.statement_index)
23
+ return f
24
+
25
+ def get_name(self) -> str:
26
+ return ""
27
+
28
+
29
+ class PivotNode(_PivotNode):
30
+ KIND = "pivot"
31
+
32
+
33
+ class UnpivotNode(_PivotNode):
34
+ KIND = "unpivot"
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ from sqlglot import exp
6
+
7
+ from sqlleaf.models.context import GeneratorContext, PositionContext
8
+ from sqlleaf.models.node import NodeAttributes
9
+
10
+
11
+ class ProgramNode(NodeAttributes):
12
+ KIND = "program"
13
+ WITH_POSITIONS = True
14
+
15
+ def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
16
+ pos_ctx = pos_ctx.replace(select_index=0) # Prevent duplicate nodes
17
+ copy_stmt = gen_ctx.query.get_original_self().statement
18
+ expr = t.cast(exp.Copy, copy_stmt)
19
+
20
+ # TODO: put these inside COPY to prevent lookups to the original query here
21
+ program = expr.args["params"][0].name
22
+ name, args = (program + " ").split(" ", maxsplit=1)
23
+ super().__init__(gen_ctx, pos_ctx, name=name)
24
+ self.program_args = args.strip()
25
+
26
+ def fields(self) -> dict[str, str]:
27
+ return {"args": f"'{self.program_args}'"}
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.context import GeneratorContext, PositionContext
4
+ from sqlleaf.models.node import NodeAttributes
5
+
6
+
7
+ class SequenceNode(NodeAttributes):
8
+ KIND = "sequence"
9
+
10
+ def __init__(self, name: str, gen_ctx: GeneratorContext, pos_ctx: PositionContext, subkind: str = ""):
11
+ super().__init__(gen_ctx, pos_ctx, name=name)
12
+ self.subkind = subkind
13
+
14
+ def fields(self) -> dict[str, str]:
15
+ f = {"type": self.data_type}
16
+ if self.subkind:
17
+ f["subkind"] = self.subkind
18
+ return f
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.node import NodeAttributes
4
+
5
+
6
+ class StarNode(NodeAttributes):
7
+ KIND = "star"
8
+ WITH_POSITIONS = True
9
+
10
+ def fields(self) -> dict[str, str]:
11
+ return {}
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.context import GeneratorContext, PositionContext
4
+ from sqlleaf.models.node import NodeAttributes
5
+
6
+
7
+ class StreamNode(NodeAttributes):
8
+ KIND = "stream"
9
+
10
+ def __init__(self, name: str, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
11
+ super().__init__(gen_ctx, pos_ctx, name=name)
12
+
13
+ def fields(self) -> dict[str, str]:
14
+ return {}
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.context import GeneratorContext, PositionContext
4
+ from sqlleaf.models.node import NodeAttributes
5
+
6
+
7
+ class UserDefinedFunctionNode(NodeAttributes):
8
+ KIND = "udf"
9
+ WITH_POSITIONS = True
10
+
11
+ def __init__(
12
+ self,
13
+ schema: str,
14
+ gen_ctx: GeneratorContext,
15
+ pos_ctx: PositionContext,
16
+ ):
17
+ super().__init__(gen_ctx, pos_ctx, name=gen_ctx.expr.this)
18
+ self.schema = schema
19
+
20
+ def get_name(self) -> str:
21
+ return str(self.name).upper()
22
+
23
+ @property
24
+ def friendly_name(self) -> str:
25
+ tokens = [self.schema, self.name]
26
+ name = ".".join([tok for tok in tokens if tok]).upper()
27
+ return self.wrap(name)
28
+
29
+ def fields(self) -> dict[str, str]:
30
+ return {
31
+ "type": self.data_type,
32
+ "schema": self.schema.upper() if self.schema else "",
33
+ }
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.node import NodeAttributes
4
+
5
+
6
+ class VarNode(NodeAttributes):
7
+ KIND = "var"
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.node import NodeAttributes
4
+
5
+
6
+ class VariableNode(NodeAttributes):
7
+ KIND = "variable"
8
+ WITH_POSITIONS = True
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ from sqlglot import exp
6
+
7
+ from sqlleaf import util
8
+ from sqlleaf.models.context import GeneratorContext, PositionContext
9
+ from sqlleaf.models.node import NodeAttributes
10
+
11
+
12
+ class WindowNode(NodeAttributes):
13
+ KIND = "window"
14
+
15
+ def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
16
+ expr = t.cast(exp.Window, gen_ctx.expr.this)
17
+ name = util.calculate_function_name(expr, gen_ctx.query.dialect)
18
+
19
+ super().__init__(gen_ctx, pos_ctx, name=name)
20
+
21
+ def fields(self) -> dict[str, str]:
22
+ return {"type": self.data_type}
@@ -0,0 +1,33 @@
1
+ import typing as t
2
+
3
+ from sqlleaf.models.query.base import Query as Query
4
+ from sqlleaf.models.query.call import CallQuery as CallQuery
5
+ from sqlleaf.models.query.copy import CopyQuery as CopyQuery
6
+ from sqlleaf.models.query.ctas import CTASQuery as CTASQuery
7
+ from sqlleaf.models.query.database import DatabaseQuery as DatabaseQuery
8
+ from sqlleaf.models.query.delete import DeleteQuery as DeleteQuery
9
+ from sqlleaf.models.query.execute import ExecuteQuery as ExecuteQuery
10
+ from sqlleaf.models.query.holder import QueryHolder as QueryHolder
11
+ from sqlleaf.models.query.insert import InsertQuery as InsertQuery
12
+ from sqlleaf.models.query.merge import MergeQuery as MergeQuery
13
+ from sqlleaf.models.query.multitable_insert import MultitableInsertQuery as MultitableInsertQuery
14
+ from sqlleaf.models.query.prepare import PrepareQuery as PrepareQuery
15
+ from sqlleaf.models.query.procedure import ProcedureQuery as ProcedureQuery
16
+ from sqlleaf.models.query.put import PutQuery as PutQuery
17
+ from sqlleaf.models.query.replace import ReplaceQuery as ReplaceQuery
18
+ from sqlleaf.models.query.schema import SchemaQuery as SchemaQuery
19
+ from sqlleaf.models.query.select import SelectQuery as SelectQuery
20
+ from sqlleaf.models.query.sequence import SequenceQuery as SequenceQuery
21
+ from sqlleaf.models.query.set import SetQuery as SetQuery
22
+ from sqlleaf.models.query.stage import StageQuery as StageQuery
23
+ from sqlleaf.models.query.table import TableQuery as TableQuery
24
+ from sqlleaf.models.query.trigger import TriggerQuery as TriggerQuery
25
+ from sqlleaf.models.query.type import TypeQuery as TypeQuery
26
+ from sqlleaf.models.query.unload import UnloadQuery as UnloadQuery
27
+ from sqlleaf.models.query.update import UpdateQuery as UpdateQuery
28
+ from sqlleaf.models.query.user_defined_function import FunctionParam as FunctionParam
29
+ from sqlleaf.models.query.user_defined_function import UserDefinedFunctionQuery as UserDefinedFunctionQuery
30
+ from sqlleaf.models.query.values import ValuesQuery as ValuesQuery
31
+ from sqlleaf.models.query.view import ViewQuery as ViewQuery
32
+
33
+ Q = t.TypeVar("Q", bound=Query)
@@ -0,0 +1,248 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+
6
+ if t.TYPE_CHECKING:
7
+ from sqlleaf.models.query import QueryHolder
8
+
9
+ from sqlglot import exp
10
+ from sqlglot.optimizer.annotate_types import annotate_types
11
+
12
+ from sqlleaf import exception, mappings, util
13
+ from sqlleaf.typing import SourceExprType, SourceInfo, SqlObjectType, TargetExprType, TargetInfo
14
+
15
+ logger = logging.getLogger("sqlleaf")
16
+
17
+
18
+ class Query:
19
+ KIND: str = ""
20
+
21
+ def __init__(
22
+ self,
23
+ dialect: str,
24
+ statement: exp.Expr,
25
+ statement_index: int,
26
+ object_mapping: mappings.ObjectMapping,
27
+ source_info: SourceInfo | None,
28
+ target_info: TargetInfo,
29
+ skip_type_annotation: bool = False,
30
+ ):
31
+ self.kind = self.KIND
32
+ self.dialect = dialect
33
+ self.object_mapping = object_mapping
34
+ self.parent_query = None
35
+ self.child_queries = []
36
+ self.column_defs: t.List[exp.ColumnDef] = []
37
+ self.property = ""
38
+
39
+ # Remove comments at initialization
40
+ for expr in statement.walk():
41
+ expr.pop_comments()
42
+
43
+ if skip_type_annotation:
44
+ # Prevent overwriting the types with UNKNOWN if they've already been annotated
45
+ annotate_types(statement, dialect=dialect, schema=object_mapping)
46
+
47
+ self.statement: exp.Expr = statement
48
+ self.statement_index = statement_index # The position of this query within a list of queries
49
+
50
+ self.source_info = source_info
51
+ self.target_info = target_info
52
+
53
+ logger.debug(
54
+ 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
+ )
56
+
57
+ def set_holder(self, holder: QueryHolder):
58
+ self.holder = holder
59
+
60
+ def _determine_expression_type(self, expr: exp.Expr | t.List[exp.Expr], dialect: str) -> SqlObjectType:
61
+ """
62
+ Determine the type of object that an expression represents.
63
+ For example, the literal "/tmp/data.csv" is of type FILE; identifier "STDIN" is of type STREAM.
64
+ """
65
+ if isinstance(expr, exp.Literal):
66
+ _type = SqlObjectType.DYNAMODB if expr.this.lower().startswith("dynamodb://") else SqlObjectType.FILE
67
+
68
+ elif isinstance(expr, exp.Identifier):
69
+ if expr.name.lower() in ["stdin", "stdout"]:
70
+ _type = SqlObjectType.STREAM
71
+ elif expr.name.lower() in ["program"]:
72
+ _type = SqlObjectType.PROGRAM
73
+ else:
74
+ raise exception.InvalidQueryError(f"Unknown object type identifier: {expr.name}")
75
+
76
+ elif isinstance(expr, exp.Var):
77
+ if dialect == "snowflake":
78
+ _type = SqlObjectType.STAGE
79
+ else:
80
+ _type = SqlObjectType.PREPARED_STATEMENT
81
+
82
+ elif isinstance(expr, exp.Table):
83
+ if isinstance(expr.this, exp.Var) and dialect == "snowflake":
84
+ _type = SqlObjectType.STAGE
85
+ else:
86
+ _type = SqlObjectType.TABLE
87
+
88
+ elif isinstance(expr, exp.Anonymous):
89
+ if expr.this == "PROCEDURE":
90
+ # CALL() statement
91
+ _type = SqlObjectType.PROCEDURE
92
+ else:
93
+ # CTAS EXECUTE <statement(args)>
94
+ _type = SqlObjectType.PREPARED_STATEMENT
95
+
96
+ elif dialect == "postgres" and isinstance(expr, exp.Alias):
97
+ if expr.this.name.upper() == "TABLE":
98
+ # PREPARE X AS TABLE Y;
99
+ _type = SqlObjectType.SELECT
100
+
101
+ elif isinstance(expr, exp.Select):
102
+ _type = SqlObjectType.SELECT
103
+
104
+ elif isinstance(expr, exp.Values):
105
+ _type = SqlObjectType.VALUES
106
+
107
+ elif isinstance(expr, exp.OnConflict):
108
+ # temporary; figure out how to handle this case
109
+ _type = SqlObjectType.SET
110
+
111
+ elif isinstance(expr, list) and isinstance(expr[0], exp.EQ):
112
+ # UPDATE .. SET
113
+ _type = SqlObjectType.SET
114
+
115
+ elif isinstance(expr, (exp.Insert, exp.Update, exp.Delete, exp.Merge)):
116
+ _type = SqlObjectType.DML
117
+
118
+ else:
119
+ raise exception.InvalidQueryError(f"Unknown source/target object type in query: {type(expr)}")
120
+
121
+ return _type
122
+
123
+ def get_original_self(self) -> Query:
124
+ return self.holder.original
125
+
126
+ def get_transformed_self(self) -> Query | None:
127
+ return self.holder.transformed
128
+
129
+ def get_columns_from_target(self) -> t.List[exp.ColumnDef]:
130
+ """
131
+ Given a query, figure out its target object, including its columns.
132
+
133
+ The structure is: INSERT INTO <target> .. <source>
134
+
135
+ This is straightforward if 'source' isn't a JOIN: we just use the source object's columns.
136
+ But if it is a JOIN, we use the selected columns rather than the source's columns.
137
+ """
138
+ if self.source_info is None:
139
+ # TODO: temp. This occurs when CALL() invokes an SP with only a SELECT
140
+ # Remove this after CopyQuery refactor is complete
141
+ return []
142
+
143
+ source_expr = self.source_info.expression
144
+ target_expr = self.target_info.expression
145
+ target_type = self.target_info.type
146
+
147
+ if SqlObjectType.type_has_no_column_defs(target_type):
148
+ object_with_columns = source_expr
149
+ elif target_type == SqlObjectType.TABLE:
150
+ object_with_columns = target_expr
151
+ else:
152
+ raise exception.InvalidQueryError(f"Unhandled target object type: {target_type}")
153
+
154
+ column_defs = self._get_column_defs(object_with_columns)
155
+ if not column_defs:
156
+ raise exception.InvalidQueryError(f"Could not find any columns for expression: {object_with_columns}")
157
+
158
+ return column_defs
159
+
160
+ def _get_column_defs(
161
+ self,
162
+ expr: SourceExprType | TargetExprType,
163
+ ) -> t.List[exp.ColumnDef]:
164
+ """
165
+ Most of the time, the sources and target are tables.
166
+ However, with COPY/UNLOAD, they can be files or streams.
167
+
168
+ If the target is not a table and the source is a SELECT,
169
+ there may be a JOIN with many tables as the source.
170
+ """
171
+ if not isinstance(expr, exp.Table):
172
+ # Fall back to the source
173
+ source = self.source_info.expression
174
+ if isinstance(source, exp.Select):
175
+ # TODO: this can't handle functions
176
+ return [
177
+ exp.ColumnDef(this=exp.to_identifier(col.alias_or_name), kind=col.unalias().type)
178
+ for col in source.expressions
179
+ ]
180
+
181
+ table_query = self.object_mapping.get_table_or_stage(expr)
182
+ if not table_query:
183
+ return []
184
+
185
+ return table_query.get_column_defs()
186
+
187
+ def get_target_as_table(self) -> exp.Table:
188
+ """
189
+ For functions that only accept tables.
190
+ """
191
+ if not isinstance(self.target_info.expression, exp.Table):
192
+ raise exception.InvalidQueryError(
193
+ message=f"Expected the target object to be a table but it is a {repr(self.target_info.type)}"
194
+ )
195
+ return self.target_info.expression
196
+
197
+ def get_statement_index(self) -> str:
198
+ """
199
+ Get the statement index for this query (including its parents).
200
+ """
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)
206
+
207
+ def get_ctes(self) -> t.List:
208
+ return []
209
+
210
+ def get_column_defs(self, include_system: bool = False) -> t.List[exp.ColumnDef]:
211
+ return self.column_defs
212
+
213
+ def get_column_names_with_types(self, include_system: bool = False) -> t.Dict[str, str]:
214
+ """
215
+ Used by sqlglot's MappingSchema
216
+ """
217
+ columns = {col.name: str(col.kind) for col in self.get_column_defs(include_system=include_system)}
218
+ return columns
219
+
220
+ @property
221
+ 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)
227
+
228
+ def get_all_queries(self, types: t.Tuple | None = None):
229
+ """
230
+ Collect all queries (self + children recursively), optionally filtered by type.
231
+ """
232
+ queries = [self]
233
+
234
+ for child in self.child_queries:
235
+ queries.extend(child.get_all_queries(types))
236
+
237
+ if types:
238
+ queries = [q for q in queries if isinstance(q, types)]
239
+
240
+ return queries
241
+
242
+ def to_dict(self):
243
+ result = {
244
+ "id": self.id,
245
+ "kind": self.kind,
246
+ "statement_index": self.statement_index,
247
+ }
248
+ return result
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from dataclasses import dataclass
5
+
6
+ import sqlglot
7
+ from sqlglot import exp
8
+
9
+ from sqlleaf import util
10
+ from sqlleaf.models.query.base import Query
11
+ from sqlleaf.typing import SqlObjectType, TargetInfo
12
+
13
+ if t.TYPE_CHECKING:
14
+ from sqlleaf import mappings
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class CallQueryProperties:
19
+ schema: t.Optional[str]
20
+ procedure: str
21
+ args: t.List[exp.Expr]
22
+
23
+ @classmethod
24
+ def from_expression(cls, statement: exp.Command, dialect: str) -> CallQueryProperties:
25
+ # The 'expression' part of the command contains the procedure call
26
+ # e.g., CALL hello('world') -> expression is "hello('world')"
27
+ call_str = statement.args.get("expression").this
28
+
29
+ # Parse the call string as a function call
30
+ inner_expr = sqlglot.parse_one(call_str, read=dialect)
31
+
32
+ schema = None
33
+ if isinstance(inner_expr, exp.Anonymous):
34
+ procedure_name = inner_expr.this
35
+ args = inner_expr.expressions
36
+ elif isinstance(inner_expr, exp.Column):
37
+ procedure_name = inner_expr.name
38
+ args = []
39
+ elif isinstance(inner_expr, exp.Dot):
40
+ schema = inner_expr.left.name
41
+ if isinstance(inner_expr.right, exp.Anonymous):
42
+ procedure_name = inner_expr.right.this
43
+ args = inner_expr.right.expressions
44
+ else:
45
+ procedure_name = inner_expr.right.name
46
+ args = []
47
+ else:
48
+ procedure_name = call_str
49
+ args = []
50
+
51
+ return cls(schema=schema, procedure=procedure_name, args=args)
52
+
53
+
54
+ class CallQuery(Query):
55
+ """
56
+ Holds metadata related to procedure calls (CALL statement).
57
+ """
58
+
59
+ KIND = "call"
60
+
61
+ def __init__(
62
+ self,
63
+ expr: exp.Command,
64
+ dialect: str,
65
+ object_mapping: mappings.ObjectMapping,
66
+ statement_index: int,
67
+ ):
68
+ self.properties = CallQueryProperties.from_expression(expr, dialect)
69
+
70
+ # We can represent the procedure being called as the target
71
+ target_info = TargetInfo(
72
+ expression=exp.Table(
73
+ this=exp.to_identifier(self.properties.procedure),
74
+ db=exp.to_identifier(self.properties.schema) if self.properties.schema else None,
75
+ ),
76
+ type=SqlObjectType.PROCEDURE,
77
+ )
78
+
79
+ super().__init__(
80
+ dialect=dialect,
81
+ statement=expr,
82
+ statement_index=statement_index,
83
+ object_mapping=object_mapping,
84
+ source_info=None,
85
+ target_info=target_info,
86
+ )
87
+
88
+ @property
89
+ def schema(self) -> t.Optional[str]:
90
+ return self.properties.schema
91
+
92
+ @property
93
+ def procedure(self) -> str:
94
+ return self.properties.procedure
95
+
96
+ @property
97
+ def args(self) -> t.List[exp.Expr]:
98
+ return self.properties.args
99
+
100
+ @property
101
+ def name(self):
102
+ return ".".join([var for var in [self.schema, self.procedure] if var])
103
+
104
+ def to_dict(self):
105
+ return {
106
+ "id": self.id,
107
+ "name": self.name,
108
+ "procedure": self.procedure,
109
+ "schema": self.schema,
110
+ "args": [str(arg) for arg in self.args],
111
+ }