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,538 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlglot.generators.postgres import PostgresGenerator
4
+ from sqlleaf.dialects.plpgsql.p_classes import *
5
+
6
+
7
+ class PlPgSQLGenerator(PostgresGenerator):
8
+ # Keep explicit TRANSFORMS to adhere to project guidance for this experimental dialect.
9
+ TRANSFORMS = {
10
+ **PostgresGenerator.TRANSFORMS,
11
+ PGBlock: lambda self, e: self.pgblock_sql(e),
12
+ # PGDeclare: lambda self, e: self.pgdeclare_sql(e),
13
+ exp.Declare: lambda self, e: self.pgdeclare_sql(e),
14
+ PGDeclareItem: lambda self, e: self.pgdeclareitem_sql(e),
15
+ PGTypeRef: lambda self, e: self.pgtyperef_sql(e),
16
+ PGException: lambda self, e: self.pgexception_sql(e),
17
+ PGWhen: lambda self, e: self.pgwhen_sql(e),
18
+ PGCase: lambda self, e: self.pgcase_sql(e),
19
+ PGIf: lambda self, e: self.pgif_sql(e),
20
+ PGIfBranch: lambda self, e: self.pgifbranch_sql(e),
21
+ PGLoop: lambda self, e: self.pgloop_sql(e),
22
+ # PGWhile: lambda self, e: self.pgwhile_sql(e),
23
+ PGReturn: lambda self, e: self.pgreturn_sql(e),
24
+ PGExit: lambda self, e: self.pgexit_sql(e),
25
+ PGContinue: lambda self, e: self.pgcontinue_sql(e),
26
+ PGRaise: lambda self, e: self.pgraise_sql(e),
27
+ PGSqlState: lambda self, e: self.pgsqlstate_sql(e),
28
+ PGOpenCursor: lambda self, e: self.pgopencursor_sql(e),
29
+ PGFetch: lambda self, e: self.pgfetch_sql(e),
30
+ PGMove: lambda self, e: self.pgmove_sql(e),
31
+ PGClose: lambda self, e: self.pgclose_sql(e),
32
+ PGNext: lambda self, e: self.pgnext_sql(e),
33
+ PGPrior: lambda self, e: self.pgprior_sql(e),
34
+ PGFirst: lambda self, e: self.pgfirst_sql(e),
35
+ PGLast: lambda self, e: self.pglast_sql(e),
36
+ PGForward: lambda self, e: self.pgforward_sql(e),
37
+ PGBackward: lambda self, e: self.pgbackward_sql(e),
38
+ PGAbsolute: lambda self, e: self.pgabsolute_sql(e),
39
+ PGRelative: lambda self, e: self.pgrelative_sql(e),
40
+ PGAll: lambda self, e: self.pgall_sql(e),
41
+ PGAssert: lambda self, e: self.pgassert_sql(e),
42
+ PGForIn: lambda self, e: self.pgforin_sql(e),
43
+ PGForEach: lambda self, e: self.pgforeach_sql(e),
44
+ PGExecute: lambda self, e: self.pgexecute_sql(e),
45
+ PGGetDiagnostics: lambda self, e: self.pggetdiagnostics_sql(e),
46
+ PGCursorArg: lambda self, e: self.pgcursorarg_sql(e),
47
+ PGCursorCall: lambda self, e: self.pgcursorcall_sql(e),
48
+ PGFound: lambda self, e: self.pgfound_sql(e),
49
+ PGOthers: lambda self , e: self.pgothers_sql(e),
50
+ PGPerform: lambda self , e: self.pgperform_sql(e),
51
+ PGDelete: lambda self , e: self.pgdelete_sql(e),
52
+ PGUpdate: lambda self , e: self.pgupdate_sql(e),
53
+ PGInto: lambda self , e: self.pginto_sql(e),
54
+ PGReturning: lambda self , e: self.pgreturning_sql(e),
55
+ }
56
+
57
+ def _loop_control_sql(self, keyword: str, expression: exp.Expression) -> str:
58
+ sql = keyword
59
+ if expression.args.get("this") is not None:
60
+ sql += self.seg(self.sql(expression, "this"))
61
+ if expression.args.get("when") is not None:
62
+ sql += self.seg("WHEN")
63
+ sql += self.seg(self.sql(expression, "when"))
64
+ return sql
65
+
66
+ def _current_of_sql(self, expression: exp.Expression) -> str:
67
+ current_of = expression.args.get("current_of")
68
+ if not current_of:
69
+ return ""
70
+ return self.seg(f"WHERE CURRENT OF {self.sql(current_of)}")
71
+
72
+ def _label_names(
73
+ self,
74
+ expression: exp.Expression,
75
+ label_key: str = "label",
76
+ end_label_key: str | None = None,
77
+ ) -> tuple[str, str]:
78
+ """
79
+ Return the leading and trailing labels for a labelled block.
80
+ """
81
+ label = expression.args.get(label_key)
82
+ end_label = expression.args.get(end_label_key or label_key)
83
+ prefix = f"<<{self.sql(label)}>> " if label is not None else ""
84
+ suffix = f" {self.sql(end_label)}" if end_label is not None else ""
85
+ return prefix, suffix
86
+
87
+ def pgupdate_sql(self, expression: PGUpdate) -> str:
88
+ update = exp.Update(**{k: v for k, v in expression.args.items() if k != "current_of"})
89
+ return self.update_sql(update) + self._current_of_sql(expression)
90
+
91
+ def pgdelete_sql(self, expression: PGDelete) -> str:
92
+ delete = exp.Delete(**{k: v for k, v in expression.args.items() if k != "current_of"})
93
+ return self.delete_sql(delete) + self._current_of_sql(expression)
94
+
95
+ # Also expose the auto-discovered method
96
+ def pgblock_sql(self, expression: PGBlock) -> str:
97
+ # Leading label, if any
98
+ label = expression.args.get("label")
99
+ prefix = f"<<{self.sql(label)}>> " if label is not None else ""
100
+ sql = prefix
101
+
102
+ # DECLARE section first if present
103
+ declare = expression.args.get("declare")
104
+ if declare and declare.expressions:
105
+ sql += self.sql(declare) + self.sep()
106
+
107
+ # BEGIN ... statements ... [EXCEPTION ...] END
108
+ sql += "BEGIN"
109
+
110
+ if expression.expressions:
111
+ body_sql = self.expressions(sqls=[self.sql(e) for e in expression.expressions], sep=f";{self.sep()}")
112
+ sql += self.seg(body_sql + ";")
113
+
114
+ ex = expression.args.get("exception")
115
+ if ex is not None:
116
+ sql += self.seg(self.sql(ex))
117
+
118
+ sql += self.seg("END")
119
+ # Trailing END label when present
120
+ if label is not None:
121
+ sql += self.seg(self.sql(label))
122
+ return sql
123
+
124
+ def pgperform_sql(self, expression: PGPerform) -> str:
125
+ select = exp.Select(**expression.args)
126
+ select_sql = self.select_sql(select)
127
+ return select_sql.replace("SELECT", "PERFORM", 1)
128
+
129
+ def pgdeclare_sql(self, expression: exp.Declare) -> str:
130
+ # Render each item separated by semicolons; keep a final trailing semicolon
131
+ if not expression.expressions:
132
+ return "DECLARE"
133
+ items_sql = self.expressions(sqls=[self.sql(item) for item in expression.expressions], sep=f";{self.sep()}")
134
+ return "DECLARE" + self.seg(items_sql + ";")
135
+
136
+ def pgtyperef_sql(self, expression: PGTypeRef) -> str:
137
+ ref = self.sql(expression.this)
138
+ keyword = "%ROWTYPE" if expression.args.get("rowtype") else "%TYPE"
139
+ suffix = "[]" if expression.args.get("array") else ""
140
+ return ref + keyword + suffix
141
+
142
+ def pgdeclareitem_sql(self, expression: PGDeclareItem) -> str:
143
+ name = self.sql(expression.this)
144
+ # Cursor declaration
145
+ if expression.args.get("cursor"):
146
+ parts: list[str] = [name]
147
+ scroll = expression.args.get("scroll")
148
+
149
+ if scroll is True:
150
+ parts.append("SCROLL")
151
+ elif scroll is False:
152
+ parts.append("NO SCROLL")
153
+
154
+ # CURSOR and optional args
155
+ cursor_args = expression.args.get("cursor_args")
156
+ if cursor_args:
157
+ parts.append(self.func("CURSOR", *cursor_args))
158
+ else:
159
+ parts.append("CURSOR")
160
+
161
+ # FOR query
162
+ parts.append("FOR")
163
+ parts.append(self.sql(expression.args.get("query")))
164
+ return " ".join(parts)
165
+
166
+ # Handle alias variant early: name ALIAS FOR $n
167
+ if expression.args.get("alias_for"):
168
+ target = self.sql(expression.args.get("expression"))
169
+ return name + self.seg("ALIAS") + self.seg("FOR") + self.seg(target)
170
+
171
+ kind = expression.args.get("kind")
172
+ typ = self.sql(kind) if kind is not None else ""
173
+ typ_render = typ if isinstance(kind, PGTypeRef) else (typ.upper() if typ else "")
174
+ const_kw = " CONSTANT" if expression.args.get("constant") else ""
175
+ collate = expression.args.get("collate")
176
+ collate_sql = (self.seg("COLLATE") + self.seg(self.sql(collate))) if collate is not None else ""
177
+ not_null_sql = self.seg("NOT NULL") if expression.args.get("not_null") else ""
178
+ init = expression.args.get("expression")
179
+
180
+ if init is not None and expression.args.get("default"):
181
+ return (
182
+ name
183
+ + const_kw
184
+ + (self.seg(typ_render) if typ_render else "")
185
+ + collate_sql
186
+ + not_null_sql
187
+ + self.seg("DEFAULT")
188
+ + self.seg(self.sql(init))
189
+ )
190
+
191
+ if init is not None:
192
+ op = expression.args.get("assign") or ":="
193
+ return (
194
+ name
195
+ + const_kw
196
+ + (self.seg(typ_render) if typ_render else "")
197
+ + collate_sql
198
+ + not_null_sql
199
+ + self.seg(op)
200
+ + self.seg(self.sql(init))
201
+ )
202
+
203
+ return name + const_kw + (self.seg(typ_render) if typ_render else "") + collate_sql + not_null_sql
204
+
205
+ def pgcursorarg_sql(self, expression: PGCursorArg) -> str:
206
+ kind = self.sql(expression.args.get("kind"))
207
+ kind_render = kind.upper() if kind else ""
208
+ return self.sql(expression.this) + (self.seg(kind_render) if kind_render else "")
209
+
210
+ def pgexception_sql(self, expression: "PGException") -> str:
211
+ whens = expression.args.get("whens") or []
212
+ sql = "EXCEPTION"
213
+ if whens:
214
+ sql += self.seg(self.expressions(sqls=[self.sql(w) for w in whens], sep=self.sep()))
215
+ return sql
216
+
217
+ def pgwhen_sql(self, expression: "PGWhen") -> str:
218
+ # Prefer the normal generator dispatch for conditions
219
+ condition = expression.args.get("condition")
220
+ conds = self.sql(condition) if condition is not None else ""
221
+ body = expression.args.get("then") or []
222
+ body_sql = self.expressions(sqls=[self.sql(stmt) for stmt in body], sep=f";{self.sep()}")
223
+ if body_sql:
224
+ body_sql += ";"
225
+ return "WHEN" + (self.seg(conds) if conds else "") + self.seg("THEN") + (self.seg(body_sql) if body_sql else "")
226
+
227
+ def pgif_sql(self, expression: PGIf) -> str:
228
+ branches = expression.args.get("ifs") or []
229
+ sql_parts: list[str] = []
230
+ for i, branch in enumerate(branches):
231
+ keyword = "IF" if i == 0 else "ELSIF"
232
+ sql_parts.append(self._pgifbranch_sql(branch, keyword))
233
+
234
+ default = expression.args.get("default")
235
+ if default is not None:
236
+ else_body = self.expressions(sqls=[self.sql(stmt) for stmt in default], sep=f";{self.sep()}")
237
+ if else_body:
238
+ else_body += ";"
239
+ sql_parts.append("ELSE" + (self.seg(else_body) if else_body else ""))
240
+
241
+ sql_parts.append("END IF")
242
+ return self.sep().join(sql_parts) if self.pretty else " ".join(sql_parts)
243
+
244
+ def pgcase_sql(self, expression: PGCase) -> str:
245
+ branches = expression.args.get("ifs") or []
246
+ sql_parts = ["CASE", *[self.sql(branch) for branch in branches]]
247
+
248
+ default = expression.args.get("default")
249
+ if default is not None:
250
+ else_body = self.expressions(sqls=[self.sql(stmt) for stmt in default], sep=f";{self.sep()}")
251
+ if else_body:
252
+ else_body += ";"
253
+ sql_parts.append("ELSE" + (self.seg(else_body) if else_body else ""))
254
+
255
+ sql_parts.append("END CASE")
256
+ return self.sep().join(sql_parts) if self.pretty else " ".join(sql_parts)
257
+
258
+ def _pgifbranch_sql(self, branch: PGIfBranch, keyword: str) -> str:
259
+ cond = self.sql(branch, "condition")
260
+ body = self.expressions(sqls=[self.sql(stmt) for stmt in (branch.args.get("then") or [])], sep=f";{self.sep()}")
261
+ if body:
262
+ body += ";"
263
+ return keyword + (self.seg(cond) if cond else "") + self.seg("THEN") + (self.seg(body) if body else "")
264
+
265
+ def pgifbranch_sql(self, expression: PGIfBranch) -> str:
266
+ # Standalone rendering (defaults to IF); PGIf normally supplies the keyword.
267
+ return self._pgifbranch_sql(expression, "IF")
268
+
269
+ # Auto-discovered generator for PGOthers
270
+ def pgothers_sql(self, expression: exp.Expression) -> str:
271
+ return "OTHERS"
272
+
273
+ # Auto-discovered generator for PGSqlState
274
+ def pgsqlstate_sql(self, expression: exp.Expression) -> str:
275
+ value = expression.this
276
+ if value is not None:
277
+ return "SQLSTATE" + self.seg(self.sql(value))
278
+ return "SQLSTATE"
279
+
280
+ def pgreturn_sql(self, expression: PGReturn) -> str:
281
+ # Prefer rendering RETURN QUERY if query flag is set
282
+ if expression.args.get("query"):
283
+ q = expression.args.get("this")
284
+ return f"RETURN QUERY {self.sql(q)}"
285
+
286
+ # Render RETURN NEXT when flagged
287
+ if expression.args.get("next"):
288
+ e = expression.args.get("this")
289
+ return f"RETURN NEXT {self.sql(e)}"
290
+
291
+ value = expression.this
292
+ if value is not None:
293
+ return f"RETURN {self.sql(value)}"
294
+ return "RETURN"
295
+
296
+ # Shared Return node renderer (for interoperability)
297
+ def return_sql(self, expression: exp.Return) -> str:
298
+ value = expression.this
299
+ if value is not None:
300
+ return f"RETURN {self.sql(value)}"
301
+ return "RETURN"
302
+
303
+ def pgraise_sql(self, expression: PGRaise) -> str:
304
+ parts: list[str] = ["RAISE"]
305
+
306
+ lvl = expression.args.get("level")
307
+ if lvl is not None:
308
+ # Render level in upper-case for canonical output
309
+ parts.append(self.sql(lvl).upper())
310
+
311
+ # payload: message (+args) | condition | sqlstate | none
312
+ msg = expression.args.get("message")
313
+ cond = expression.args.get("condition")
314
+ state = expression.args.get("sqlstate")
315
+
316
+ if msg is not None:
317
+ seg = self.sql(msg)
318
+ args = expression.args.get("expressions") or []
319
+ if args:
320
+ seg = f"{seg}, " + ", ".join(self.sql(a) for a in args)
321
+ parts.append(seg)
322
+ elif cond is not None:
323
+ parts.append(self.sql(cond))
324
+ elif state is not None:
325
+ # Call dedicated renderer to avoid relying on global transform
326
+ parts.append(self.pgsqlstate_sql(state))
327
+
328
+ using_args = expression.args.get("using") or []
329
+ if using_args:
330
+ parts.append("USING " + ", ".join(self.sql(a) for a in using_args))
331
+
332
+ return " ".join(parts)
333
+
334
+ def pgassert_sql(self, expression: PGAssert) -> str:
335
+ cond = self.sql(expression.args.get("condition"))
336
+ msg = expression.args.get("message")
337
+ if msg is not None:
338
+ return f"ASSERT {cond}, {self.sql(msg)}"
339
+ return f"ASSERT {cond}"
340
+
341
+ def pgloop_sql(self, expression: PGLoop) -> str:
342
+ body = expression.args.get("body") or []
343
+ body_sql = " ".join(f"{self.sql(stmt)};" for stmt in body)
344
+ prefix, suffix = self._label_names(expression, end_label_key="end_label")
345
+ # Render exactly: LOOP <stmts>; END LOOP [label]
346
+ if body_sql:
347
+ return f"{prefix}LOOP {body_sql} END LOOP{suffix}"
348
+ return f"{prefix}LOOP END LOOP{suffix}"
349
+
350
+ def whileblock_sql(self, expression: exp.WhileBlock) -> str:
351
+ cond_sql = self.sql(expression.this)
352
+ body = expression.args.get("body") or []
353
+ body_sql = " ".join(f"{self.sql(stmt)};" for stmt in body)
354
+ prefix, suffix = self._label_names(expression)
355
+ if body_sql:
356
+ return f"{prefix}WHILE {cond_sql} LOOP {body_sql} END LOOP{suffix}"
357
+ return f"{prefix}WHILE {cond_sql} LOOP END LOOP{suffix}"
358
+
359
+ def pgforin_sql(self, expression: PGForIn) -> str:
360
+ target_sql = self.sql(expression.this)
361
+ body_sql = " ".join(f"{self.sql(stmt)};" for stmt in expression.expressions)
362
+ prefix, suffix = self._label_names(expression)
363
+
364
+ query = expression.args.get("query")
365
+ if query is not None:
366
+ header = f"{prefix}FOR {target_sql} IN {self.sql(query)} LOOP"
367
+ else:
368
+ reverse = "REVERSE " if expression.args.get("reverse") else ""
369
+ start_sql = self.sql(expression.args.get("start"))
370
+ end_sql = self.sql(expression.args.get("end"))
371
+ step = expression.args.get("step")
372
+ by_sql = f" BY {self.sql(step)}" if step is not None else ""
373
+ header = f"{prefix}FOR {target_sql} IN {reverse}{start_sql}..{end_sql}{by_sql} LOOP"
374
+
375
+ if body_sql:
376
+ return f"{header} {body_sql} END LOOP{suffix}"
377
+ return f"{header} END LOOP{suffix}"
378
+
379
+ def pgcursorcall_sql(self, expression: PGCursorCall) -> str:
380
+ cursor_sql = self.sql(expression.this)
381
+ args = expression.args.get("expressions")
382
+ if args:
383
+ return f"{cursor_sql}({', '.join(self.sql(arg) for arg in args)})"
384
+ return cursor_sql
385
+
386
+ def pgforeach_sql(self, expression: PGForEach) -> str:
387
+ target_sql = self.sql(expression.this)
388
+ array_sql = self.sql(expression.args.get("expression"))
389
+ body_sql = " ".join(f"{self.sql(stmt)};" for stmt in expression.expressions)
390
+
391
+ slice_expr = expression.args.get("slice")
392
+ slice_sql = f" SLICE {self.sql(slice_expr)}" if slice_expr is not None else ""
393
+
394
+ prefix, suffix = self._label_names(expression)
395
+
396
+ header = f"{prefix}FOREACH {target_sql}{slice_sql} IN ARRAY {array_sql} LOOP"
397
+ if body_sql:
398
+ return f"{header} {body_sql} END LOOP{suffix}"
399
+ return f"{header} END LOOP{suffix}"
400
+
401
+ def pgexit_sql(self, expression: PGExit) -> str:
402
+ return self._loop_control_sql("EXIT", expression)
403
+
404
+ def pgcontinue_sql(self, expression: PGContinue) -> str:
405
+ return self._loop_control_sql("CONTINUE", expression)
406
+
407
+ # Shared node for EXIT/LEAVE
408
+ def leave_sql(self, expression: exp.Leave) -> str:
409
+ return self._loop_control_sql("EXIT", expression)
410
+
411
+ # Shared node for CONTINUE/ITERATE
412
+ def iterate_sql(self, expression: exp.Iterate) -> str:
413
+ return self._loop_control_sql("CONTINUE", expression)
414
+
415
+ def _direction_and_cursor_sql(self, expression: exp.Expression) -> str:
416
+ dir_expr = expression.args.get("direction")
417
+ if dir_expr is not None:
418
+ prep = expression.args.get("preposition") or "FROM"
419
+ return f"{self.sql(dir_expr)} {prep} {self.sql(expression.this)}"
420
+ return self.sql(expression.this)
421
+
422
+ def pgopencursor_sql(self, expression: PGOpenCursor) -> str:
423
+ # Unbound form: OPEN c [NO|SCROLL] FOR <query>
424
+ if expression.args.get("expression") is not None:
425
+ parts: list[str] = ["OPEN", self.sql(expression.this)]
426
+ scroll = expression.args.get("scroll")
427
+ if scroll is True:
428
+ parts.append("SCROLL")
429
+ elif scroll is False:
430
+ parts.append("NO SCROLL")
431
+ parts.append("FOR")
432
+ parts.append(self.sql(expression.args.get("expression")))
433
+ return " ".join(parts)
434
+
435
+ # Bound form: OPEN c or OPEN c(<args>)
436
+ name_sql = self.sql(expression.this)
437
+ if expression.expressions:
438
+ args_sql = ", ".join(self.sql(arg) for arg in expression.expressions)
439
+ name_sql = f"{name_sql}({args_sql})"
440
+ return f"OPEN {name_sql}"
441
+
442
+ def pgfetch_sql(self, expression: PGFetch) -> str:
443
+ targets_sql = ", ".join(self.sql(e) for e in expression.expressions)
444
+ frag = self._direction_and_cursor_sql(expression)
445
+ return f"FETCH {frag} INTO {targets_sql}"
446
+
447
+ def pgmove_sql(self, expression: "PGMove") -> str:
448
+ frag = self._direction_and_cursor_sql(expression)
449
+ return f"MOVE {frag}"
450
+
451
+ def pgclose_sql(self, expression: "PGClose") -> str:
452
+ return f"CLOSE {self.sql(expression.this)}"
453
+
454
+ def pgexecute_sql(self, expression: PGExecute) -> str:
455
+ parts: list[str] = ["EXECUTE", self.sql(expression.this)]
456
+
457
+ targets = expression.args.get("expressions")
458
+ if targets:
459
+ into = "INTO STRICT" if expression.args.get("strict") else "INTO"
460
+ parts.append(f"{into} " + ", ".join(self.sql(t) for t in targets))
461
+
462
+ using_args = expression.args.get("using")
463
+ if using_args:
464
+ parts.append("USING " + ", ".join(self.sql(a) for a in using_args))
465
+
466
+ return " ".join(parts)
467
+
468
+ def pginto_sql(self, expression: exp.Into) -> str:
469
+ strict = " STRICT" if expression.args.get("strict") else ""
470
+
471
+ targets = expression.args.get("expressions")
472
+ if targets:
473
+ target_sql = ", ".join(self.sql(target) for target in targets)
474
+ else:
475
+ target_sql = self.sql(expression, "this")
476
+
477
+ return f"{self.seg('INTO')}{strict} {target_sql}"
478
+
479
+ def pgreturning_sql(self, expression: PGReturning) -> str:
480
+ # Start with base RETURNING expressions
481
+ base = f"{self.seg('RETURNING')} {self.expressions(expression, flat=True)}"
482
+ into = expression.args.get("into")
483
+ if into is not None:
484
+ # Delegate to pginto_sql which prefixes with a space via seg('INTO')
485
+ base += self.sql(into)
486
+ return base
487
+
488
+ def pggetdiagnostics_sql(self, expression: PGGetDiagnostics) -> str:
489
+ parts: list[str] = ["GET"]
490
+ if expression.args.get("current"):
491
+ parts.append("CURRENT")
492
+ elif expression.args.get("stacked"):
493
+ parts.append("STACKED")
494
+ parts.append("DIAGNOSTICS")
495
+
496
+ assigns = expression.expressions or []
497
+ assigns_sql = ", ".join(self.sql(a) for a in assigns)
498
+ parts.append(assigns_sql)
499
+ return " ".join(parts)
500
+
501
+ def pgfound_sql(self, expression: PGFound) -> str:
502
+ return "FOUND"
503
+
504
+ # Direction generators (auto-discovered)
505
+ def pgnext_sql(self, expression: PGNext) -> str:
506
+ return "NEXT"
507
+
508
+ def pgprior_sql(self, expression: PGPrior) -> str:
509
+ return "PRIOR"
510
+
511
+ def pgfirst_sql(self, expression: PGFirst) -> str:
512
+ return "FIRST"
513
+
514
+ def pglast_sql(self, expression: PGLast) -> str:
515
+ return "LAST"
516
+
517
+ def pgforward_sql(self, expression: PGForward) -> str:
518
+ if expression.args.get("all"):
519
+ return "FORWARD ALL"
520
+ if expression.args.get("this") is not None:
521
+ return f"FORWARD {self.sql(expression.this)}"
522
+ return "FORWARD"
523
+
524
+ def pgbackward_sql(self, expression: PGBackward) -> str:
525
+ if expression.args.get("all"):
526
+ return "BACKWARD ALL"
527
+ if expression.args.get("this") is not None:
528
+ return f"BACKWARD {self.sql(expression.this)}"
529
+ return "BACKWARD"
530
+
531
+ def pgabsolute_sql(self, expression: PGAbsolute) -> str:
532
+ return f"ABSOLUTE {self.sql(expression.this)}"
533
+
534
+ def pgrelative_sql(self, expression: PGRelative) -> str:
535
+ return f"RELATIVE {self.sql(expression.this)}"
536
+
537
+ def pgall_sql(self, expression: PGAll) -> str:
538
+ return "ALL"