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,396 @@
1
+ import logging
2
+ import typing as t
3
+
4
+ from sqlglot import exp
5
+
6
+ from sqlleaf import mappings, util, exception
7
+ from sqlleaf.models.query import CallQuery, CTASQuery, ExecuteQuery, FunctionParam, UserDefinedFunctionQuery
8
+
9
+ logger = logging.getLogger("sqlleaf")
10
+
11
+
12
+ def find_arg(args: t.List[exp.Expr], param: FunctionParam, index: int) -> t.Optional[exp.Expr]:
13
+ """
14
+ Finds the argument corresponding to a parameter by its name or position.
15
+
16
+ Example:
17
+ Given query: SELECT my_func(1, y => 2)
18
+ and parameter 'y',
19
+ we get output 2.
20
+ """
21
+ # 1. Named lookup
22
+ param_name = param.name.lower()
23
+ for arg in args:
24
+ if isinstance(arg, exp.Kwarg):
25
+ arg_key = arg.this
26
+ expr = arg.expression
27
+ elif isinstance(arg, exp.PropertyEQ):
28
+ arg_key = arg.left
29
+ expr = arg.right
30
+ else:
31
+ continue
32
+
33
+ is_variadic_call = isinstance(arg_key, exp.Variadic)
34
+ arg_name = (arg_key.this.name if is_variadic_call else arg_key.name).lower()
35
+
36
+ if arg_name == param_name:
37
+ if param.is_variadic:
38
+ if isinstance(expr, exp.Array) and not is_variadic_call and not isinstance(expr, exp.Variadic):
39
+ raise exception.MappingError(
40
+ f"VARIADIC keyword must be used when passing an array to variadic parameter '{param.name}'"
41
+ )
42
+ if isinstance(expr, exp.Variadic):
43
+ return expr.this
44
+ if is_variadic_call:
45
+ return expr
46
+ return expr
47
+
48
+ # 2. Positional lookup
49
+ if index < len(args) and not isinstance(args[index], (exp.Kwarg, exp.PropertyEQ)):
50
+ arg = args[index]
51
+ if not param.is_variadic:
52
+ return arg
53
+
54
+ # If the parameter is variadic and the supplied argument is an array,
55
+ # the keyword 'VARIADIC' must also be supplied.
56
+ if isinstance(arg, exp.Array):
57
+ raise exception.MappingError(
58
+ f"VARIADIC keyword must be used when passing an array to variadic parameter '{param.name}'"
59
+ )
60
+
61
+ if isinstance(arg, exp.Variadic):
62
+ return arg.this
63
+
64
+ # Convert variadic arguments into an array
65
+ # e.g. (1,2,3) -> ARRAY[1,2,3]
66
+ variadic = [a.copy() for a in args[index:] if not isinstance(a, (exp.Kwarg, exp.PropertyEQ))]
67
+ return exp.Array(expressions=variadic) if variadic else None
68
+
69
+ return None
70
+
71
+
72
+ def transform_arguments(
73
+ node: exp.Anonymous, query: UserDefinedFunctionQuery
74
+ ) -> t.Tuple[t.Dict[str, exp.Expr], t.Dict[str, exp.Expr]]:
75
+ """
76
+ Transforms every argument passed to the UDF into the correct format.
77
+
78
+ Example:
79
+ For a UDF with parameters `(x, y)` called as `my_udf(1, y => 2)`, returns:
80
+ param_map = {"x": 1, "y": 2}
81
+ positional_map = {"1": 1, "2": 2}
82
+ """
83
+ param_map = {}
84
+ positional_map = {}
85
+ args = node.expressions
86
+
87
+ logger.debug(f"Transforming arguments for UDF: {query.name}")
88
+
89
+ for i, param in enumerate(query.parameters):
90
+ arg_expr = find_arg(args, param, i) or param.default
91
+
92
+ if arg_expr:
93
+ # Normalize people.* to people
94
+ if isinstance(arg_expr, exp.Column) and arg_expr.this.name == "*":
95
+ arg_expr = exp.column(arg_expr.table)
96
+ elif isinstance(arg_expr, exp.TableColumn):
97
+ arg_expr = exp.column(arg_expr.this)
98
+
99
+ # If the parameter is a table type and the argument is a ROW expression without a cast,
100
+ # we need to add the cast to the expected type.
101
+ if util.is_row_function(arg_expr) and (
102
+ isinstance(param.type, exp.DataType) and param.type.this == exp.DataType.Type.USERDEFINED
103
+ ):
104
+ arg_expr = exp.Cast(
105
+ this=arg_expr,
106
+ to=param.type.copy(),
107
+ )
108
+
109
+ param_map[param.name.lower()] = arg_expr
110
+ positional_map[str(i + 1)] = arg_expr
111
+
112
+ return param_map, positional_map
113
+
114
+
115
+ def substitute_parameters(
116
+ replacement_expr: exp.Expr,
117
+ query: UserDefinedFunctionQuery,
118
+ param_map: t.Dict[str, exp.Expr],
119
+ positional_map: t.Dict[str, exp.Expr],
120
+ ) -> exp.Expr:
121
+ """
122
+ Replaces parameter references within an expression with their corresponding arguments.
123
+
124
+ Example:
125
+ Expression: `SELECT $1 + x`
126
+ Maps: param_map={'x': 10}, positional_map={'1': 5}
127
+ Result: `SELECT 5 + 10`
128
+ """
129
+ # 1. Handle case where the root replacement expression itself needs substitution (e.g., expression is just 'x')
130
+ if isinstance(replacement_expr, exp.Column) and not replacement_expr.table:
131
+ col_name = replacement_expr.this.name.lower()
132
+ if col_name in param_map:
133
+ return param_map[col_name].copy()
134
+
135
+ elif isinstance(replacement_expr, exp.Parameter):
136
+ param_id = replacement_expr.this.name
137
+ if param_id in positional_map:
138
+ return positional_map[param_id].copy()
139
+
140
+ # 2. Walk and replace all references in the expression tree
141
+ for subnode in replacement_expr.walk():
142
+ substitute_parameter_node(subnode, query, param_map, positional_map)
143
+
144
+ return replacement_expr
145
+
146
+
147
+ def substitute_parameter_node(
148
+ node: exp.Expr,
149
+ query: UserDefinedFunctionQuery,
150
+ param_map: t.Dict[str, exp.Expr],
151
+ positional_map: t.Dict[str, exp.Expr],
152
+ ) -> None:
153
+ """
154
+ Replaces parameter references and member access for a single node.
155
+
156
+ Example:
157
+ Before: `x + $1`
158
+ After: `10 + 5` (where x=10, $1=5)
159
+ """
160
+ replacement = None
161
+
162
+ # 1. Determine if this node is a parameter reference and find its replacement
163
+ if isinstance(node, exp.Column) and not node.table:
164
+ col_name = node.this.name.lower()
165
+ if col_name in param_map:
166
+ # In Postgres, if a param name matches a column name in a query with a
167
+ # FROM clause, the column takes precedence.
168
+ parent_select = node.find_ancestor(exp.Select)
169
+ if parent_select:
170
+ for table in parent_select.find_all(exp.Table):
171
+ table_query = query.object_mapping.lookup_table_query(table)
172
+ if table_query and col_name in [c.name for c in table_query.get_column_defs()]:
173
+ return
174
+
175
+ replacement = param_map[col_name]
176
+ elif isinstance(node, exp.Parameter):
177
+ param_id = node.this.name
178
+ if param_id in positional_map:
179
+ replacement = positional_map[param_id]
180
+
181
+ # 2. Apply replacement if found
182
+ if replacement is not None:
183
+ # Avoid replacing the left side of a Dot (member access), as it's handled separately
184
+ if not (isinstance(node.parent, exp.Dot) and node.arg_key == "this"):
185
+ replacement = replacement.copy()
186
+
187
+ # Wrap cast in parentheses if it's being indexed to ensure valid PostgreSQL syntax
188
+ # e.g. (ARRAY[]::numeric[])[i]
189
+ if isinstance(node.parent, exp.Bracket) and isinstance(replacement, exp.Cast):
190
+ replacement = exp.Paren(this=replacement)
191
+
192
+ node.replace(replacement)
193
+
194
+ # 3. Handle member access (e.g. SELECT x.name)
195
+ elif isinstance(node, exp.Dot):
196
+ replace_dot_reference(node, param_map, positional_map)
197
+
198
+
199
+ def replace_dot_reference(
200
+ node: exp.Dot, param_map: t.Dict[str, exp.Expr], positional_map: t.Dict[str, exp.Expr]
201
+ ) -> None:
202
+ """
203
+ Replacements related to member access on parameters (e.g., $1.field -> table.field).
204
+
205
+ Example:
206
+ Before: `$1.name` (where $1 is a table-type argument 'people')
207
+ After: `people.name`
208
+ """
209
+ # 1. Substitute the left side if it's a parameter
210
+ left = node.left
211
+ sub = (
212
+ param_map.get(left.this.name.lower())
213
+ if isinstance(left, exp.Column) and not left.table
214
+ else positional_map.get(left.this.name)
215
+ if isinstance(left, (exp.Parameter, exp.Placeholder))
216
+ else None
217
+ )
218
+ if not sub:
219
+ return
220
+
221
+ # 2. Extract table name from the substituted expression
222
+ table_name = (
223
+ sub.table or (sub.this.name if isinstance(sub.this, exp.Identifier) else None)
224
+ if isinstance(sub, exp.Column)
225
+ else sub.this.name
226
+ if isinstance(sub, exp.TableColumn)
227
+ else sub.name
228
+ if isinstance(sub, exp.Table)
229
+ else None
230
+ )
231
+
232
+ if table_name:
233
+ node.replace(exp.Column(table=exp.Identifier(this=table_name, quoted=False), this=node.right.copy()))
234
+ elif isinstance(sub, exp.Cast):
235
+ # Wrap cast in parentheses so we can look up fields on it, e.g. (ROW(...)::type).field
236
+ node.set("this", exp.Paren(this=sub.copy()))
237
+
238
+
239
+ def substitute_call(query: CallQuery) -> t.List[exp.Expr]:
240
+ """
241
+ Substitutes a CALL statement's arguments into the statmenets inside its body and returns them.
242
+ Circular references in procedure-to-procedure calls could cause infinite recursion.
243
+
244
+ Example:
245
+ CREATE PROCEDURE hello() ... SELECT 3;
246
+ CALL hello();
247
+ ->
248
+ SELECT 3;
249
+ """
250
+ procedure_table = exp.Table(
251
+ this=exp.to_identifier(query.procedure),
252
+ db=exp.to_identifier(query.schema) if query.schema else None,
253
+ )
254
+ matched_proc = query.object_mapping.lookup_procedure_query(procedure_table, raise_on_missing=False)
255
+ if not matched_proc:
256
+ return []
257
+
258
+ logger.debug(f"Substituting CALL query '{query.name}' with procedure body")
259
+
260
+ # Re-use find_arg logic. We don't have an Anonymous node,
261
+ # just a list of args from the CallQuery.
262
+ param_map = {}
263
+ positional_map = {}
264
+ args = query.args
265
+
266
+ for i, param in enumerate(matched_proc.parameters):
267
+ arg_expr = find_arg(args, param, i) or param.default
268
+
269
+ if arg_expr:
270
+ param_map[param.name.lower()] = arg_expr
271
+ positional_map[str(i + 1)] = arg_expr
272
+
273
+ replacement_exprs = []
274
+ for stmt in matched_proc.inner_statements:
275
+ # Procedures can contain multiple statements
276
+ replacement_exprs.append(substitute_parameters(stmt.copy(), None, param_map, positional_map))
277
+
278
+ logger.debug(f"Substituted to {len(replacement_exprs)} statements:")
279
+ for r in replacement_exprs:
280
+ logger.debug(f" {r.sql(dialect=query.dialect)}")
281
+ return replacement_exprs
282
+
283
+
284
+ def substitute_execute_with_plan(
285
+ execute_name: str, execute_arguments: t.List[exp.Literal], object_mapping: mappings.ObjectMapping
286
+ ) -> t.List[exp.Expr]:
287
+ """
288
+ Example:
289
+ PREPARE stmt AS SELECT 1;
290
+ EXECUTE stmt;
291
+ ->
292
+ SELECT 1;
293
+ """
294
+ plan_table = exp.to_table(execute_name)
295
+ matched_prepare = object_mapping.lookup_prepare_query(plan_table, raise_on_missing=False)
296
+
297
+ if not matched_prepare:
298
+ raise exception.MappingError(message=f"Could not find PREPARE statement for plan: {execute_name}")
299
+
300
+ expected = matched_prepare.parameter_count
301
+ actual = len(execute_arguments)
302
+ if expected > 0 and actual != expected:
303
+ raise exception.MappingError(
304
+ message=f"Wrong number of parameters for prepared statement (expected: {expected}, actual: {actual})"
305
+ )
306
+
307
+ logger.debug(f"Substituting EXECUTE query for plan '{execute_name}'")
308
+
309
+ positional_map = {str(i + 1): arg for i, arg in enumerate(execute_arguments)}
310
+ result_expr = substitute_parameters(matched_prepare.statement.copy(), None, {}, positional_map)
311
+
312
+ return [result_expr]
313
+
314
+
315
+ def substitute_execute(query: ExecuteQuery) -> t.List[exp.Expr]:
316
+ """
317
+ Substitutes an EXECUTE statement with the statement of the PREPARE it refers to.
318
+
319
+ Example:
320
+ PREPARE stmt AS SELECT 1;
321
+ EXECUTE stmt;
322
+ ->
323
+ SELECT 1;
324
+ """
325
+ execute_name = query.parameters.name
326
+ execute_args = query.parameters.arguments
327
+
328
+ return substitute_execute_with_plan(execute_name, execute_args, query.object_mapping)
329
+
330
+
331
+ def substitute_create_execute(query: CTASQuery) -> exp.Expr:
332
+ """
333
+ Substitutes 'EXECUTE <plan>' in 'CREATE TABLE AS' with the actual query.
334
+
335
+ Example:
336
+ PREPARE stmt AS SELECT 1 AS one;
337
+ CREATE TABLE t AS EXECUTE stmt;
338
+ ->
339
+ CREATE TABLE t AS SELECT 1 AS one;
340
+ """
341
+ if exec_prop := query.statement.find(exp.ExecuteAsProperty):
342
+ if isinstance(exec_prop.this, exp.Anonymous):
343
+ execute_name = exec_prop.this.this
344
+ execute_args = exec_prop.this.expressions
345
+ else:
346
+ execute_name = exec_prop.this.name
347
+ execute_args = []
348
+
349
+ result_expr = substitute_execute_with_plan(execute_name, execute_args, query.object_mapping)[0]
350
+
351
+ # Replace property with the actual expression
352
+ expression = query.statement
353
+ expression.set("expression", result_expr)
354
+ exec_prop.pop()
355
+
356
+ return expression
357
+
358
+
359
+ def substitute_session_variables(
360
+ stmt: exp.Expr,
361
+ session_variables: dict[str, exp.Expr],
362
+ ) -> exp.Expr:
363
+ """
364
+ Replaces $var Parameter nodes with their stored session variable values,
365
+ and resolves IDENTIFIER($var) DynamicIdentifier nodes to concrete identifiers.
366
+ """
367
+ # 1. Walk and replace exp.Parameter nodes
368
+ for node in stmt.walk():
369
+ if isinstance(node, exp.Parameter):
370
+ var_name = node.this.name.upper()
371
+ if var_name in session_variables:
372
+ logger.debug(f"Substituting session variable: ${var_name}")
373
+ node.replace(session_variables[var_name].copy())
374
+
375
+ # 2. Resolve any remaining DynamicIdentifier nodes that wrap a resolved Literal
376
+ # (e.g. IDENTIFIER('my_table') or IDENTIFIER($var) after step 1)
377
+ # Use transform to handle replacements more robustly during traversal
378
+ def _transform_dynamic_identifiers(node):
379
+ if isinstance(node, exp.DynamicIdentifier):
380
+ inner = node.this
381
+ if isinstance(inner, exp.Literal) and inner.is_string:
382
+ resolved_name = inner.this
383
+
384
+ if isinstance(node.parent, exp.Table):
385
+ # FROM IDENTIFIER($b) → FROM my_table
386
+ # Here we modify the parent Table node's 'this' arg
387
+ node.parent.set("this", exp.to_identifier(resolved_name.upper()))
388
+ return node # It's already disconnected or about to be ignored
389
+ else:
390
+ # SELECT IDENTIFIER($col) → SELECT col_name
391
+ return exp.column(resolved_name.upper())
392
+ return node
393
+
394
+ stmt.transform(_transform_dynamic_identifiers, copy=False)
395
+
396
+ return stmt
File without changes
@@ -0,0 +1,5 @@
1
+ from sqlleaf.processors.generator.dialects.athena import AthenaGenerator as AthenaGenerator
2
+ from sqlleaf.processors.generator.dialects.mysql import MySqlGenerator as MySqlGenerator
3
+ from sqlleaf.processors.generator.dialects.postgres import PostgresGenerator as PostgresGenerator
4
+ from sqlleaf.processors.generator.dialects.redshift import RedshiftGenerator as RedshiftGenerator
5
+ from sqlleaf.processors.generator.dialects.snowflake import SnowflakeGenerator as SnowflakeGenerator
@@ -0,0 +1,45 @@
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 FileColumnNode
10
+ from sqlleaf.processors.generator.dialects.base import BaseGenerator, EdgeToCreate, singledispatchmethodlogger
11
+
12
+ logger = logging.getLogger("sqlleaf")
13
+
14
+
15
+ class AthenaGenerator(BaseGenerator):
16
+ dialect = "athena"
17
+
18
+ @singledispatchmethodlogger
19
+ def process(self, expr: exp.Expr, gen_ctx: GeneratorContext, pos_ctx: PositionContext) -> t.Iterator[EdgeToCreate]:
20
+ yield from super().process(expr, gen_ctx, pos_ctx)
21
+
22
+ @process.register
23
+ def process_location(
24
+ self, expr: exp.LocationProperty, gen_ctx: GeneratorContext, pos_ctx: PositionContext
25
+ ) -> t.Iterator[EdgeToCreate]:
26
+ """
27
+ CREATE EXTERNAL TABLE ... LOCATION
28
+ """
29
+ location = expr.this
30
+ child_node = gen_ctx.child_node
31
+
32
+ # TODO: change to source/target info
33
+
34
+ file_format_property = gen_ctx.query.statement.args["properties"].find(exp.FileFormatProperty)
35
+ file_format = file_format_property.this.name if file_format_property else ""
36
+
37
+ column_node = self.create_node(FileColumnNode(
38
+ column=child_node.name,
39
+ file_format=file_format,
40
+ file_path=location.name,
41
+ gen_ctx=gen_ctx,
42
+ pos_ctx=pos_ctx,
43
+ ))
44
+
45
+ yield EdgeToCreate(column_node, child_node)