bqsqlparse 0.1.0__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.
bqsqlparse/unparse.py ADDED
@@ -0,0 +1,766 @@
1
+ """SQL generation (AST -> BigQuery SQL text)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import singledispatch
6
+ from typing import List
7
+
8
+ from . import nodes as n
9
+ from .lexer import RESERVED
10
+
11
+ _SAFE_IDENT_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
12
+
13
+
14
+ def _ident(name: str) -> str:
15
+ if (
16
+ name
17
+ and all(c in _SAFE_IDENT_CHARS for c in name)
18
+ and not name[0].isdigit()
19
+ and name.upper() not in RESERVED
20
+ ):
21
+ return name
22
+ return f"`{name}`"
23
+
24
+
25
+ def _path(parts: List[str]) -> str:
26
+ return ".".join(_ident(p) for p in parts)
27
+
28
+
29
+ def _str_lit(value: str) -> str:
30
+ escaped = value.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n")
31
+ return f"'{escaped}'"
32
+
33
+
34
+ def to_sql(node: "n.Node") -> str:
35
+ """Render an AST node back to BigQuery SQL."""
36
+ return _sql(node)
37
+
38
+
39
+ @singledispatch
40
+ def _sql(node) -> str:
41
+ raise TypeError(f"Cannot generate SQL for {type(node).__name__}")
42
+
43
+
44
+ # ------------------------------------------------------------------- literals
45
+
46
+ @_sql.register(n.Literal)
47
+ def _(node: n.Literal) -> str:
48
+ if node.kind == "null":
49
+ return "NULL"
50
+ if node.kind == "default":
51
+ return "DEFAULT"
52
+ if node.kind == "bool":
53
+ return "TRUE" if node.value else "FALSE"
54
+ if node.kind == "number":
55
+ return str(node.value)
56
+ if node.kind == "bytes":
57
+ return "b" + _str_lit(str(node.value))
58
+ return _str_lit(str(node.value))
59
+
60
+
61
+ @_sql.register(n.TypedLiteral)
62
+ def _(node: n.TypedLiteral) -> str:
63
+ return f"{node.type_name} {_str_lit(node.value)}"
64
+
65
+
66
+ @_sql.register(n.ColumnRef)
67
+ def _(node: n.ColumnRef) -> str:
68
+ return _path(node.path)
69
+
70
+
71
+ @_sql.register(n.Param)
72
+ def _(node: n.Param) -> str:
73
+ return node.name
74
+
75
+
76
+ @_sql.register(n.Star)
77
+ def _(node: n.Star) -> str:
78
+ out = (_path(node.prefix) + "." if node.prefix else "") + "*"
79
+ if node.except_:
80
+ out += f" EXCEPT ({', '.join(_ident(c) for c in node.except_)})"
81
+ if node.replace:
82
+ items = ", ".join(f"{_sql(r.expr)} AS {_ident(r.name)}" for r in node.replace)
83
+ out += f" REPLACE ({items})"
84
+ return out
85
+
86
+
87
+ # ---------------------------------------------------------------- expressions
88
+
89
+ @_sql.register(n.BinaryOp)
90
+ def _(node: n.BinaryOp) -> str:
91
+ return f"{_sql(node.left)} {node.op} {_sql(node.right)}"
92
+
93
+
94
+ @_sql.register(n.UnaryOp)
95
+ def _(node: n.UnaryOp) -> str:
96
+ if node.op == "NOT":
97
+ return f"NOT {_sql(node.operand)}"
98
+ return f"{node.op}{_sql(node.operand)}"
99
+
100
+
101
+ @_sql.register(n.Between)
102
+ def _(node: n.Between) -> str:
103
+ neg = "NOT " if node.negated else ""
104
+ return f"{_sql(node.expr)} {neg}BETWEEN {_sql(node.low)} AND {_sql(node.high)}"
105
+
106
+
107
+ @_sql.register(n.InExpr)
108
+ def _(node: n.InExpr) -> str:
109
+ neg = "NOT " if node.negated else ""
110
+ if node.unnest is not None:
111
+ return f"{_sql(node.expr)} {neg}IN UNNEST({_sql(node.unnest)})"
112
+ if node.query is not None:
113
+ return f"{_sql(node.expr)} {neg}IN ({_sql(node.query)})"
114
+ vals = ", ".join(_sql(v) for v in node.values or [])
115
+ return f"{_sql(node.expr)} {neg}IN ({vals})"
116
+
117
+
118
+ @_sql.register(n.LikeExpr)
119
+ def _(node: n.LikeExpr) -> str:
120
+ neg = "NOT " if node.negated else ""
121
+ if node.quantifier:
122
+ pats = ", ".join(_sql(p) for p in node.patterns)
123
+ return f"{_sql(node.expr)} {neg}LIKE {node.quantifier} ({pats})"
124
+ return f"{_sql(node.expr)} {neg}LIKE {_sql(node.patterns[0])}"
125
+
126
+
127
+ @_sql.register(n.IsExpr)
128
+ def _(node: n.IsExpr) -> str:
129
+ neg = "NOT " if node.negated else ""
130
+ return f"{_sql(node.expr)} IS {neg}{node.value}"
131
+
132
+
133
+ @_sql.register(n.Case)
134
+ def _(node: n.Case) -> str:
135
+ parts = ["CASE"]
136
+ if node.operand is not None:
137
+ parts.append(_sql(node.operand))
138
+ for w in node.whens:
139
+ parts.append(f"WHEN {_sql(w.condition)} THEN {_sql(w.result)}")
140
+ if node.else_ is not None:
141
+ parts.append(f"ELSE {_sql(node.else_)}")
142
+ parts.append("END")
143
+ return " ".join(parts)
144
+
145
+
146
+ @_sql.register(n.Cast)
147
+ def _(node: n.Cast) -> str:
148
+ fn = "SAFE_CAST" if node.safe else "CAST"
149
+ fmt = f" FORMAT {_sql(node.format)}" if node.format is not None else ""
150
+ return f"{fn}({_sql(node.expr)} AS {_sql(node.to_type)}{fmt})"
151
+
152
+
153
+ @_sql.register(n.Extract)
154
+ def _(node: n.Extract) -> str:
155
+ tz = f" AT TIME ZONE {_sql(node.at_time_zone)}" if node.at_time_zone is not None else ""
156
+ return f"EXTRACT({node.part} FROM {_sql(node.expr)}{tz})"
157
+
158
+
159
+ @_sql.register(n.IntervalExpr)
160
+ def _(node: n.IntervalExpr) -> str:
161
+ to = f" TO {node.to_unit}" if node.to_unit else ""
162
+ return f"INTERVAL {_sql(node.value)} {node.unit}{to}"
163
+
164
+
165
+ @_sql.register(n.ArrayExpr)
166
+ def _(node: n.ArrayExpr) -> str:
167
+ prefix = f"ARRAY<{_sql(node.elem_type)}>" if node.elem_type is not None else ""
168
+ return f"{prefix}[{', '.join(_sql(e) for e in node.elements)}]"
169
+
170
+
171
+ @_sql.register(n.StructExpr)
172
+ def _(node: n.StructExpr) -> str:
173
+ prefix = "STRUCT"
174
+ if node.type_fields is not None:
175
+ inner = ", ".join(
176
+ (f"{_ident(f.name)} " if f.name else "") + _sql(f.type)
177
+ for f in node.type_fields
178
+ )
179
+ prefix = f"STRUCT<{inner}>"
180
+ fields = ", ".join(
181
+ _sql(f.expr) + (f" AS {_ident(f.name)}" if f.name else "")
182
+ for f in node.fields_
183
+ )
184
+ if node.type_fields is None and prefix == "STRUCT":
185
+ return f"STRUCT({fields})"
186
+ return f"{prefix}({fields})"
187
+
188
+
189
+ @_sql.register(n.Subscript)
190
+ def _(node: n.Subscript) -> str:
191
+ if node.mode:
192
+ return f"{_sql(node.base)}[{node.mode}({_sql(node.index)})]"
193
+ return f"{_sql(node.base)}[{_sql(node.index)}]"
194
+
195
+
196
+ @_sql.register(n.FieldAccess)
197
+ def _(node: n.FieldAccess) -> str:
198
+ return f"{_sql(node.base)}.{_ident(node.field_name)}"
199
+
200
+
201
+ @_sql.register(n.NamedArg)
202
+ def _(node: n.NamedArg) -> str:
203
+ return f"{node.name} => {_sql(node.value)}"
204
+
205
+
206
+ @_sql.register(n.FuncCall)
207
+ def _(node: n.FuncCall) -> str:
208
+ name = ".".join(node.name)
209
+ if not node.args and not node.distinct and node.name_str in (
210
+ "CURRENT_DATE", "CURRENT_DATETIME", "CURRENT_TIME", "CURRENT_TIMESTAMP",
211
+ ) and node.over is None:
212
+ return name
213
+ parts = []
214
+ if node.distinct:
215
+ parts.append("DISTINCT")
216
+ parts.append(", ".join(_sql(a) for a in node.args))
217
+ if node.nulls:
218
+ parts.append(f"{node.nulls} NULLS")
219
+ if node.having is not None:
220
+ parts.append(f"HAVING {node.having.kind} {_sql(node.having.expr)}")
221
+ if node.order_by:
222
+ parts.append("ORDER BY " + ", ".join(_sql(o) for o in node.order_by))
223
+ if node.limit is not None:
224
+ parts.append(f"LIMIT {_sql(node.limit)}")
225
+ body = " ".join(p for p in parts if p)
226
+ out = f"{name}({body})"
227
+ if node.over is not None:
228
+ if node.over.is_ref:
229
+ out += f" OVER {node.over.name}"
230
+ else:
231
+ out += f" OVER ({_window_body(node.over)})"
232
+ return out
233
+
234
+
235
+ def _window_body(spec: "n.WindowSpec") -> str:
236
+ parts = []
237
+ if spec.name:
238
+ parts.append(spec.name)
239
+ if spec.partition_by:
240
+ parts.append("PARTITION BY " + ", ".join(_sql(e) for e in spec.partition_by))
241
+ if spec.order_by:
242
+ parts.append("ORDER BY " + ", ".join(_sql(o) for o in spec.order_by))
243
+ if spec.frame is not None:
244
+ f = spec.frame
245
+ if f.end is not None:
246
+ parts.append(f"{f.unit} BETWEEN {_bound(f.start)} AND {_bound(f.end)}")
247
+ else:
248
+ parts.append(f"{f.unit} {_bound(f.start)}")
249
+ return " ".join(parts)
250
+
251
+
252
+ def _bound(b: "n.FrameBound") -> str:
253
+ if b.value is not None:
254
+ return f"{_sql(b.value)} {b.kind}"
255
+ return b.kind
256
+
257
+
258
+ @_sql.register(n.OrderItem)
259
+ def _(node: n.OrderItem) -> str:
260
+ out = _sql(node.expr)
261
+ if node.desc is True:
262
+ out += " DESC"
263
+ elif node.desc is False:
264
+ out += " ASC"
265
+ if node.nulls:
266
+ out += f" NULLS {node.nulls}"
267
+ return out
268
+
269
+
270
+ @_sql.register(n.ScalarSubquery)
271
+ def _(node: n.ScalarSubquery) -> str:
272
+ return f"({_sql(node.query)})"
273
+
274
+
275
+ @_sql.register(n.ArraySubquery)
276
+ def _(node: n.ArraySubquery) -> str:
277
+ return f"ARRAY({_sql(node.query)})"
278
+
279
+
280
+ @_sql.register(n.ExistsSubquery)
281
+ def _(node: n.ExistsSubquery) -> str:
282
+ return f"EXISTS ({_sql(node.query)})"
283
+
284
+
285
+ # ---------------------------------------------------------------------- types
286
+
287
+ @_sql.register(n.TypeNode)
288
+ def _(node: n.TypeNode) -> str:
289
+ if node.name == "ARRAY" and node.element is not None:
290
+ return f"ARRAY<{_sql(node.element)}>"
291
+ if node.name == "RANGE" and node.element is not None:
292
+ return f"RANGE<{_sql(node.element)}>"
293
+ if node.name == "STRUCT":
294
+ if not node.fields_:
295
+ return "STRUCT"
296
+ inner = ", ".join(
297
+ (f"{_ident(f.name)} " if f.name else "") + _sql(f.type)
298
+ for f in node.fields_
299
+ )
300
+ return f"STRUCT<{inner}>"
301
+ if node.params:
302
+ return f"{node.name}({', '.join(node.params)})"
303
+ return node.name
304
+
305
+
306
+ # ---------------------------------------------------------------- query parts
307
+
308
+ @_sql.register(n.SelectItem)
309
+ def _(node: n.SelectItem) -> str:
310
+ out = _sql(node.expr)
311
+ if node.alias:
312
+ out += f" AS {_ident(node.alias)}"
313
+ return out
314
+
315
+
316
+ @_sql.register(n.Select)
317
+ def _(node: n.Select) -> str:
318
+ parts = ["SELECT"]
319
+ if node.distinct:
320
+ parts.append("DISTINCT")
321
+ if node.as_mode:
322
+ parts.append(f"AS {node.as_mode}")
323
+ parts.append(", ".join(_sql(i) for i in node.items))
324
+ if node.from_ is not None:
325
+ parts.append(f"FROM {_sql(node.from_)}")
326
+ if node.where is not None:
327
+ parts.append(f"WHERE {_sql(node.where)}")
328
+ if node.group_by is not None:
329
+ parts.append(_sql(node.group_by))
330
+ if node.having is not None:
331
+ parts.append(f"HAVING {_sql(node.having)}")
332
+ if node.qualify is not None:
333
+ parts.append(f"QUALIFY {_sql(node.qualify)}")
334
+ if node.windows:
335
+ wins = ", ".join(f"{w.name} AS ({_window_body(w.spec)})" for w in node.windows)
336
+ parts.append(f"WINDOW {wins}")
337
+ return " ".join(parts)
338
+
339
+
340
+ @_sql.register(n.GroupBy)
341
+ def _(node: n.GroupBy) -> str:
342
+ if node.kind == "all":
343
+ return "GROUP BY ALL"
344
+ if node.kind == "rollup":
345
+ return f"GROUP BY ROLLUP({', '.join(_sql(e) for e in node.exprs)})"
346
+ if node.kind == "cube":
347
+ return f"GROUP BY CUBE({', '.join(_sql(e) for e in node.exprs)})"
348
+ if node.kind == "sets":
349
+ sets = ", ".join("(" + ", ".join(_sql(e) for e in s) + ")" for s in node.sets)
350
+ return f"GROUP BY GROUPING SETS ({sets})"
351
+ return "GROUP BY " + ", ".join(_sql(e) for e in node.exprs)
352
+
353
+
354
+ @_sql.register(n.SetOp)
355
+ def _(node: n.SetOp) -> str:
356
+ mode = "ALL" if node.all else "DISTINCT"
357
+ return f"{_sql(node.left)} {node.op} {mode} {_sql(node.right)}"
358
+
359
+
360
+ @_sql.register(n.Query)
361
+ def _(node: n.Query) -> str:
362
+ parts = []
363
+ if node.ctes:
364
+ rec = "RECURSIVE " if node.recursive else ""
365
+ ctes = ", ".join(f"{_ident(c.name)} AS ({_sql(c.query)})" for c in node.ctes)
366
+ parts.append(f"WITH {rec}{ctes}")
367
+ body = _sql(node.body)
368
+ if isinstance(node.body, n.Query):
369
+ body = f"({body})"
370
+ parts.append(body)
371
+ if node.order_by:
372
+ parts.append("ORDER BY " + ", ".join(_sql(o) for o in node.order_by))
373
+ if node.limit is not None:
374
+ parts.append(f"LIMIT {_sql(node.limit)}")
375
+ if node.offset is not None:
376
+ parts.append(f"OFFSET {_sql(node.offset)}")
377
+ return " ".join(parts)
378
+
379
+
380
+ # ----------------------------------------------------------------- FROM items
381
+
382
+ @_sql.register(n.TableRef)
383
+ def _(node: n.TableRef) -> str:
384
+ out = _path(node.path)
385
+ if node.system_time is not None:
386
+ out += f" FOR SYSTEM_TIME AS OF {_sql(node.system_time)}"
387
+ if node.alias:
388
+ out += f" AS {_ident(node.alias)}"
389
+ if node.sample is not None:
390
+ out += f" TABLESAMPLE {node.sample.method} ({_sql(node.sample.value)} {node.sample.unit})"
391
+ return out
392
+
393
+
394
+ @_sql.register(n.SubqueryRef)
395
+ def _(node: n.SubqueryRef) -> str:
396
+ out = f"({_sql(node.query)})"
397
+ if node.alias:
398
+ out += f" AS {_ident(node.alias)}"
399
+ return out
400
+
401
+
402
+ @_sql.register(n.UnnestRef)
403
+ def _(node: n.UnnestRef) -> str:
404
+ out = f"UNNEST({_sql(node.expr)})"
405
+ if node.alias:
406
+ out += f" AS {_ident(node.alias)}"
407
+ if node.with_offset:
408
+ out += " WITH OFFSET"
409
+ if node.offset_alias:
410
+ out += f" AS {_ident(node.offset_alias)}"
411
+ return out
412
+
413
+
414
+ @_sql.register(n.TableFuncRef)
415
+ def _(node: n.TableFuncRef) -> str:
416
+ out = f"{'.'.join(node.name)}({', '.join(_sql(a) for a in node.args)})"
417
+ if node.alias:
418
+ out += f" AS {_ident(node.alias)}"
419
+ return out
420
+
421
+
422
+ @_sql.register(n.Join)
423
+ def _(node: n.Join) -> str:
424
+ if node.kind == "CROSS" and node.on is None and not node.using:
425
+ return f"{_sql(node.left)} CROSS JOIN {_sql(node.right)}"
426
+ kw = "JOIN" if node.kind == "INNER" else f"{node.kind} JOIN"
427
+ out = f"{_sql(node.left)} {kw} {_sql(node.right)}"
428
+ if node.on is not None:
429
+ out += f" ON {_sql(node.on)}"
430
+ elif node.using:
431
+ out += f" USING ({', '.join(_ident(c) for c in node.using)})"
432
+ return out
433
+
434
+
435
+ @_sql.register(n.PivotRef)
436
+ def _(node: n.PivotRef) -> str:
437
+ aggs = ", ".join(
438
+ _sql(a.func) + (f" AS {_ident(a.alias)}" if a.alias else "")
439
+ for a in node.aggregates
440
+ )
441
+ vals = ", ".join(
442
+ _sql(v.value) + (f" AS {_ident(v.alias)}" if v.alias else "")
443
+ for v in node.in_values
444
+ )
445
+ out = f"{_sql(node.input)} PIVOT({aggs} FOR {_sql(node.for_col)} IN ({vals}))"
446
+ if node.alias:
447
+ out += f" AS {_ident(node.alias)}"
448
+ return out
449
+
450
+
451
+ @_sql.register(n.UnpivotRef)
452
+ def _(node: n.UnpivotRef) -> str:
453
+ nulls = ""
454
+ if node.include_nulls is True:
455
+ nulls = " INCLUDE NULLS"
456
+ elif node.include_nulls is False:
457
+ nulls = " EXCLUDE NULLS"
458
+ multi = len(node.value_columns) > 1
459
+ vcols = "(" + ", ".join(node.value_columns) + ")" if multi else node.value_columns[0]
460
+ groups = []
461
+ for g in node.groups:
462
+ cols = ", ".join(_path(c) for c in g.columns)
463
+ if multi:
464
+ cols = f"({cols})"
465
+ if g.label is not None:
466
+ cols += f" AS {_sql(g.label)}"
467
+ groups.append(cols)
468
+ out = (f"{_sql(node.input)} UNPIVOT{nulls}({vcols} FOR {node.name_column} "
469
+ f"IN ({', '.join(groups)}))")
470
+ if node.alias:
471
+ out += f" AS {_ident(node.alias)}"
472
+ return out
473
+
474
+
475
+ # ----------------------------------------------------------------- statements
476
+
477
+ @_sql.register(n.CreateTableAsSelect)
478
+ def _(node: n.CreateTableAsSelect) -> str:
479
+ parts = ["CREATE"]
480
+ if node.replace:
481
+ parts.append("OR REPLACE")
482
+ if node.temp:
483
+ parts.append("TEMP")
484
+ parts.append(node.kind)
485
+ if node.if_not_exists:
486
+ parts.append("IF NOT EXISTS")
487
+ parts.append(_path(node.name))
488
+ if node.columns:
489
+ cols = ", ".join(
490
+ _ident(c.name) + (f" {_sql(c.type)}" if c.type is not None else "")
491
+ for c in node.columns
492
+ )
493
+ parts.append(f"({cols})")
494
+ if node.partition_by is not None:
495
+ parts.append(f"PARTITION BY {_sql(node.partition_by)}")
496
+ if node.cluster_by:
497
+ parts.append("CLUSTER BY " + ", ".join(_sql(e) for e in node.cluster_by))
498
+ if node.options_sql:
499
+ parts.append(f"OPTIONS{node.options_sql}")
500
+ if node.query is not None:
501
+ parts.append(f"AS {_sql(node.query)}")
502
+ return " ".join(parts)
503
+
504
+
505
+ @_sql.register(n.InsertStmt)
506
+ def _(node: n.InsertStmt) -> str:
507
+ parts = [f"INSERT INTO {_path(node.table)}"]
508
+ if node.columns:
509
+ parts.append("(" + ", ".join(_ident(c) for c in node.columns) + ")")
510
+ if node.values:
511
+ rows = ", ".join("(" + ", ".join(_sql(v) for v in row) + ")" for row in node.values)
512
+ parts.append(f"VALUES {rows}")
513
+ elif node.query is not None:
514
+ parts.append(_sql(node.query))
515
+ return " ".join(parts)
516
+
517
+
518
+ @_sql.register(n.UpdateStmt)
519
+ def _(node: n.UpdateStmt) -> str:
520
+ parts = [f"UPDATE {_path(node.table)}"]
521
+ if node.alias:
522
+ parts.append(f"AS {_ident(node.alias)}")
523
+ sets = ", ".join(f"{_path(a.target)} = {_sql(a.value)}" for a in node.assignments)
524
+ parts.append(f"SET {sets}")
525
+ if node.from_ is not None:
526
+ parts.append(f"FROM {_sql(node.from_)}")
527
+ if node.where is not None:
528
+ parts.append(f"WHERE {_sql(node.where)}")
529
+ return " ".join(parts)
530
+
531
+
532
+ @_sql.register(n.DeleteStmt)
533
+ def _(node: n.DeleteStmt) -> str:
534
+ parts = [f"DELETE FROM {_path(node.table)}"]
535
+ if node.alias:
536
+ parts.append(f"AS {_ident(node.alias)}")
537
+ if node.where is not None:
538
+ parts.append(f"WHERE {_sql(node.where)}")
539
+ return " ".join(parts)
540
+
541
+
542
+ @_sql.register(n.MergeStmt)
543
+ def _(node: n.MergeStmt) -> str:
544
+ parts = [f"MERGE INTO {_path(node.target)}"]
545
+ if node.alias:
546
+ parts.append(f"AS {_ident(node.alias)}")
547
+ parts.append(f"USING {_sql(node.source)}")
548
+ parts.append(f"ON {_sql(node.on)}")
549
+ for w in node.whens:
550
+ kind = {
551
+ "MATCHED": "WHEN MATCHED",
552
+ "NOT_MATCHED": "WHEN NOT MATCHED",
553
+ "NOT_MATCHED_BY_SOURCE": "WHEN NOT MATCHED BY SOURCE",
554
+ }[w.match_kind]
555
+ if w.condition is not None:
556
+ kind += f" AND {_sql(w.condition)}"
557
+ kind += " THEN "
558
+ if w.action_kind == "UPDATE":
559
+ sets = ", ".join(f"{_path(a.target)} = {_sql(a.value)}" for a in w.assignments)
560
+ kind += f"UPDATE SET {sets}"
561
+ elif w.action_kind == "DELETE":
562
+ kind += "DELETE"
563
+ elif w.action_kind == "INSERT_ROW":
564
+ kind += "INSERT ROW"
565
+ else:
566
+ cols = ""
567
+ if w.columns:
568
+ cols = "(" + ", ".join(_ident(c) for c in w.columns) + ") "
569
+ kind += f"INSERT {cols}VALUES (" + ", ".join(_sql(v) for v in w.values) + ")"
570
+ parts.append(kind)
571
+ return " ".join(parts)
572
+
573
+
574
+ # ------------------------------------------------------------------ scripting
575
+
576
+ def _stmts(statements) -> str:
577
+ return " ".join(_sql(s) + ";" for s in statements)
578
+
579
+
580
+ @_sql.register(n.DeclareStmt)
581
+ def _(node: n.DeclareStmt) -> str:
582
+ out = "DECLARE " + ", ".join(_ident(x) for x in node.names)
583
+ if node.type is not None:
584
+ out += f" {_sql(node.type)}"
585
+ if node.default is not None:
586
+ out += f" DEFAULT {_sql(node.default)}"
587
+ return out
588
+
589
+
590
+ @_sql.register(n.SetStmt)
591
+ def _(node: n.SetStmt) -> str:
592
+ if len(node.targets) > 1:
593
+ tgt = "(" + ", ".join(node.targets) + ")"
594
+ else:
595
+ tgt = node.targets[0] if node.targets[0].startswith("@") else _ident(node.targets[0])
596
+ return f"SET {tgt} = {_sql(node.value)}"
597
+
598
+
599
+ @_sql.register(n.ScriptBlock)
600
+ def _(node: n.ScriptBlock) -> str:
601
+ label = f"{node.label}: " if node.label else ""
602
+ out = f"{label}BEGIN {_stmts(node.statements)}"
603
+ if node.has_exception_handler:
604
+ out += f" EXCEPTION WHEN ERROR THEN {_stmts(node.exception_statements)}"
605
+ return out.rstrip() + " END"
606
+
607
+
608
+ @_sql.register(n.IfStmt)
609
+ def _(node: n.IfStmt) -> str:
610
+ first = node.branches[0]
611
+ out = f"IF {_sql(first.condition)} THEN {_stmts(first.statements)}"
612
+ for b in node.branches[1:]:
613
+ out += f" ELSEIF {_sql(b.condition)} THEN {_stmts(b.statements)}"
614
+ if node.else_statements:
615
+ out += f" ELSE {_stmts(node.else_statements)}"
616
+ return out.rstrip() + " END IF"
617
+
618
+
619
+ @_sql.register(n.LoopStmt)
620
+ def _(node: n.LoopStmt) -> str:
621
+ label = f"{node.label}: " if node.label else ""
622
+ return f"{label}LOOP {_stmts(node.statements)}".rstrip() + " END LOOP"
623
+
624
+
625
+ @_sql.register(n.WhileStmt)
626
+ def _(node: n.WhileStmt) -> str:
627
+ label = f"{node.label}: " if node.label else ""
628
+ return (f"{label}WHILE {_sql(node.condition)} DO "
629
+ f"{_stmts(node.statements)}").rstrip() + " END WHILE"
630
+
631
+
632
+ @_sql.register(n.RepeatStmt)
633
+ def _(node: n.RepeatStmt) -> str:
634
+ label = f"{node.label}: " if node.label else ""
635
+ return (f"{label}REPEAT {_stmts(node.statements)} "
636
+ f"UNTIL {_sql(node.until)} END REPEAT")
637
+
638
+
639
+ @_sql.register(n.ForInStmt)
640
+ def _(node: n.ForInStmt) -> str:
641
+ label = f"{node.label}: " if node.label else ""
642
+ return (f"{label}FOR {_ident(node.var)} IN ({_sql(node.query)}) DO "
643
+ f"{_stmts(node.statements)}").rstrip() + " END FOR"
644
+
645
+
646
+ @_sql.register(n.BreakContinueStmt)
647
+ def _(node: n.BreakContinueStmt) -> str:
648
+ return f"{node.kind} {node.label}" if node.label else node.kind
649
+
650
+
651
+ @_sql.register(n.CallStmt)
652
+ def _(node: n.CallStmt) -> str:
653
+ return f"CALL {_path(node.name)}({', '.join(_sql(a) for a in node.args)})"
654
+
655
+
656
+ @_sql.register(n.ReturnStmt)
657
+ def _(node: n.ReturnStmt) -> str:
658
+ return "RETURN"
659
+
660
+
661
+ @_sql.register(n.RaiseStmt)
662
+ def _(node: n.RaiseStmt) -> str:
663
+ if node.message is not None:
664
+ return f"RAISE USING MESSAGE = {_sql(node.message)}"
665
+ return "RAISE"
666
+
667
+
668
+ @_sql.register(n.ExecuteImmediate)
669
+ def _(node: n.ExecuteImmediate) -> str:
670
+ out = f"EXECUTE IMMEDIATE {_sql(node.sql_expr)}"
671
+ if node.into:
672
+ out += " INTO " + ", ".join(_ident(v) for v in node.into)
673
+ if node.using:
674
+ args = ", ".join(
675
+ _sql(e) + (f" AS {_ident(a)}" if a else "") for e, a in node.using
676
+ )
677
+ out += f" USING {args}"
678
+ return out
679
+
680
+
681
+ @_sql.register(n.AssertStmt)
682
+ def _(node: n.AssertStmt) -> str:
683
+ out = f"ASSERT {_sql(node.condition)}"
684
+ if node.message is not None:
685
+ out += f" AS {_sql(node.message)}"
686
+ return out
687
+
688
+
689
+ @_sql.register(n.TransactionStmt)
690
+ def _(node: n.TransactionStmt) -> str:
691
+ return f"{node.kind} TRANSACTION"
692
+
693
+
694
+ @_sql.register(n.TruncateStmt)
695
+ def _(node: n.TruncateStmt) -> str:
696
+ return f"TRUNCATE TABLE {_path(node.table)}"
697
+
698
+
699
+ @_sql.register(n.DropStmt)
700
+ def _(node: n.DropStmt) -> str:
701
+ exists = "IF EXISTS " if node.if_exists else ""
702
+ return f"DROP {node.kind} {exists}{_path(node.name)}"
703
+
704
+
705
+ @_sql.register(n.RoutineParam)
706
+ def _(node: n.RoutineParam) -> str:
707
+ out = f"{node.mode} " if node.mode else ""
708
+ out += _ident(node.name)
709
+ if node.type is not None:
710
+ out += f" {_sql(node.type)}"
711
+ return out
712
+
713
+
714
+ @_sql.register(n.CreateProcedure)
715
+ def _(node: n.CreateProcedure) -> str:
716
+ parts = ["CREATE"]
717
+ if node.replace:
718
+ parts.append("OR REPLACE")
719
+ parts.append("PROCEDURE")
720
+ if node.if_not_exists:
721
+ parts.append("IF NOT EXISTS")
722
+ parts.append(f"{_path(node.name)}({', '.join(_sql(p) for p in node.params)})")
723
+ if node.options_sql:
724
+ parts.append(f"OPTIONS{node.options_sql}")
725
+ parts.append(_sql(node.body))
726
+ return " ".join(parts)
727
+
728
+
729
+ @_sql.register(n.CreateFunction)
730
+ def _(node: n.CreateFunction) -> str:
731
+ parts = ["CREATE"]
732
+ if node.replace:
733
+ parts.append("OR REPLACE")
734
+ if node.temp:
735
+ parts.append("TEMP")
736
+ if node.aggregate:
737
+ parts.append("AGGREGATE")
738
+ if node.table_function:
739
+ parts.append("TABLE")
740
+ parts.append("FUNCTION")
741
+ if node.if_not_exists:
742
+ parts.append("IF NOT EXISTS")
743
+ parts.append(f"{_path(node.name)}({', '.join(_sql(p) for p in node.params)})")
744
+ if node.returns_table:
745
+ inner = ", ".join(
746
+ (f"{_ident(f.name)} " if f.name else "") + _sql(f.type)
747
+ for f in node.returns_table
748
+ )
749
+ parts.append(f"RETURNS TABLE<{inner}>")
750
+ elif node.returns is not None:
751
+ parts.append(f"RETURNS {_sql(node.returns)}")
752
+ if node.deterministic is True:
753
+ parts.append("DETERMINISTIC")
754
+ elif node.deterministic is False:
755
+ parts.append("NOT DETERMINISTIC")
756
+ if node.language:
757
+ parts.append(f"LANGUAGE {node.language}")
758
+ if node.options_sql:
759
+ parts.append(f"OPTIONS{node.options_sql}")
760
+ if node.body_query is not None:
761
+ parts.append(f"AS ({_sql(node.body_query)})")
762
+ elif node.body_expr is not None:
763
+ parts.append(f"AS ({_sql(node.body_expr)})")
764
+ elif node.body_string is not None:
765
+ parts.append(f"AS {_str_lit(node.body_string)}")
766
+ return " ".join(parts)