code-atlas-forge 1.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.
Files changed (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1510 @@
1
+ """Parser SQL descendente-recursivo em stdlib puro (lf-0111, Phase 3).
2
+
3
+ Cobre o subconjunto declarativo necessário à linhagem: SELECT (com CTE,
4
+ joins, subqueries, set-ops), INSERT (INTO/OVERWRITE + col list +
5
+ PARTITION), UPDATE/DELETE/MERGE, CREATE TABLE (DDL + CTAS + LIKE),
6
+ CREATE VIEW, USE, TRUNCATE e PREPARE. O restante vira ``SUnsupported``.
7
+
8
+ Dialeto é **rótulo**, não gate: o parser é permissivo e aceita sintaxes
9
+ Athena/Trino (UNNEST→unsupported-source, ``->`` lambda, ``WITH (props)``)
10
+ e Spark (INSERT OVERWRITE, LATERAL VIEW→unsupported-source, DISTRIBUTE/
11
+ CLUSTER/SORT BY, ``${}``→token dynamic) onde elas divergem de ANSI.
12
+
13
+ Disciplina de erro: ``ParseError`` interno → diagnóstico + resync no
14
+ próximo ``;`` top-level — extração parcial, nunca crash. ``parse_file``
15
+ é o único ponto de entrada e nunca levanta.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import bisect
21
+ import re
22
+ from dataclasses import dataclass, field
23
+
24
+ from code_atlas.domains.sql.nodes import (
25
+ ColRef,
26
+ Cte,
27
+ ExprRefs,
28
+ NameRef,
29
+ SCreateTable,
30
+ SCreateView,
31
+ SDelete,
32
+ SelectItem,
33
+ SelectStmt,
34
+ SInsert,
35
+ SMerge,
36
+ Source,
37
+ SrcGroup,
38
+ SrcJoin,
39
+ SrcSub,
40
+ SrcTable,
41
+ SrcUnsupported,
42
+ SrcValues,
43
+ SSelect,
44
+ StarRef,
45
+ Statement,
46
+ STruncate,
47
+ SUnsupported,
48
+ SUpdate,
49
+ SUse,
50
+ )
51
+ from code_atlas.domains.sql.tokens import (
52
+ _SPECIAL_ARG_FNS,
53
+ _TYPE_LITERAL_WORDS,
54
+ Token,
55
+ is_kw,
56
+ is_name,
57
+ is_word,
58
+ tokenize,
59
+ )
60
+ from code_atlas.parsers.contracts import ParseDiagnostic, SourceSpan
61
+
62
+ _MAX_DEPTH = 32 # subqueries/parênteses — além disso é patológico
63
+
64
+ # Keywords que terminam qualquer expressão em depth 0. Inclui inícios de
65
+ # cláusula e de statement — um segundo SELECT no meio de uma expressão é
66
+ # erro de sincronização, não continuação.
67
+ _EXPR_STOPS: frozenset[str] = frozenset(
68
+ {
69
+ "FROM", "WHERE", "GROUP", "HAVING", "QUALIFY", "WINDOW", "ORDER",
70
+ "LIMIT", "OFFSET", "FETCH", "UNION", "INTERSECT", "EXCEPT",
71
+ "INTO", "ON", "USING", "JOIN", "INNER", "LEFT", "RIGHT", "FULL",
72
+ "CROSS", "NATURAL", "SEMI", "ANTI", "LATERAL", "PIVOT",
73
+ "TABLESAMPLE", "WHEN", "THEN", "ELSE", "END", "AS", "PARTITION",
74
+ "CLUSTER", "DISTRIBUTE", "SORT", "MATCHED", "SET", "VALUES",
75
+ "SELECT", "INSERT", "UPDATE", "DELETE", "MERGE", "CREATE", "USE",
76
+ "DROP", "ALTER", "GRANT", "REVOKE", "TRUNCATE", "SHOW",
77
+ "DESCRIBE", "EXPLAIN", "ANALYZE", "MSCK", "CACHE",
78
+ "UNCACHE", "REFRESH", "EXEC", "EXECUTE", "CALL", "PREPARE",
79
+ "DEALLOCATE", "COMMIT", "ROLLBACK", "VACUUM", "OPTIMIZE",
80
+ "LOAD", "EXPORT", "IMPORT",
81
+ }
82
+ )
83
+
84
+ _ITEM_STOPS = _EXPR_STOPS | frozenset({"AS"})
85
+
86
+ _UNSUPPORTED_START: frozenset[str] = frozenset(
87
+ {
88
+ "DROP", "ALTER", "GRANT", "REVOKE", "ANALYZE", "EXPLAIN",
89
+ "DESCRIBE", "DESC", "SHOW", "MSCK", "CACHE", "UNCACHE",
90
+ "REFRESH", "SET", "RESET", "ADD", "JAR", "LOAD", "EXPORT",
91
+ "IMPORT", "VACUUM", "OPTIMIZE", "COMMIT", "ROLLBACK", "BEGIN",
92
+ "START", "TRANSACTION", "DEALLOCATE",
93
+ }
94
+ )
95
+
96
+ _DYNAMIC_START: frozenset[str] = frozenset({"EXEC", "EXECUTE", "CALL"})
97
+
98
+ _CREATE_SUB: frozenset[str] = frozenset(
99
+ {
100
+ "SCHEMA", "DATABASE", "INDEX", "FUNCTION", "PROCEDURE", "ROLE",
101
+ "TRIGGER", "SEQUENCE",
102
+ }
103
+ )
104
+
105
+ _CONSTRAINT_HEAD: frozenset[str] = frozenset(
106
+ {"PRIMARY", "FOREIGN", "UNIQUE", "CHECK", "CONSTRAINT", "INDEX", "KEY"}
107
+ )
108
+
109
+ _INTERVAL_UNITS: frozenset[str] = frozenset(
110
+ {
111
+ "YEAR", "QUARTER", "MONTH", "WEEK", "DAY", "HOUR", "MINUTE",
112
+ "SECOND", "MILLISECOND", "TO",
113
+ }
114
+ )
115
+
116
+ _FN_FIELD_WORDS: frozenset[str] = frozenset(
117
+ {
118
+ "BOTH", "LEADING", "TRAILING", "YEAR", "QUARTER", "MONTH", "WEEK",
119
+ "DAY", "HOUR", "MINUTE", "SECOND", "EPOCH", "TIMEZONE_HOUR",
120
+ "TIMEZONE_MINUTE", "TIMEZONE_ABBR", "DOW", "DOY", "CENTURY",
121
+ "DECADE", "MILLENNIUM", "ISOYEAR",
122
+ }
123
+ )
124
+
125
+ # Padrões de rótulo de dialeto — heurística declarada, nunca gate.
126
+ _SPARK_RE = re.compile(
127
+ r"\b(?:insert\s+overwrite|lateral\s+view|distribute\s+by|cluster\s+by"
128
+ r"|msck\s+repair|tblproperties|stored\s+as|bucketed\s+by"
129
+ r"|using\s+(?:delta|parquet|orc|json|csv|hive|iceberg))\b|\$\{",
130
+ re.IGNORECASE | re.DOTALL,
131
+ )
132
+ _ATHENA_RE = re.compile(
133
+ r"\b(?:unnest|try_cast|approx_\w+|cardinality|external_location"
134
+ r"|partitioned_by|bucketed_by|ordinality)\b|->",
135
+ re.IGNORECASE,
136
+ )
137
+
138
+
139
+ def detect_dialect(text: str) -> str:
140
+ """Rótulo heurístico do dialeto dominante do arquivo.
141
+
142
+ Retorna ``spark`` | ``athena`` | ``ansi``. É metadata honesta de
143
+ discovery — o parser não muda comportamento por ela.
144
+ """
145
+ if _SPARK_RE.search(text):
146
+ return "spark"
147
+ if _ATHENA_RE.search(text):
148
+ return "athena"
149
+ return "ansi"
150
+
151
+
152
+ class ParseError(Exception):
153
+ """Erro interno de parse — vira diagnóstico, nunca propaga."""
154
+
155
+ def __init__(self, msg: str, tok: Token) -> None:
156
+ super().__init__(msg)
157
+ self.msg = msg
158
+ self.tok = tok
159
+
160
+
161
+ class _Src:
162
+ """Mapeia offsets de caractere → ``SourceSpan`` (bytes, linha, col)."""
163
+
164
+ def __init__(self, text: str) -> None:
165
+ self._lines = [0]
166
+ for m in re.finditer("\n", text):
167
+ self._lines.append(m.end())
168
+ self._bytes: list[int] | None = None
169
+ if not text.isascii():
170
+ offs: list[int] = []
171
+ b = 0
172
+ for ch in text:
173
+ offs.append(b)
174
+ b += len(ch.encode("utf-8"))
175
+ offs.append(b)
176
+ self._bytes = offs
177
+
178
+ def _b(self, i: int) -> int:
179
+ if self._bytes is None:
180
+ return i
181
+ return self._bytes[min(i, len(self._bytes) - 1)]
182
+
183
+ def span(self, start: int, end: int) -> SourceSpan:
184
+ """``start``/``end`` são offsets de CARACTERE."""
185
+ li = bisect.bisect_right(self._lines, start) - 1
186
+ le = bisect.bisect_right(self._lines, end) - 1
187
+ return SourceSpan(
188
+ start_byte=self._b(start),
189
+ end_byte=self._b(end),
190
+ start_line=li + 1,
191
+ start_col=self._b(start) - self._b(self._lines[li]),
192
+ end_line=le + 1,
193
+ end_col=self._b(end) - self._b(self._lines[le]),
194
+ )
195
+
196
+
197
+ @dataclass
198
+ class ParsedFile:
199
+ statements: list[Statement]
200
+ diagnostics: list[ParseDiagnostic]
201
+ dialect: str
202
+ problems: list[str] = field(default_factory=list)
203
+
204
+
205
+ @dataclass
206
+ class _ExprScan:
207
+ """Estado mutável do scan de expressão (``_P.expr`` + helpers)."""
208
+
209
+ refs: ExprRefs = field(default_factory=ExprRefs)
210
+ tagged: list[tuple[ColRef, int]] = field(default_factory=list)
211
+ bound: dict[int, set[str]] = field(default_factory=dict)
212
+ recent: list[str] = field(default_factory=list)
213
+ lvl: int = 0
214
+ case_depth: int = 0
215
+ value_end: bool = False
216
+
217
+
218
+ def parse_file(text: str) -> ParsedFile:
219
+ """Tokeniza + parseia; nunca levanta — problemas viram diagnósticos."""
220
+ toks, problems = tokenize(text)
221
+ src = _Src(text)
222
+ p = _P(toks, src)
223
+ diags: list[ParseDiagnostic] = [
224
+ ParseDiagnostic(code=prob, severity="warning", message=prob)
225
+ for prob in problems
226
+ ]
227
+ dialect = detect_dialect(text)
228
+ statements: list[Statement] = []
229
+ while not p.at_eof():
230
+ if p.accept_punct(";"):
231
+ continue
232
+ start = p.i
233
+ try:
234
+ st = p.statement()
235
+ except ParseError as exc:
236
+ diags.append(
237
+ ParseDiagnostic(
238
+ code="parse-error",
239
+ severity="error",
240
+ message=f"{exc.msg} (near {exc.tok.text!r})",
241
+ span=src.span(exc.tok.start, exc.tok.end),
242
+ )
243
+ )
244
+ p.resync()
245
+ continue
246
+ if st is not None:
247
+ st.dynamic = st.dynamic or p.had_dynamic(start, p.i)
248
+ statements.append(st)
249
+ # tokens não consumidos antes do próximo `;` → resíduo honesto
250
+ if not p.at_eof() and not p.at_punct(";"):
251
+ trailing = p.cur()
252
+ diags.append(
253
+ ParseDiagnostic(
254
+ code="trailing-tokens",
255
+ severity="warning",
256
+ message=f"unparsed trailing tokens (near {trailing.text!r})",
257
+ span=src.span(trailing.start, trailing.end),
258
+ )
259
+ )
260
+ p.resync()
261
+ return ParsedFile(
262
+ statements=statements, diagnostics=diags,
263
+ dialect=dialect, problems=problems,
264
+ )
265
+
266
+
267
+ class _P:
268
+ """Cursor de tokens com helpers de match/span."""
269
+
270
+ def __init__(self, toks: list[Token], src: _Src) -> None:
271
+ self.toks = toks
272
+ self.i = 0
273
+ self.src = src
274
+
275
+ # --- cursor -----------------------------------------------------
276
+
277
+ def cur(self) -> Token:
278
+ return self.toks[self.i]
279
+
280
+ def peek(self, k: int = 1) -> Token:
281
+ j = self.i + k
282
+ return self.toks[min(j, len(self.toks) - 1)]
283
+
284
+ def at_eof(self) -> bool:
285
+ return self.toks[self.i].kind == "eof"
286
+
287
+ def at_punct(self, s: str) -> bool:
288
+ t = self.toks[self.i]
289
+ return t.kind == "punct" and t.text == s
290
+
291
+ def at_kw(self, *words: str) -> bool:
292
+ return is_kw(self.toks[self.i], *words)
293
+
294
+ def at_name(self) -> bool:
295
+ return is_name(self.toks[self.i])
296
+
297
+ def at(self, idx: int, *words: str) -> bool:
298
+ """``at_kw`` em posição arbitrária (lookahead)."""
299
+ return is_kw(self.toks[min(idx, len(self.toks) - 1)], *words)
300
+
301
+ def accept_punct(self, s: str) -> bool:
302
+ if self.at_punct(s):
303
+ self.i += 1
304
+ return True
305
+ return False
306
+
307
+ def accept_kw(self, *words: str) -> bool:
308
+ if self.at_kw(*words):
309
+ self.i += 1
310
+ return True
311
+ return False
312
+
313
+ def expect_kw(self, *words: str) -> Token:
314
+ t = self.toks[self.i]
315
+ if not is_kw(t, *words):
316
+ raise ParseError(f"expected {'/'.join(words)}", t)
317
+ self.i += 1
318
+ return t
319
+
320
+ def expect_punct(self, s: str) -> Token:
321
+ t = self.toks[self.i]
322
+ if t.kind != "punct" or t.text != s:
323
+ raise ParseError(f"expected {s!r}", t)
324
+ self.i += 1
325
+ return t
326
+
327
+ def span(self, start: int, end: int) -> SourceSpan:
328
+ """Offsets de caractere → SourceSpan em bytes."""
329
+ return self.src.span(start, end)
330
+
331
+ def last_end(self) -> int:
332
+ """Offset char do fim do último token consumido."""
333
+ return self.toks[self.i - 1].end if self.i else 0
334
+
335
+ def had_dynamic(self, start: int, end: int) -> bool:
336
+ return any(t.kind == "dynamic" for t in self.toks[start:end])
337
+
338
+ def resync(self) -> None:
339
+ """Avança até o próximo ``;`` top-level (ou EOF)."""
340
+ depth = 0
341
+ while not self.at_eof():
342
+ t = self.cur()
343
+ if t.kind == "punct":
344
+ if t.text == "(":
345
+ depth += 1
346
+ elif t.text == ")":
347
+ depth = max(0, depth - 1)
348
+ elif t.text == ";" and depth == 0:
349
+ return
350
+ self.i += 1
351
+
352
+ def skip_balanced(self) -> None:
353
+ """Consome ``( … )`` balanceado; erro se não abre com ``(``."""
354
+ self.expect_punct("(")
355
+ depth = 1
356
+ while depth > 0:
357
+ if self.at_eof():
358
+ raise ParseError("unbalanced parens", self.cur())
359
+ t = self.cur()
360
+ if t.kind == "punct":
361
+ if t.text == "(":
362
+ depth += 1
363
+ elif t.text == ")":
364
+ depth -= 1
365
+ self.i += 1
366
+
367
+ # --- nomes --------------------------------------------------------
368
+
369
+ def name_chain(self, *, loose: bool = False) -> NameRef:
370
+ """``part(.part)*`` — parts normalizadas (word→lower, qident→case).
371
+
372
+ ``loose=True`` aceita keyword como primeira parte (USE/CREATE com
373
+ nomes como ``default``); caso contrário a primeira parte deve ser
374
+ nome não-keyword.
375
+ """
376
+ start = self.cur()
377
+ t = self.cur()
378
+ if not is_word(t):
379
+ raise ParseError("expected name", t)
380
+ if not loose and not is_name(t):
381
+ raise ParseError("expected identifier (reserved word)", t)
382
+ parts = [t.iname]
383
+ written = [t.text]
384
+ end = t.end
385
+ self.i += 1
386
+ while self.at_punct(".") and is_word(self.peek()):
387
+ self.i += 1 # '.'
388
+ t = self.cur()
389
+ parts.append(t.iname)
390
+ written.append(t.text)
391
+ end = t.end
392
+ self.i += 1
393
+ return NameRef(
394
+ parts=tuple(parts),
395
+ written=".".join(written),
396
+ span=self.span(start.start, end),
397
+ )
398
+
399
+ def opt_alias(self, *, cols: bool = False) -> tuple[str | None, tuple[str, ...]]:
400
+ """Alias opcional: ``AS name`` ou nome implícito.
401
+
402
+ ``cols=True`` tenta ``( a, b )`` de col-aliases logo após o
403
+ alias — restaura a posição quando não é lista de nomes.
404
+ """
405
+ alias: str | None = None
406
+ col_aliases: tuple[str, ...] = ()
407
+ if self.accept_kw("AS"):
408
+ t = self.cur()
409
+ if not is_word(t):
410
+ raise ParseError("expected alias name", t)
411
+ alias = t.iname
412
+ self.i += 1
413
+ elif self.at_name():
414
+ alias = self.cur().iname
415
+ self.i += 1
416
+ if cols and alias is not None and self.at_punct("("):
417
+ mark = self.i
418
+ try:
419
+ col_aliases = self._col_list()
420
+ except ParseError:
421
+ self.i = mark # era outra coisa — restaura
422
+ return alias, col_aliases
423
+
424
+ def _col_list(self) -> tuple[str, ...]:
425
+ """``( nome, … )`` — apenas nomes; erro em qualquer outra coisa."""
426
+ self.expect_punct("(")
427
+ names: list[str] = []
428
+ while True:
429
+ t = self.cur()
430
+ if not is_word(t):
431
+ raise ParseError("expected column name", t)
432
+ names.append(t.iname)
433
+ self.i += 1
434
+ if self.accept_punct(","):
435
+ continue
436
+ self.expect_punct(")")
437
+ return tuple(names)
438
+
439
+ # --- statements ---------------------------------------------------
440
+
441
+ def statement(self) -> Statement | None:
442
+ ctes: list[Cte] = []
443
+ start = self.cur()
444
+ if self.at_kw("WITH"):
445
+ ctes = self.with_clause()
446
+ t = self.cur()
447
+ if t.kind == "dynamic":
448
+ raise ParseError("dynamic/template SQL at statement start", t)
449
+ if is_kw(t, "SELECT"):
450
+ q = self.query(ctes)
451
+ return SSelect(
452
+ kind="select",
453
+ span=self.span(start.start, self.last_end()),
454
+ query=q,
455
+ )
456
+ if is_kw(t, "INSERT", "UPDATE", "DELETE", "MERGE", "CREATE"):
457
+ return self._statement_dml(t, ctes, start)
458
+ return self._statement_aux(t, start)
459
+
460
+ def _statement_dml(
461
+ self, t: Token, ctes: list[Cte], start: Token
462
+ ) -> Statement:
463
+ """DML + CREATE — ``t`` é a keyword de início do statement."""
464
+ if is_kw(t, "INSERT"):
465
+ return self.insert_stmt(ctes, start)
466
+ if is_kw(t, "UPDATE"):
467
+ return self.update_stmt(ctes, start)
468
+ if is_kw(t, "DELETE"):
469
+ return self.delete_stmt(ctes, start)
470
+ if is_kw(t, "MERGE"):
471
+ return self.merge_stmt(ctes, start)
472
+ if ctes:
473
+ raise ParseError("WITH before CREATE is not supported", t)
474
+ return self.create_stmt(start)
475
+
476
+ def _statement_aux(self, t: Token, start: Token) -> Statement:
477
+ """USE/TRUNCATE/PREPARE + dinâmico/unsupported — ``t`` é a head."""
478
+ if is_kw(t, "USE"):
479
+ self.i += 1
480
+ name = self.name_chain(loose=True)
481
+ return SUse(
482
+ kind="use", span=self.span(start.start, self.last_end()),
483
+ parts=name.parts, written=name.written,
484
+ )
485
+ if is_kw(t, "TRUNCATE"):
486
+ self.i += 1
487
+ self.accept_kw("TABLE")
488
+ name = self.name_chain(loose=True)
489
+ return STruncate(
490
+ kind="truncate",
491
+ span=self.span(start.start, self.last_end()),
492
+ name=name,
493
+ )
494
+ if is_kw(t, "PREPARE"):
495
+ return self.prepare_stmt(start)
496
+ if is_kw(t, *_DYNAMIC_START):
497
+ st = SUnsupported(
498
+ kind="unsupported", span=self.span(start.start, t.end),
499
+ why="dynamic-exec", head=t.norm,
500
+ )
501
+ self.skip_to_boundary()
502
+ return st
503
+ if is_kw(t, *_UNSUPPORTED_START):
504
+ st = SUnsupported(
505
+ kind="unsupported", span=self.span(start.start, t.end),
506
+ why=t.norm.lower(), head=t.norm,
507
+ )
508
+ self.skip_to_boundary()
509
+ return st
510
+ raise ParseError("unrecognized statement start", t)
511
+
512
+ def skip_to_boundary(self) -> None:
513
+ """Consome até ``;`` top-level — para statements não-parseados."""
514
+ self.resync()
515
+
516
+ def with_clause(self) -> list[Cte]:
517
+ self.expect_kw("WITH")
518
+ recursive = self.accept_kw("RECURSIVE")
519
+ ctes: list[Cte] = []
520
+ while True:
521
+ t = self.cur()
522
+ if not is_word(t):
523
+ raise ParseError("expected CTE name", t)
524
+ name = t.iname
525
+ self.i += 1
526
+ cols: tuple[str, ...] = ()
527
+ if self.at_punct("("):
528
+ cols = self._col_list()
529
+ self.expect_kw("AS")
530
+ self.accept_kw("NOT")
531
+ self.accept_kw("MATERIALIZED")
532
+ self.expect_punct("(")
533
+ q = self.query()
534
+ self.expect_punct(")")
535
+ ctes.append(
536
+ Cte(
537
+ name=name, columns=cols, query=q,
538
+ span=self.span(t.start, self.last_end()),
539
+ recursive=recursive,
540
+ )
541
+ )
542
+ if not self.accept_punct(","):
543
+ break
544
+ return ctes
545
+
546
+ # --- SELECT ---------------------------------------------------------
547
+
548
+ def query(self, ctes: list[Cte] | None = None) -> SelectStmt:
549
+ """select_term (set_op select_term)* — parênteses nos termos."""
550
+ if self.at_kw("WITH"):
551
+ ctes = [*self.with_clause(), *(ctes or [])]
552
+ first = self.query_term()
553
+ if ctes:
554
+ first.ctes = list(ctes) + first.ctes
555
+ while self.at_kw("UNION", "INTERSECT", "EXCEPT"):
556
+ op = self.cur().norm
557
+ self.i += 1
558
+ if self.accept_kw("ALL"):
559
+ op += " ALL"
560
+ else:
561
+ self.accept_kw("DISTINCT")
562
+ first.set_ops.append(op)
563
+ first.set_branches.append(self.query_term())
564
+ return first
565
+
566
+ def query_term(self) -> SelectStmt:
567
+ if self.at_punct("("):
568
+ self.i += 1
569
+ s = self.query()
570
+ self.expect_punct(")")
571
+ return s
572
+ return self.select_core()
573
+
574
+ def select_core(self) -> SelectStmt:
575
+ start = self.cur()
576
+ self.expect_kw("SELECT")
577
+ self.accept_kw("ALL")
578
+ self.accept_kw("DISTINCT")
579
+ if self.accept_kw("TOP"):
580
+ # TOP n — quantificador não-ansi; consome um token.
581
+ if self.at_punct("("):
582
+ self.skip_balanced()
583
+ elif not self.at_eof():
584
+ self.i += 1
585
+ items: list[SelectItem] = []
586
+ subs: list[SelectStmt] = []
587
+ if (
588
+ self.at_kw("FROM") or self.at_punct(";") or self.at_eof()
589
+ or self.at_punct(")")
590
+ ):
591
+ raise ParseError("empty select list", self.cur())
592
+ while True:
593
+ items.append(self.select_item(subs))
594
+ if self.accept_punct(","):
595
+ continue
596
+ break
597
+ sources: list[Source] = []
598
+ if self.accept_kw("FROM"):
599
+ sources = self.source_list(subs)
600
+ self.tail_clauses(subs)
601
+ return SelectStmt(
602
+ ctes=[], items=items, sources=sources, set_branches=[],
603
+ set_ops=[], tail_subqueries=subs,
604
+ span=self.span(start.start, self.last_end()),
605
+ )
606
+
607
+ def tail_clauses(self, subs: list[SelectStmt]) -> None:
608
+ """WHERE/GROUP/HAVING/QUALIFY/ORDER/LIMIT… — scan sem linhagem."""
609
+ while True:
610
+ if self.accept_kw("WHERE", "HAVING", "QUALIFY", "WINDOW"):
611
+ refs = self.expr(_EXPR_STOPS)
612
+ subs.extend(refs.subqueries)
613
+ elif self.accept_kw("GROUP", "ORDER"):
614
+ self.accept_kw("BY")
615
+ refs = self.expr(_EXPR_STOPS)
616
+ subs.extend(refs.subqueries)
617
+ while self.accept_punct(","):
618
+ refs = self.expr(_EXPR_STOPS)
619
+ subs.extend(refs.subqueries)
620
+ elif self.at_kw("CLUSTER", "DISTRIBUTE", "SORT") and (
621
+ self.peek().kind == "word" and self.peek().norm == "BY"
622
+ ):
623
+ self.i += 2 # kw + BY
624
+ refs = self.expr(_EXPR_STOPS)
625
+ subs.extend(refs.subqueries)
626
+ elif self.at_kw("LIMIT", "OFFSET", "FETCH"):
627
+ self.i += 1
628
+ self.limit_spec()
629
+ else:
630
+ return
631
+
632
+ def limit_spec(self) -> None:
633
+ """Consome ``LIMIT n[,m]|ALL|OFFSET n ROWS|FETCH NEXT n …``."""
634
+ while not self.at_eof():
635
+ t = self.cur()
636
+ if t.kind == "number" or is_kw(
637
+ t, "ALL", "ROWS", "ROW", "ONLY", "WITH", "TIES",
638
+ "FIRST", "NEXT", "OFFSET", "PERCENT",
639
+ ):
640
+ self.i += 1
641
+ continue
642
+ if self.accept_punct(","):
643
+ continue
644
+ return
645
+
646
+ def select_item(self, subs: list[SelectStmt]) -> SelectItem:
647
+ start = self.cur()
648
+ if self.at_punct("*"):
649
+ self.i += 1
650
+ star = StarRef(qualifier=(), span=self.span(start.start, start.end))
651
+ return SelectItem(
652
+ refs=ExprRefs(), alias=None,
653
+ span=self.span(start.start, start.end), star=star,
654
+ )
655
+ refs = self.expr(_ITEM_STOPS)
656
+ subs.extend(refs.subqueries)
657
+ alias: str | None = None
658
+ if self.accept_kw("AS"):
659
+ t = self.cur()
660
+ if not is_word(t):
661
+ raise ParseError("expected alias", t)
662
+ alias = t.iname
663
+ self.i += 1
664
+ elif self.at_name():
665
+ alias = self.cur().iname
666
+ self.i += 1
667
+ star_ref = refs.top_star if not refs.cols else None
668
+ return SelectItem(
669
+ refs=refs, alias=alias,
670
+ span=self.span(start.start, self.last_end()), star=star_ref,
671
+ )
672
+
673
+ # --- FROM ---------------------------------------------------------
674
+
675
+ def source_list(self, subs: list[SelectStmt]) -> list[Source]:
676
+ srcs = [self.join_chain(subs)]
677
+ while self.accept_punct(","):
678
+ srcs.append(self.join_chain(subs))
679
+ return srcs
680
+
681
+ _JOIN_MODS = frozenset(
682
+ {
683
+ "INNER", "LEFT", "RIGHT", "FULL", "OUTER", "CROSS",
684
+ "NATURAL", "SEMI", "ANTI",
685
+ }
686
+ )
687
+
688
+ def join_chain(self, subs: list[SelectStmt], depth: int = 0) -> Source:
689
+ start = self.cur()
690
+ left = self.primary(subs, depth)
691
+ while True:
692
+ t = self.cur()
693
+ if is_kw(t, "JOIN"):
694
+ kind = "JOIN"
695
+ self.i += 1
696
+ elif is_kw(t, *self._JOIN_MODS):
697
+ mark = self.i
698
+ parts = []
699
+ while self.at_kw(*self._JOIN_MODS):
700
+ parts.append(self.cur().norm)
701
+ self.i += 1
702
+ if not self.at_kw("JOIN"):
703
+ self.i = mark # não era join — restaura
704
+ break
705
+ kind = " ".join(parts) + " JOIN"
706
+ self.i += 1
707
+ else:
708
+ break
709
+ right = self.primary(subs, depth)
710
+ if self.accept_kw("ON"):
711
+ refs = self.expr(_EXPR_STOPS)
712
+ subs.extend(refs.subqueries)
713
+ elif self.accept_kw("USING"):
714
+ self.skip_balanced()
715
+ left = SrcJoin(
716
+ left=left, right=right, kind=kind,
717
+ span=self.span(start.start, self.last_end()),
718
+ )
719
+ return left
720
+
721
+ def primary(self, subs: list[SelectStmt], depth: int) -> Source:
722
+ t = self.cur()
723
+ if depth + 1 > _MAX_DEPTH:
724
+ raise ParseError("nesting too deep", t)
725
+ start = self.cur()
726
+ if self.at_punct("("):
727
+ return self._primary_paren(subs, depth, start)
728
+ if self.at_kw("LATERAL", "PIVOT"):
729
+ what = "lateral-view" if t.norm == "LATERAL" else "pivot"
730
+ self._skip_source_tail()
731
+ return SrcUnsupported(
732
+ what=what, written=t.norm,
733
+ span=self.span(start.start, self.last_end()),
734
+ )
735
+ if self.at_kw("UNNEST"):
736
+ return self._primary_unnest(t, start)
737
+ if self.at_kw("VALUES"):
738
+ return self._primary_values(start)
739
+ if is_word(t):
740
+ return self._primary_word(t, start)
741
+ if t.kind == "dynamic":
742
+ self.i += 1
743
+ return SrcUnsupported(
744
+ what="dynamic", written=t.text,
745
+ span=self.span(start.start, self.last_end()),
746
+ )
747
+ raise ParseError("expected table source", t)
748
+
749
+ def _primary_paren(
750
+ self, subs: list[SelectStmt], depth: int, start: Token
751
+ ) -> Source:
752
+ """``( query )`` subquery ou ``( a JOIN b )`` grupo parentizado."""
753
+ if is_kw(self.peek(), "SELECT", "WITH"):
754
+ self.i += 1
755
+ q = self.query()
756
+ self.expect_punct(")")
757
+ alias, cols = self.opt_alias(cols=True)
758
+ return SrcSub(
759
+ query=q, alias=alias, col_aliases=cols,
760
+ span=self.span(start.start, self.last_end()),
761
+ )
762
+ # fonte agrupada: ( a JOIN b … ) [alias]
763
+ self.i += 1
764
+ inner = self.join_chain(subs, depth + 1)
765
+ self.expect_punct(")")
766
+ alias, _ = self.opt_alias()
767
+ return SrcGroup(
768
+ inner=inner, alias=alias,
769
+ span=self.span(start.start, self.last_end()),
770
+ )
771
+
772
+ def _primary_unnest(self, t: Token, start: Token) -> Source:
773
+ """``UNNEST(arr) [WITH ORDINALITY] [AS u(x)]`` — fonte opaca."""
774
+ self.i += 1
775
+ if self.at_punct("("):
776
+ self.skip_balanced()
777
+ self.accept_kw("WITH")
778
+ self.accept_kw("ORDINALITY")
779
+ self.opt_alias()
780
+ return SrcUnsupported(
781
+ what="unnest", written=t.norm,
782
+ span=self.span(start.start, self.last_end()),
783
+ )
784
+
785
+ def _primary_values(self, start: Token) -> Source:
786
+ """``FROM VALUES (…), (…)`` — dados literais; linhagem termina."""
787
+ self.i += 1
788
+ while self.at_punct("("):
789
+ self.skip_balanced()
790
+ if not self.accept_punct(","):
791
+ break
792
+ alias, cols = self.opt_alias(cols=True)
793
+ return SrcValues(
794
+ alias=alias, col_aliases=cols,
795
+ span=self.span(start.start, self.last_end()),
796
+ )
797
+
798
+ def _primary_word(self, t: Token, start: Token) -> Source:
799
+ """``name[.name]*`` tabela ou ``fn(…)`` função de tabela (opaca)."""
800
+ if self.peek().kind == "punct" and self.peek().text == "(":
801
+ # função de tabela (JSON_TABLE, explode, …) — não expande
802
+ self.i += 1
803
+ self.skip_balanced()
804
+ self.opt_alias()
805
+ return SrcUnsupported(
806
+ what="table-function", written=t.text,
807
+ span=self.span(start.start, self.last_end()),
808
+ )
809
+ name = self.name_chain()
810
+ alias, _ = self.opt_alias()
811
+ return SrcTable(
812
+ name=name, alias=alias,
813
+ span=self.span(start.start, self.last_end()),
814
+ )
815
+
816
+ def _skip_source_tail(self) -> None:
817
+ """Consome fonte não-suportada até fronteira (join/vírgula/cláusula)."""
818
+ depth = 0
819
+ while not self.at_eof():
820
+ t = self.cur()
821
+ if t.kind == "punct":
822
+ if t.text in "([":
823
+ depth += 1
824
+ elif t.text in ")]":
825
+ if depth == 0:
826
+ return
827
+ depth -= 1
828
+ elif (t.text == "," and depth == 0) or (t.text == ";" and depth == 0):
829
+ return
830
+ if depth == 0 and t.kind == "word" and t.norm in _EXPR_STOPS:
831
+ return
832
+ self.i += 1
833
+
834
+ # --- expressões -----------------------------------------------------
835
+
836
+ def expr(self, stops: frozenset[str]) -> ExprRefs:
837
+ """Scan de expressão coletando colrefs/stars/subqueries/dynamic.
838
+
839
+ Termina em keyword de ``stops`` ou vírgula/``;`` em depth 0, ou
840
+ em ``)`` que não abrimos. CASE/WHEN é balanceado (``case_depth``)
841
+ para não cortar expressões no meio. Keywords não-estruturais são
842
+ operadores/cláusulas internas — consumidas, nunca colrefs.
843
+
844
+ ``value_end``: após um valor completo em nível 0 (colref,
845
+ ``)`` fechando o nível, literal), uma palavra solta que não é
846
+ keyword de expressão é início de **alias implícito** — para a
847
+ expressão. Sem isso ``SELECT a b FROM`` engoliria ``b`` como
848
+ colref fantasma.
849
+ """
850
+ s = _ExprScan()
851
+ while True:
852
+ t = self.cur()
853
+ if t.kind == "eof":
854
+ break
855
+ if t.kind == "punct":
856
+ if self._expr_punct(s, t):
857
+ break
858
+ continue
859
+ if t.kind == "dynamic":
860
+ s.refs.dynamic = True
861
+ self.i += 1
862
+ continue
863
+ if t.kind in ("string", "number", "param"):
864
+ s.recent.clear()
865
+ s.value_end = True # literal completa um valor
866
+ self.i += 1
867
+ continue
868
+ if t.kind in ("word", "qident"):
869
+ if self._expr_word(s, t, stops):
870
+ break
871
+ continue
872
+ self.i += 1
873
+ return self._finish_expr(s)
874
+
875
+ def _finish_expr(self, s: _ExprScan) -> ExprRefs:
876
+ """Fecha o scan: parênses abertos = erro; lambda-bound filtrado."""
877
+ if s.lvl > 0:
878
+ # EOF com parênses abertos — nunca silencia
879
+ raise ParseError("unbalanced parens", self.cur())
880
+ # lambda-bound: refs coletados com nome ligado no mesmo nível
881
+ # (ou raso) caem — bound por nível sobrevive ao fecho dos parênses
882
+ if any(s.bound.values()):
883
+ s.refs.cols = [
884
+ col for (col, d) in s.tagged
885
+ if not (
886
+ len(col.parts) == 1
887
+ and any(
888
+ col.parts[0] in b
889
+ for k, b in s.bound.items() if k <= d
890
+ )
891
+ )
892
+ ]
893
+ return s.refs
894
+
895
+ def _expr_punct(self, s: _ExprScan, t: Token) -> bool:
896
+ """Pontuação dentro de ``expr`` — True = expressão terminou."""
897
+ if t.text == "(":
898
+ self._expr_open(s)
899
+ return False
900
+ if t.text == ")":
901
+ if s.lvl == 0:
902
+ return True # fecho do caller
903
+ s.lvl -= 1
904
+ if s.lvl == 0:
905
+ s.value_end = True # `) x` — fecho de valor → alias
906
+ self.i += 1
907
+ return False
908
+ if t.text in (",", ";") and s.lvl == 0:
909
+ return True
910
+ if t.text == ";":
911
+ # `;` dentro de parênses não-fechados — malformado
912
+ raise ParseError("unbalanced parens", t)
913
+ if t.text == "*":
914
+ self._expr_star(s, t)
915
+ return False
916
+ if t.text == "->":
917
+ s.bound.setdefault(s.lvl, set()).update(s.recent)
918
+ s.recent.clear()
919
+ s.value_end = False
920
+ self.i += 1
921
+ return False
922
+ if t.text == "::":
923
+ # cast postfixado — o tipo não é colref
924
+ self.i += 1
925
+ self._skip_type()
926
+ s.recent.clear()
927
+ s.value_end = s.lvl == 0
928
+ return False
929
+ # operador/pontuação — divide candidatos a parâmetro de
930
+ # lambda (`,`` em `f(xs, x -> ...)`) e segue a expressão
931
+ s.value_end = False
932
+ s.recent.clear()
933
+ self.i += 1
934
+ return False
935
+
936
+ def _expr_open(self, s: _ExprScan) -> None:
937
+ """``(`` dentro de ``expr``: subquery ou nível de profundidade."""
938
+ if is_kw(self.peek(), "SELECT", "WITH"):
939
+ self.i += 1
940
+ q = self.query()
941
+ self.expect_punct(")")
942
+ s.refs.subqueries.append(q)
943
+ s.recent.clear()
944
+ s.value_end = True
945
+ return
946
+ s.lvl += 1
947
+ s.recent.clear()
948
+ s.value_end = False
949
+ self.i += 1
950
+
951
+ def _expr_star(self, s: _ExprScan, t: Token) -> None:
952
+ """``*``/``t.*`` — qualificador vem do colref imediatamente anterior."""
953
+ qual: tuple[str, ...] = ()
954
+ prev = self.toks[self.i - 1] if self.i else t
955
+ if (
956
+ prev.kind == "punct" and prev.text == "."
957
+ and s.tagged and s.tagged[-1][1] == s.lvl
958
+ ):
959
+ qual = s.tagged[-1][0].parts
960
+ s.tagged.pop()
961
+ s.refs.cols.pop()
962
+ star = StarRef(qualifier=qual, span=self.span(t.start, t.end))
963
+ s.refs.stars.append(star)
964
+ if s.lvl == 0:
965
+ s.refs.top_star = star
966
+ s.value_end = True
967
+ s.recent.clear()
968
+ self.i += 1
969
+
970
+ def _expr_word(
971
+ self, s: _ExprScan, t: Token, stops: frozenset[str]
972
+ ) -> bool:
973
+ """Palavra/qident dentro de ``expr`` — True = expressão terminou."""
974
+ norm = t.norm
975
+ # keywords só atuam como keywords quando não-quotadas
976
+ if t.kind == "word":
977
+ kw = self._expr_keyword(s, t, stops)
978
+ if kw == "end":
979
+ return True
980
+ if kw == "consumed":
981
+ return False
982
+ if s.value_end and s.lvl == 0 and s.case_depth == 0:
983
+ return True # alias implícito: valor completo + palavra solta
984
+ # cadeia de identificador: part(.part)*
985
+ parts = [t.iname]
986
+ self.i += 1
987
+ while self.at_punct("."):
988
+ nx = self.peek()
989
+ if is_word(nx) or nx.kind == "number":
990
+ parts.append(nx.iname)
991
+ self.i += 2
992
+ continue
993
+ break # `.*` fica para o ramo de star
994
+ if self.at_punct("(") or self.at_punct("["):
995
+ # chamada de função/construtor — o nome não é colref
996
+ if t.kind == "word" and norm in _SPECIAL_ARG_FNS:
997
+ self._special_fn_field(norm)
998
+ s.recent.clear()
999
+ s.value_end = False
1000
+ return False
1001
+ col = ColRef(
1002
+ parts=tuple(parts),
1003
+ span=self.span(t.start, self.last_end()),
1004
+ )
1005
+ s.refs.cols.append(col)
1006
+ s.tagged.append((col, s.lvl))
1007
+ # apenas idents simples podem ser parâmetros de lambda
1008
+ s.recent.clear()
1009
+ if len(parts) == 1:
1010
+ s.recent.append(parts[0])
1011
+ s.value_end = s.lvl == 0
1012
+ return False
1013
+
1014
+ def _expr_keyword(
1015
+ self, s: _ExprScan, t: Token, stops: frozenset[str]
1016
+ ) -> str:
1017
+ """Keyword dentro de ``expr``: ``end``|``consumed``|``ident``.
1018
+
1019
+ ``ident`` = a palavra não atua como keyword aqui — o caller
1020
+ segue para a cadeia de identificador (colref/alias).
1021
+ """
1022
+ norm = t.norm
1023
+ if s.lvl == 0 and s.case_depth == 0 and norm in stops:
1024
+ return "end"
1025
+ case = self._expr_case_kw(s, norm)
1026
+ if case is not None:
1027
+ return case
1028
+ if norm == "INTERVAL":
1029
+ # INTERVAL '1' DAY / INTERVAL 1 YEAR TO MONTH
1030
+ self.i += 1
1031
+ if self.cur().kind in ("string", "number"):
1032
+ self.i += 1
1033
+ while is_word(self.cur()) and (
1034
+ self.cur().norm in _INTERVAL_UNITS
1035
+ ):
1036
+ self.i += 1
1037
+ s.recent.clear()
1038
+ s.value_end = s.lvl == 0
1039
+ return "consumed"
1040
+ if norm in _TYPE_LITERAL_WORDS and self.peek().kind == "string":
1041
+ self.i += 2 # DATE '…' — literal, não colref
1042
+ s.recent.clear()
1043
+ s.value_end = s.lvl == 0
1044
+ return "consumed"
1045
+ if norm == "AS" and s.lvl > 0:
1046
+ # CAST(x AS tipo) — pula a cadeia de tipo
1047
+ self.i += 1
1048
+ self._skip_type()
1049
+ s.recent.clear()
1050
+ return "consumed"
1051
+ if norm in KEYWORDS_FOR_EXPR:
1052
+ # operador/cláusula interna — não é colref
1053
+ self.i += 1
1054
+ s.recent.clear()
1055
+ # literais completam valor; operadores seguem
1056
+ s.value_end = norm in _EXPR_LITERALS and s.lvl == 0
1057
+ return "consumed"
1058
+ return "ident"
1059
+
1060
+ def _expr_case_kw(self, s: _ExprScan, norm: str) -> str | None:
1061
+ """CASE/WHEN/THEN/ELSE/END — balanceado dentro de ``expr``.
1062
+
1063
+ ``None`` = não é keyword de CASE — segue o dispatch normal.
1064
+ """
1065
+ if norm == "CASE":
1066
+ s.case_depth += 1
1067
+ s.value_end = False
1068
+ self.i += 1
1069
+ return "consumed"
1070
+ if norm == "END" and s.case_depth > 0:
1071
+ s.case_depth -= 1
1072
+ s.value_end = s.lvl == 0 and s.case_depth == 0
1073
+ self.i += 1
1074
+ return "consumed"
1075
+ if (
1076
+ norm in ("WHEN", "THEN", "ELSE")
1077
+ and s.case_depth == 0 and s.lvl == 0
1078
+ ):
1079
+ return "end"
1080
+ return None
1081
+
1082
+ def _special_fn_field(self, fn: str) -> None:
1083
+ """``EXTRACT(YEAR FROM …)``: após '(', salta campo de extração.
1084
+
1085
+ Só pula a primeira palavra quando ela é field-word conhecida
1086
+ (``YEAR``/``BOTH``/``LEADING``…) ou quando ``FROM``/``IN``/``FOR``
1087
+ segue num EXTRACT — ``SUBSTRING(x FROM …)`` mantém ``x``.
1088
+ """
1089
+ if not self.at_punct("("):
1090
+ return
1091
+ self.i += 1
1092
+ t = self.cur()
1093
+ if not is_word(t):
1094
+ return
1095
+ if t.norm in _FN_FIELD_WORDS or (
1096
+ fn == "EXTRACT" and is_kw(self.peek(), "FROM", "IN", "FOR")
1097
+ ):
1098
+ self.i += 1
1099
+
1100
+ def _skip_type(self) -> None:
1101
+ """Pula nome de tipo: word* [ (…) ] [ <…> ] (``x::decimal(10,2)``)."""
1102
+ while True:
1103
+ t = self.cur()
1104
+ if is_word(t) or t.kind == "number":
1105
+ self.i += 1
1106
+ if self.at_punct("."):
1107
+ self.i += 1
1108
+ continue
1109
+ if self.at_punct("("):
1110
+ self.skip_balanced()
1111
+ continue
1112
+ if self.at_punct("<"):
1113
+ depth = 0
1114
+ while not self.at_eof():
1115
+ c = self.cur()
1116
+ if c.kind == "punct" and c.text == "<":
1117
+ depth += 1
1118
+ elif c.kind == "punct" and c.text == ">":
1119
+ depth -= 1
1120
+ if depth == 0:
1121
+ self.i += 1
1122
+ break
1123
+ self.i += 1
1124
+ continue
1125
+ return
1126
+
1127
+ # --- DML -----------------------------------------------------------
1128
+
1129
+ def insert_stmt(self, ctes: list[Cte], start: Token) -> Statement:
1130
+ self.expect_kw("INSERT")
1131
+ overwrite = self.accept_kw("OVERWRITE")
1132
+ self.accept_kw("INTO")
1133
+ self.accept_kw("TABLE")
1134
+ target = self.name_chain(loose=True)
1135
+ st = SInsert(
1136
+ kind="insert",
1137
+ span=self.span(start.start, self.last_end()),
1138
+ target=target, overwrite=overwrite,
1139
+ )
1140
+ # (cols) opcional — '(' que NÃO abre select/values
1141
+ if self.at_punct("(") and not is_kw(self.peek(), "SELECT", "WITH", "VALUES"):
1142
+ mark = self.i
1143
+ try:
1144
+ st.columns = self._col_list()
1145
+ except ParseError:
1146
+ self.i = mark # talvez seja query parentizada — tenta adiante
1147
+ if self.accept_kw("PARTITION"):
1148
+ if self.at_punct("("):
1149
+ self.skip_balanced()
1150
+ self.accept_kw("IF")
1151
+ self.accept_kw("NOT")
1152
+ self.accept_kw("EXISTS")
1153
+ if self.at_kw("SELECT", "WITH") or (
1154
+ self.at_punct("(") and is_kw(self.peek(), "SELECT", "WITH")
1155
+ ):
1156
+ q = self.query()
1157
+ if ctes:
1158
+ q.ctes = ctes + q.ctes
1159
+ st.query = q
1160
+ elif self.accept_kw("VALUES"):
1161
+ while self.at_punct("("):
1162
+ st.value_rows.append(self._values_row())
1163
+ if not self.accept_punct(","):
1164
+ break
1165
+ else:
1166
+ raise ParseError("INSERT without source query", self.cur())
1167
+ st.span = self.span(start.start, self.last_end())
1168
+ return st
1169
+
1170
+ def _values_row(self) -> list[ExprRefs]:
1171
+ """``( expr, … )`` de VALUES — ExprRefs por posição."""
1172
+ self.expect_punct("(")
1173
+ row: list[ExprRefs] = []
1174
+ if self.at_punct(")"):
1175
+ self.i += 1
1176
+ return row
1177
+ while True:
1178
+ row.append(self.expr(_EXPR_STOPS))
1179
+ if self.accept_punct(","):
1180
+ continue
1181
+ self.expect_punct(")")
1182
+ return row
1183
+
1184
+ def update_stmt(self, ctes: list[Cte], start: Token) -> Statement:
1185
+ self.expect_kw("UPDATE")
1186
+ target = self.name_chain(loose=True)
1187
+ alias, _ = self.opt_alias()
1188
+ self.expect_kw("SET")
1189
+ sets: list[tuple[str, ExprRefs]] = []
1190
+ while True:
1191
+ col = self.name_chain(loose=True)
1192
+ self.expect_punct("=")
1193
+ refs = self.expr(_EXPR_STOPS)
1194
+ sets.append((col.parts[-1], refs))
1195
+ if self.accept_punct(","):
1196
+ continue
1197
+ break
1198
+ st = SUpdate(
1199
+ kind="update",
1200
+ span=self.span(start.start, self.last_end()),
1201
+ target=target, target_alias=alias, sets=sets, ctes=ctes,
1202
+ )
1203
+ if self.accept_kw("FROM"):
1204
+ st.sources = self.source_list(st.tail_subqueries)
1205
+ if self.accept_kw("WHERE"):
1206
+ refs = self.expr(_EXPR_STOPS)
1207
+ st.tail_subqueries.extend(refs.subqueries)
1208
+ st.span = self.span(start.start, self.last_end())
1209
+ return st
1210
+
1211
+ def delete_stmt(self, ctes: list[Cte], start: Token) -> Statement:
1212
+ self.expect_kw("DELETE")
1213
+ self.accept_kw("FROM")
1214
+ target = self.name_chain(loose=True)
1215
+ self.opt_alias()
1216
+ st = SDelete(
1217
+ kind="delete",
1218
+ span=self.span(start.start, self.last_end()),
1219
+ target=target, ctes=ctes,
1220
+ )
1221
+ if self.accept_kw("USING"):
1222
+ st.sources = self.source_list(st.tail_subqueries)
1223
+ if self.accept_kw("WHERE"):
1224
+ refs = self.expr(_EXPR_STOPS)
1225
+ st.tail_subqueries.extend(refs.subqueries)
1226
+ st.span = self.span(start.start, self.last_end())
1227
+ return st
1228
+
1229
+ def merge_stmt(self, ctes: list[Cte], start: Token) -> Statement:
1230
+ self.expect_kw("MERGE")
1231
+ self.accept_kw("INTO")
1232
+ target = self.name_chain(loose=True)
1233
+ alias, _ = self.opt_alias()
1234
+ self.expect_kw("USING")
1235
+ src_subs: list[SelectStmt] = []
1236
+ source = self.primary(src_subs, 0)
1237
+ self.expect_kw("ON")
1238
+ refs = self.expr(_EXPR_STOPS)
1239
+ st = SMerge(
1240
+ kind="merge",
1241
+ span=self.span(start.start, self.last_end()),
1242
+ target=target, target_alias=alias, source=source, ctes=ctes,
1243
+ tail_subqueries=[*src_subs, *refs.subqueries],
1244
+ )
1245
+ while self.at_kw("WHEN"):
1246
+ self._merge_when(st)
1247
+ st.span = self.span(start.start, self.last_end())
1248
+ return st
1249
+
1250
+ def _merge_when(self, st: SMerge) -> None:
1251
+ """``WHEN [NOT] MATCHED [BY …] [AND …] THEN <ação>``."""
1252
+ self.i += 1
1253
+ self.accept_kw("NOT")
1254
+ if not self.accept_kw("MATCHED"):
1255
+ raise ParseError("expected MATCHED in WHEN clause", self.cur())
1256
+ if self.accept_kw("BY"):
1257
+ self.accept_kw("TARGET", "SOURCE")
1258
+ if self.accept_kw("AND"):
1259
+ refs = self.expr(_EXPR_STOPS)
1260
+ st.tail_subqueries.extend(refs.subqueries)
1261
+ self.expect_kw("THEN")
1262
+ self._merge_action(st)
1263
+
1264
+ def _merge_action(self, st: SMerge) -> None:
1265
+ """Ação do WHEN: ``UPDATE SET`` / ``DELETE`` / ``INSERT VALUES``."""
1266
+ t = self.cur()
1267
+ if self.accept_kw("UPDATE"):
1268
+ self.expect_kw("SET")
1269
+ while True:
1270
+ col = self.name_chain(loose=True)
1271
+ self.expect_punct("=")
1272
+ refs = self.expr(_EXPR_STOPS)
1273
+ st.sets.append((col.parts[-1], refs))
1274
+ if self.accept_punct(","):
1275
+ continue
1276
+ break
1277
+ return
1278
+ if self.accept_kw("DELETE"):
1279
+ return
1280
+ if self.accept_kw("INSERT"):
1281
+ if self.at_punct("(") and not is_kw(
1282
+ self.peek(), "SELECT", "WITH", "VALUES"
1283
+ ):
1284
+ st.insert_cols = self._col_list()
1285
+ self.expect_kw("VALUES")
1286
+ st.insert_exprs = self._values_row()
1287
+ return
1288
+ if self.accept_kw("DO"):
1289
+ self.accept_kw("NOTHING")
1290
+ return
1291
+ raise ParseError("unsupported WHEN clause", t)
1292
+
1293
+ # --- DDL --------------------------------------------------------------
1294
+
1295
+ def create_stmt(self, start: Token) -> Statement:
1296
+ self.expect_kw("CREATE")
1297
+ or_replace = False
1298
+ temporary = False
1299
+ external = False
1300
+ materialized = False
1301
+ while True:
1302
+ if self.accept_kw("OR"):
1303
+ self.expect_kw("REPLACE")
1304
+ or_replace = True
1305
+ elif self.accept_kw("TEMP", "TEMPORARY", "GLOBAL", "TRANSIENT"):
1306
+ temporary = True
1307
+ elif self.accept_kw("EXTERNAL"):
1308
+ external = True
1309
+ elif self.accept_kw("MATERIALIZED"):
1310
+ materialized = True
1311
+ elif self.accept_kw("UNIQUE"):
1312
+ pass
1313
+ else:
1314
+ break
1315
+ if self.accept_kw("TABLE"):
1316
+ return self.create_table(
1317
+ start, or_replace=or_replace, temporary=temporary,
1318
+ external=external,
1319
+ )
1320
+ if self.accept_kw("VIEW"):
1321
+ return self.create_view(
1322
+ start, or_replace=or_replace, temporary=temporary,
1323
+ materialized=materialized,
1324
+ )
1325
+ t = self.cur()
1326
+ if is_kw(t, *_CREATE_SUB):
1327
+ st = SUnsupported(
1328
+ kind="unsupported", span=self.span(start.start, t.end),
1329
+ why=f"create-{t.norm.lower()}", head=t.norm,
1330
+ )
1331
+ self.skip_to_boundary()
1332
+ return st
1333
+ raise ParseError("unsupported CREATE target", t)
1334
+
1335
+ def create_table(
1336
+ self, start: Token, *, or_replace: bool, temporary: bool, external: bool
1337
+ ) -> Statement:
1338
+ self.accept_kw("IF")
1339
+ self.accept_kw("NOT")
1340
+ self.accept_kw("EXISTS")
1341
+ name = self.name_chain(loose=True)
1342
+ st = SCreateTable(
1343
+ kind="create-table",
1344
+ span=self.span(start.start, self.last_end()),
1345
+ name=name, or_replace=or_replace, temporary=temporary,
1346
+ external=external,
1347
+ )
1348
+ # primeira lista de parênteses logo após o nome = definição de colunas
1349
+ if self.at_punct("(") and not is_kw(self.peek(), "SELECT", "WITH", "VALUES"):
1350
+ mark = self.i
1351
+ try:
1352
+ st.columns = self._coldefs()
1353
+ except ParseError:
1354
+ self.i = mark
1355
+ self.skip_balanced()
1356
+ # opções de tabela até AS (CTAS) ou fim do statement
1357
+ prev_kw = ""
1358
+ while not self.at_eof() and not self.at_punct(";"):
1359
+ t = self.cur()
1360
+ if self.at_kw("AS") and prev_kw != "STORED":
1361
+ self.i += 1
1362
+ st.query = self.query()
1363
+ break
1364
+ if self.at_kw("LIKE") and st.like is None:
1365
+ self.i += 1
1366
+ st.like = self.name_chain(loose=True)
1367
+ prev_kw = "LIKE"
1368
+ continue
1369
+ if t.kind == "punct" and t.text == "(":
1370
+ self.skip_balanced()
1371
+ continue
1372
+ if t.kind == "word":
1373
+ prev_kw = t.norm
1374
+ self.i += 1
1375
+ # resíduo de opções após o SELECT (WITH NO DATA, ENGINE, …)
1376
+ while not self.at_eof() and not self.at_punct(";"):
1377
+ self.i += 1
1378
+ st.span = self.span(start.start, self.last_end())
1379
+ return st
1380
+
1381
+ def _coldefs(self) -> list[tuple[str, str]]:
1382
+ """``( coldef|constraint, … )`` — coldef = nome + texto de tipo."""
1383
+ self.expect_punct("(")
1384
+ cols: list[tuple[str, str]] = []
1385
+ if self.at_punct(")"):
1386
+ self.i += 1
1387
+ return cols
1388
+ while True:
1389
+ t = self.cur()
1390
+ if is_kw(t, *_CONSTRAINT_HEAD) or is_kw(t, "LIKE"):
1391
+ self._skip_table_constraint()
1392
+ else:
1393
+ cols.append(self._coldef_item())
1394
+ if self.accept_punct(","):
1395
+ continue
1396
+ self.expect_punct(")")
1397
+ return cols
1398
+
1399
+ def _skip_table_constraint(self) -> None:
1400
+ """Constraint de tabela — consome o item inteiro até ``,``/``)``."""
1401
+ while (
1402
+ not self.at_eof()
1403
+ and not self.at_punct(",")
1404
+ and not self.at_punct(")")
1405
+ ):
1406
+ if self.at_punct("("):
1407
+ self.skip_balanced()
1408
+ continue
1409
+ self.i += 1
1410
+
1411
+ def _coldef_item(self) -> tuple[str, str]:
1412
+ """``nome tipo[…]`` — o tipo é preservado como texto bruto."""
1413
+ t = self.cur()
1414
+ if not is_word(t):
1415
+ raise ParseError("expected column definition", t)
1416
+ cname = t.iname
1417
+ self.i += 1
1418
+ type_parts: list[str] = []
1419
+ depth = 0
1420
+ while not self.at_eof():
1421
+ c = self.cur()
1422
+ if c.kind == "punct" and c.text == "(":
1423
+ depth += 1
1424
+ elif c.kind == "punct" and c.text == ")":
1425
+ if depth == 0:
1426
+ break
1427
+ depth -= 1
1428
+ elif c.kind == "punct" and c.text == "," and depth == 0:
1429
+ break
1430
+ type_parts.append(c.text)
1431
+ self.i += 1
1432
+ return cname, " ".join(type_parts)
1433
+
1434
+ def create_view(
1435
+ self, start: Token, *, or_replace: bool, temporary: bool, materialized: bool
1436
+ ) -> Statement:
1437
+ self.accept_kw("IF")
1438
+ self.accept_kw("NOT")
1439
+ self.accept_kw("EXISTS")
1440
+ name = self.name_chain(loose=True)
1441
+ st = SCreateView(
1442
+ kind="create-view",
1443
+ span=self.span(start.start, self.last_end()),
1444
+ name=name, or_replace=or_replace, temporary=temporary,
1445
+ materialized=materialized,
1446
+ )
1447
+ if self.at_punct("(") and not is_kw(self.peek(), "SELECT", "WITH"):
1448
+ st.col_aliases = self._col_list()
1449
+ # opções até AS — COMMENT/TBLPROPERTIES são comuns em views
1450
+ while not self.at_eof() and not self.at_punct(";"):
1451
+ if self.at_kw("AS"):
1452
+ self.i += 1
1453
+ st.query = self.query()
1454
+ break
1455
+ if self.at_punct("("):
1456
+ self.skip_balanced()
1457
+ continue
1458
+ self.i += 1
1459
+ if st.query is None:
1460
+ raise ParseError("CREATE VIEW without AS", self.cur())
1461
+ # resíduo de opções após o SELECT (WITH CHECK OPTION, …)
1462
+ while not self.at_eof() and not self.at_punct(";"):
1463
+ self.i += 1
1464
+ st.span = self.span(start.start, self.last_end())
1465
+ return st
1466
+
1467
+ def prepare_stmt(self, start: Token) -> Statement:
1468
+ self.expect_kw("PREPARE")
1469
+ t = self.cur()
1470
+ if not is_word(t):
1471
+ raise ParseError("expected prepared statement name", t)
1472
+ name = t.iname
1473
+ self.i += 1
1474
+ self.expect_kw("FROM")
1475
+ if self.at_kw("SELECT", "WITH"):
1476
+ q = self.query()
1477
+ return SSelect(
1478
+ kind="select",
1479
+ span=self.span(start.start, self.last_end()),
1480
+ query=q, prepared_name=name,
1481
+ )
1482
+ raise ParseError("PREPARE with non-SELECT body", self.cur())
1483
+
1484
+
1485
+ # Keywords aceitas DENTRO de expressões (operadores, cláusulas internas,
1486
+ # keywords de função). Separado de _EXPR_STOPS: estas não terminam o
1487
+ # scan — apenas são consumidas sem virar colrefs.
1488
+ KEYWORDS_FOR_EXPR: frozenset[str] = frozenset(
1489
+ {
1490
+ "AND", "OR", "NOT", "IN", "IS", "NULL", "LIKE", "ILIKE", "RLIKE",
1491
+ "REGEXP", "BETWEEN", "EXISTS", "CAST", "TRY_CAST", "AS",
1492
+ "DISTINCT", "ALL", "CASE", "WHEN", "THEN", "ELSE", "END",
1493
+ "OVER", "FILTER", "WITHIN", "PARTITION", "ORDER", "BY", "ASC",
1494
+ "DESC", "NULLS", "FIRST", "LAST", "GROUPING", "ROLLUP", "CUBE",
1495
+ "SETS", "UNNEST", "ORDINALITY", "FROM", "FOR", "SEPARATOR",
1496
+ "RESPECT", "IGNORE", "RANGE", "UNBOUNDED", "PRECEDING",
1497
+ "FOLLOWING", "CURRENT", "TIES", "OTHERS", "EXCLUDE", "GROUPS",
1498
+ "ARRAY", "MAP", "STRUCT", "ROW", "ESCAPE", "SIMILAR",
1499
+ }
1500
+ )
1501
+
1502
+ # Keywords que completam um valor (literais de SQL) — após elas, uma
1503
+ # palavra solta é alias implícito, não continuação da expressão.
1504
+ _EXPR_LITERALS: frozenset[str] = frozenset(
1505
+ {
1506
+ "NULL", "TRUE", "FALSE", "DEFAULT", "CURRENT_DATE",
1507
+ "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER",
1508
+ "SESSION_USER", "LOCALTIME", "LOCALTIMESTAMP", "USER",
1509
+ }
1510
+ )