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,148 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from dataclasses import asdict
5
+
6
+ from sqlglot import exp
7
+
8
+ from sqlleaf import util
9
+ from sqlleaf.models.context import GeneratorContext, PositionContext
10
+ from sqlleaf.models.query import Q, QueryHolder
11
+
12
+
13
+ class NodeAttributes:
14
+ KIND: str = ""
15
+ WITH_POSITIONS: bool = False
16
+
17
+ def __init__(
18
+ self,
19
+ gen_ctx: GeneratorContext,
20
+ pos_ctx: PositionContext,
21
+ name: str | None = "",
22
+ ):
23
+ self.expr: exp.Expr = gen_ctx.expr
24
+ self._data_type: exp.DataType | None = gen_ctx.data_type
25
+ self.data_type: str = str(self._data_type) if self._data_type else ""
26
+
27
+ self.name: str = name
28
+ self.kind: str = self.KIND
29
+ self.with_positions: bool = self.WITH_POSITIONS
30
+ self.ctx: PositionContext = pos_ctx
31
+
32
+ # Allows the class to be used a networkx node
33
+ def __hash__(self) -> int:
34
+ return hash(self.full_name)
35
+
36
+ def get_data_type(self) -> exp.DataType:
37
+ assert self._data_type is not None
38
+ return self._data_type
39
+
40
+ def wrap(self, name: str) -> str:
41
+ return f"{self.kind}[{name}]"
42
+
43
+ def fields(self) -> dict[str, str]:
44
+ return {"type": self.data_type}
45
+
46
+ def friendly_fields(self) -> dict[str, str]:
47
+ return {}
48
+
49
+ def get_name(self) -> str:
50
+ return self.name
51
+
52
+ def _build_name(self, fields_dict: dict[str, str], with_positions: bool = False) -> str:
53
+ """Build a formatted name with fields."""
54
+ fields_dict = fields_dict.copy()
55
+ main_parts = [f"name={self.get_name()}"]
56
+
57
+ type_val = fields_dict.pop("type", None)
58
+ if type_val:
59
+ main_parts.append(f"type={type_val}")
60
+
61
+ props = " ".join([f"{k}={v}" for k, v in fields_dict.items() if v])
62
+ if props:
63
+ main_parts.append(f"properties=[{props}]")
64
+
65
+ content = " ".join(main_parts)
66
+ if with_positions:
67
+ content = f"{content} position=[{self.ctx.as_str()}]"
68
+
69
+ return self.wrap(content)
70
+
71
+ @property
72
+ def full_name(self) -> str:
73
+ return self._build_name(self.fields(), with_positions=self.with_positions)
74
+
75
+ @property
76
+ def friendly_name(self) -> str:
77
+ name = self.get_name()
78
+ fields = self.friendly_fields()
79
+
80
+ parts = []
81
+ if name:
82
+ parts.append(name)
83
+
84
+ for k, v in fields.items():
85
+ if v:
86
+ parts.append(f"{k}={v}")
87
+
88
+ content = " ".join(parts)
89
+ return self.wrap(content)
90
+
91
+ @property
92
+ def id(self) -> str:
93
+ name = "node:" + util.short_sha256_hash(self.full_name)
94
+ return name
95
+
96
+ def to_dict(self) -> dict:
97
+ return {
98
+ "id": self.id,
99
+ "kind": self.kind,
100
+ "name": self.name,
101
+ "data_type": self.data_type,
102
+ **self.fields(),
103
+ "position": asdict(self.ctx) if self.with_positions else {},
104
+ }
105
+
106
+
107
+ class EdgeAttributes:
108
+ def __init__(
109
+ self,
110
+ parent: NodeAttributes,
111
+ child: NodeAttributes,
112
+ query: Q,
113
+ path_idx: int,
114
+ ):
115
+ self.parent = parent
116
+ self.child = child
117
+ self.query = query
118
+ self.path_idx = path_idx # The sequence number if a duplicate path
119
+
120
+ @property
121
+ def id(self) -> str:
122
+ edge_id = ":".join([
123
+ str(s)
124
+ for s in [
125
+ self.parent.id,
126
+ self.child.id,
127
+ self.path_idx,
128
+ ]
129
+ ])
130
+ return "edge:" + util.short_sha256_hash(edge_id)
131
+
132
+ def to_dict(self) -> dict:
133
+ result = {
134
+ "id": self.id,
135
+ "source": self.parent.id,
136
+ "target": self.child.id,
137
+ "query": self.query.id,
138
+ "path_index": self.path_idx,
139
+ }
140
+ return result
141
+
142
+
143
+ class GraphAttributes:
144
+ def __init__(self):
145
+ self.queries: t.List[QueryHolder] = []
146
+
147
+ def add_query_to_graph(self, holder: QueryHolder):
148
+ self.queries.append(holder)
@@ -0,0 +1,345 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+
6
+ from sqlglot import exp
7
+ from sqlglot.optimizer import Scope
8
+ from sqlglot.optimizer.scope import ScopeType
9
+
10
+ from sqlleaf import exception
11
+ from sqlleaf.models.context import GeneratorContext, PositionContext
12
+ from sqlleaf.models.node import NodeAttributes
13
+ from sqlleaf.typing import TableOrScopeType, TableSubtype, TableType
14
+
15
+ logger = logging.getLogger("sqlleaf")
16
+
17
+
18
+ class ColumnNode(NodeAttributes):
19
+ KIND = "column"
20
+
21
+ def __init__(
22
+ self,
23
+ catalog: str,
24
+ schema: str,
25
+ table: str,
26
+ column: str,
27
+ gen_ctx: GeneratorContext,
28
+ pos_ctx: PositionContext,
29
+ source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None = None,
30
+ ):
31
+ super().__init__(gen_ctx, pos_ctx, name=column)
32
+ self.catalog = catalog
33
+ self.schema = schema
34
+ self.table = table
35
+ self.member = ""
36
+
37
+ self.parent_kind: str = ""
38
+ self.parent_subkind: str = ""
39
+ self.source_scope: TableOrScopeType | None = None
40
+
41
+ self.set_table_properties(catalog, schema, table, gen_ctx, source=source)
42
+ if source is not None and not isinstance(source, (exp.Subquery, Scope)):
43
+ self._apply_rename(source, gen_ctx.query.dialect)
44
+
45
+ # TODO: new algorithm
46
+ # if table_type == "cte":
47
+ # self.member = gen_ctx.node.recursive_cte_member_kind
48
+
49
+ def to_dict(self):
50
+ d = super().to_dict()
51
+ d.update({
52
+ "catalog": self.catalog,
53
+ "schema": self.schema,
54
+ "table": self.table,
55
+ })
56
+ return d
57
+
58
+ def _apply_rename(self, source: exp.Table | exp.Select | exp.Lateral, dialect: str) -> None:
59
+ """
60
+ Change the column's source table to be its fully qualified name, not its alias,
61
+ so that the ColumnNode is provided complete information.
62
+ """
63
+ column = t.cast(exp.Column, self.expr)
64
+ _c = column.copy()
65
+
66
+ if isinstance(source, exp.Table):
67
+ if source.catalog:
68
+ column.set("catalog", exp.to_identifier(source.catalog))
69
+ if source.db:
70
+ column.set("db", exp.to_identifier(source.db))
71
+ if source.name:
72
+ if dialect == "snowflake":
73
+ if source.this.args.get("quoted", False): # exp.Identifier
74
+ column.set("table", exp.to_identifier(source.name))
75
+ else:
76
+ column.set("table", exp.to_identifier(source.name))
77
+ if _c != column:
78
+ logger.debug(f"Renamed node {_c.sql()} to {column.sql()}")
79
+
80
+ self.expr = column
81
+ self.catalog = column.catalog
82
+ self.schema = column.db
83
+ self.table = column.table
84
+
85
+ def set_table_properties(
86
+ self,
87
+ catalog: str,
88
+ schema: str,
89
+ table: str,
90
+ gen_ctx: GeneratorContext,
91
+ source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None = None,
92
+ ) -> None:
93
+ """
94
+ Figure out the table's kind (view, table, etc) and its subkind (temp, recursive, etc) by
95
+ inspecting the query's scope and the table mapping.
96
+ """
97
+ scope = gen_ctx.scope
98
+
99
+ # logical_source is what we use to determine CTE/UDTF etc.
100
+ logical_source = source
101
+ if isinstance(scope, Scope):
102
+ # Always try to get the Scope from sources if it exists for this alias
103
+ logical_source = scope.sources.get(table) or source
104
+
105
+ if not logical_source:
106
+ # Nested 'rows_from' queries have their aliases in 'references'
107
+ logical_source = dict(scope.references).get(table)
108
+
109
+ self.source_scope = logical_source
110
+
111
+ # If it's in references but NOT in sources, it's a derived table (e.g. ROWS FROM)
112
+ if (
113
+ logical_source is not None
114
+ and isinstance(scope, Scope)
115
+ and table not in scope.sources
116
+ and table in dict(scope.references)
117
+ ):
118
+ self.parent_kind = TableType.DERIVED_TABLE
119
+ return
120
+
121
+ # Determine logical properties (CTE, UDTF, Derived Table)
122
+ if self._set_kind_of_derived_table(logical_source, table, gen_ctx):
123
+ return
124
+
125
+ # Determine physical properties (TABLE, VIEW, etc.)
126
+ self._set_kind_of_physical_table(catalog, schema, table, source or logical_source, gen_ctx)
127
+
128
+ def _set_kind_of_derived_table(
129
+ self,
130
+ source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None,
131
+ table: str,
132
+ gen_ctx: GeneratorContext,
133
+ ) -> bool:
134
+ """
135
+ Identify if the source is a logical entity such as a CTE, UDTF, or derived table and set its properties.
136
+ """
137
+ if isinstance(source, Scope):
138
+ if source.scope_type == ScopeType.CTE:
139
+ self._set_cte_properties(source, table, gen_ctx)
140
+ return True
141
+
142
+ if source.scope_type == ScopeType.DERIVED_TABLE:
143
+ self.parent_kind = TableType.DERIVED_TABLE
144
+ return True
145
+
146
+ if source.scope_type == ScopeType.UDTF:
147
+ self.parent_kind = TableType.UDTF
148
+ return True
149
+
150
+ if isinstance(source, exp.Table) and "rows_from" in source.args:
151
+ self.parent_kind = TableType.DERIVED_TABLE
152
+ return True
153
+
154
+ if isinstance(source, exp.Table) and isinstance(source.this, exp.Anonymous):
155
+ # Table function, e.g. SELECT * FROM FUNCTION()
156
+ self.parent_kind = TableType.UDTF
157
+ return True
158
+
159
+ if isinstance(source, (exp.Select, exp.Subquery)):
160
+ self.parent_kind = TableType.DERIVED_TABLE
161
+ return True
162
+
163
+ return False
164
+
165
+ def _set_cte_properties(self, source: Scope, table: str, gen_ctx: GeneratorContext) -> None:
166
+ """
167
+ Resolve and set properties specific to Common Table Expressions, including recursive and materialized status.
168
+ """
169
+ self.parent_kind = TableType.CTE
170
+ logger.debug("Set node to be a CTE.")
171
+
172
+ # Check if it's recursive or materialized
173
+ if not source.parent:
174
+ return
175
+
176
+ scope = gen_ctx.scope
177
+ if not isinstance(scope, Scope):
178
+ return
179
+
180
+ selected_table, _ = scope.selected_sources.get(table, (None, None))
181
+ if not selected_table:
182
+ # Try the regular sources; may occur when column nested inside subquery references a CTE outside itself
183
+ # In this case, skip it.
184
+ selected_source = scope.sources.get(table)
185
+ if selected_source and selected_source.scope_type == ScopeType.CTE:
186
+ return
187
+ else:
188
+ message = f"Table '{table}' is referenced but there is no FROM containing it."
189
+ raise exception.MappingError(message=message)
190
+
191
+ for cte in source.parent.ctes:
192
+ if cte.alias_or_name == selected_table.name:
193
+ if cte.args.get("materialized"):
194
+ self.parent_subkind = TableSubtype.MATERIALIZED
195
+ else:
196
+ with_ = cte.parent
197
+ if isinstance(with_, exp.With) and with_.recursive:
198
+ logger.debug("Set node to be a recursive CTE.")
199
+ self.parent_subkind = TableSubtype.RECURSIVE
200
+ break
201
+
202
+ def _set_kind_of_physical_table(
203
+ self,
204
+ catalog: str,
205
+ schema: str,
206
+ table: str,
207
+ source: TableOrScopeType | exp.Select | exp.Subquery | exp.Lateral | None,
208
+ gen_ctx: GeneratorContext,
209
+ ) -> None:
210
+ """
211
+ Determine the physical type of the table, such as a base table or view, by resolving its name against the object mapping.
212
+ """
213
+ if isinstance(source, exp.Table):
214
+ tokens = [str(s) for s in source.parts]
215
+ else:
216
+ tokens = [catalog, schema, table]
217
+
218
+ # Get the table type from the mapping
219
+ name = ".".join([tok for tok in tokens if tok])
220
+ if not name:
221
+ self.parent_kind = TableType.TABLE
222
+ return
223
+
224
+ tab = exp.to_table(name, dialect=gen_ctx.query.dialect)
225
+ query = gen_ctx.query.object_mapping.get_table_or_stage(table=tab, raise_on_missing=True)
226
+
227
+ if not query or query.kind == "ctas":
228
+ self.parent_kind = TableType.TABLE
229
+ else:
230
+ self.parent_kind = TableType(query.kind)
231
+
232
+ if query and query.property:
233
+ self.parent_subkind = TableSubtype(query.property)
234
+
235
+ @property
236
+ def friendly_name(self) -> str:
237
+ tokens = [self.catalog, self.schema, self.table, self.name]
238
+ name = ".".join([tok for tok in tokens if tok])
239
+ fields = self.friendly_fields()
240
+
241
+ parts = []
242
+ if name:
243
+ parts.append(name)
244
+
245
+ for k, v in fields.items():
246
+ if v:
247
+ parts.append(f"{k}={v}")
248
+
249
+ content = " ".join(parts)
250
+ return self.wrap(content)
251
+
252
+ def as_table(self) -> exp.Table:
253
+ return exp.table_(catalog=self.catalog, db=self.schema, table=self.table)
254
+
255
+ def fields(self) -> dict[str, str]:
256
+ f = {"kind": self.parent_kind}
257
+ if self.parent_subkind:
258
+ f["subkind"] = self.parent_subkind
259
+
260
+ f |= {
261
+ "table": self.table,
262
+ "schema": self.schema,
263
+ "type": self.data_type,
264
+ }
265
+
266
+ if self.parent_kind == TableType.CTE and self.parent_subkind == TableSubtype.RECURSIVE:
267
+ f["member"] = self.member
268
+
269
+ if self.parent_kind in [TableType.CTE, TableType.DERIVED_TABLE]:
270
+ f["statement"] = str(self.ctx.statement_index)
271
+
272
+ return f
273
+
274
+
275
+ class FileColumnNode(NodeAttributes):
276
+ KIND = "column"
277
+
278
+ def __init__(
279
+ self,
280
+ column: str,
281
+ file_format: str,
282
+ file_path: str,
283
+ gen_ctx: GeneratorContext,
284
+ pos_ctx: PositionContext,
285
+ ):
286
+ super().__init__(gen_ctx, pos_ctx, name=column)
287
+ self.file_format = file_format
288
+ self.file_path = file_path
289
+
290
+ def to_dict(self):
291
+ d = super().to_dict()
292
+ d.update({
293
+ "format": self.file_format,
294
+ "path": self.file_path,
295
+ })
296
+ return d
297
+
298
+ def fields(self) -> dict[str, str]:
299
+ return {
300
+ "type": self.data_type,
301
+ "kind": "file",
302
+ "format": self.file_format,
303
+ "path": self.file_path,
304
+ }
305
+
306
+ def friendly_fields(self) -> dict[str, str]:
307
+ return {"path": self.file_path}
308
+
309
+
310
+ class StageColumnNode(NodeAttributes):
311
+ KIND = "column"
312
+
313
+ def __init__(
314
+ self,
315
+ column: str,
316
+ stage: exp.Var,
317
+ gen_ctx: GeneratorContext,
318
+ pos_ctx: PositionContext,
319
+ path: str = "",
320
+ ):
321
+ super().__init__(gen_ctx, pos_ctx, name=column)
322
+ self.stage = stage.name.removeprefix("@").replace('"', "")
323
+ self.path = path
324
+
325
+ def to_dict(self):
326
+ d = super().to_dict()
327
+ d.update({
328
+ "stage": self.stage,
329
+ "path": self.path,
330
+ })
331
+ return d
332
+
333
+ def fields(self) -> dict[str, str]:
334
+ return {
335
+ "type": self.data_type,
336
+ "kind": "stage",
337
+ "stage": self.stage,
338
+ "path": self.path,
339
+ }
340
+
341
+ def friendly_fields(self) -> dict[str, str]:
342
+ return {
343
+ "stage": self.stage,
344
+ "path": self.path,
345
+ }
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from sqlleaf.models.context import GeneratorContext, PositionContext
6
+ from sqlleaf.models.node import NodeAttributes
7
+
8
+ logger = logging.getLogger("sqlleaf")
9
+
10
+
11
+ class DynamoDbNode(NodeAttributes):
12
+ KIND = "column"
13
+
14
+ def __init__(
15
+ self,
16
+ gen_ctx: GeneratorContext,
17
+ pos_ctx: PositionContext,
18
+ column: str,
19
+ ):
20
+ super().__init__(gen_ctx, pos_ctx, name=column)
21
+ self.path = gen_ctx.expr.name
22
+ self.table_name = self.path.removeprefix("dynamodb://")
23
+
24
+ def to_dict(self):
25
+ d = super().to_dict()
26
+ d.update({"table": self.table_name})
27
+ return d
28
+
29
+ def fields(self) -> dict[str, str]:
30
+ return {
31
+ "type": self.data_type,
32
+ "kind": "dynamodb",
33
+ "table": self.table_name,
34
+ }
35
+
36
+ def friendly_fields(self) -> dict[str, str]:
37
+ return {
38
+ "path": self.path,
39
+ }
@@ -0,0 +1,33 @@
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 FunctionNode(NodeAttributes):
13
+ KIND = "function"
14
+ WITH_POSITIONS = True
15
+
16
+ def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
17
+ expr = t.cast(t.Union[exp.Binary, exp.Func], gen_ctx.expr)
18
+ if isinstance(expr, exp.Binary):
19
+ name = expr.key
20
+ elif isinstance(expr, exp.Case):
21
+ name = "CASE"
22
+ elif isinstance(expr, exp.Anonymous):
23
+ _, name = util.get_udf_name(expr)
24
+ else:
25
+ name = util.calculate_function_name(expr, gen_ctx.query.dialect)
26
+
27
+ super().__init__(gen_ctx, pos_ctx, name=name)
28
+
29
+ def fields(self) -> dict[str, str]:
30
+ return {"type": self.data_type}
31
+
32
+ def get_name(self) -> str:
33
+ return f"{self.name}".upper()
@@ -0,0 +1,21 @@
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 IntervalNode(NodeAttributes):
12
+ KIND = "interval"
13
+ WITH_POSITIONS = True
14
+
15
+ def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
16
+ expr = t.cast(exp.Interval, gen_ctx.expr)
17
+ name = f'"{str(expr.this.name)} {str(expr.unit)}"'
18
+ super().__init__(gen_ctx, pos_ctx, name=name)
19
+
20
+ def fields(self) -> dict[str, str]:
21
+ return {"type": self.data_type}
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+
6
+ from sqlglot import exp
7
+
8
+ from sqlleaf.models.context import GeneratorContext, PositionContext
9
+ from sqlleaf.models.node import NodeAttributes
10
+
11
+ logger = logging.getLogger("sqlleaf")
12
+
13
+
14
+ class JsonPathNode(NodeAttributes):
15
+ KIND = "jsonpath"
16
+
17
+ def __init__(self, gen_ctx: GeneratorContext, pos_ctx: PositionContext):
18
+ expr = t.cast(exp.JSONExtract, gen_ctx.expr)
19
+
20
+ self.selectors = self.json_selectors(expr)
21
+ self.selector = "".join([str(s) for s in self.selectors])
22
+ self.selector_depth = len(self.selectors)
23
+
24
+ super().__init__(gen_ctx, pos_ctx, name=self.selector)
25
+
26
+ def json_selectors(self, expr: exp.JSONExtract):
27
+ """
28
+ Collect all the JSON path elements recursively.
29
+ e.g.
30
+ SELECT my_json -> 'a' -> 'b'
31
+ produces
32
+ ['a', 'b']
33
+ """
34
+ methods = {
35
+ # Mapping of expression -> attribute getter
36
+ exp.JSONPathKey: None,
37
+ exp.JSONPathSubscript: "this",
38
+ }
39
+ elements = list(expr.expression.find_all(tuple(methods)))
40
+
41
+ left = expr.left
42
+ while isinstance(left, (exp.JSONExtract, exp.JSONExtractScalar)):
43
+ elements.extend(list(left.expression.find_all(tuple(methods))))
44
+ left = left.left
45
+ elements.reverse()
46
+
47
+ result = []
48
+ for elem in elements:
49
+ getter = methods[type(elem)]
50
+ result.append(getattr(elem, getter) if getter else elem)
51
+ return result
52
+
53
+ def fields(self) -> dict[str, str]:
54
+ return {"depth": str(self.selector_depth)}
55
+
56
+ def friendly_fields(self) -> dict[str, str]:
57
+ return {}
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.node import NodeAttributes
4
+
5
+
6
+ class LiteralNode(NodeAttributes):
7
+ KIND = "literal"
8
+ WITH_POSITIONS = True
9
+
10
+ def fields(self) -> dict[str, str]:
11
+ return {"type": self.data_type}
12
+
13
+ def get_name(self) -> str:
14
+ if self.expr.is_string:
15
+ return f'"{self.name.replace("'", "")}"'
16
+ else:
17
+ return self.name
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlleaf.models.node import NodeAttributes
4
+
5
+
6
+ class NullNode(NodeAttributes):
7
+ KIND = "null"
8
+ WITH_POSITIONS = True
9
+
10
+ def fields(self) -> dict[str, str]:
11
+ return {"type": "NULL"}
12
+
13
+ def get_name(self) -> str:
14
+ return "NULL"