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.
- code_atlas/__init__.py +6 -0
- code_atlas/__main__.py +6 -0
- code_atlas/api/__init__.py +5 -0
- code_atlas/api/handlers.py +1514 -0
- code_atlas/api/server.py +394 -0
- code_atlas/cli.py +2084 -0
- code_atlas/config.py +149 -0
- code_atlas/context/__init__.py +66 -0
- code_atlas/context/contracts.py +277 -0
- code_atlas/context/domain.py +760 -0
- code_atlas/context/engine.py +442 -0
- code_atlas/context/expand.py +133 -0
- code_atlas/context/renderers.py +71 -0
- code_atlas/context/tokenizer.py +37 -0
- code_atlas/contracts/__init__.py +38 -0
- code_atlas/contracts/enums.py +67 -0
- code_atlas/contracts/models.py +175 -0
- code_atlas/contracts/ports.py +70 -0
- code_atlas/discovery/__init__.py +25 -0
- code_atlas/discovery/gitignore.py +119 -0
- code_atlas/discovery/identity.py +52 -0
- code_atlas/discovery/models.py +204 -0
- code_atlas/discovery/scanner.py +495 -0
- code_atlas/domains/__init__.py +107 -0
- code_atlas/domains/api/__init__.py +28 -0
- code_atlas/domains/api/adapter.py +405 -0
- code_atlas/domains/api/common.py +308 -0
- code_atlas/domains/api/consumers.py +224 -0
- code_atlas/domains/api/emit.py +231 -0
- code_atlas/domains/api/openapi.py +398 -0
- code_atlas/domains/api/routes.py +582 -0
- code_atlas/domains/api/yaml_subset.py +683 -0
- code_atlas/domains/aws/__init__.py +28 -0
- code_atlas/domains/aws/adapter.py +382 -0
- code_atlas/domains/aws/common.py +261 -0
- code_atlas/domains/aws/configs.py +1164 -0
- code_atlas/domains/aws/emit.py +411 -0
- code_atlas/domains/aws/pyfiles.py +2220 -0
- code_atlas/domains/aws/sqlbridge.py +448 -0
- code_atlas/domains/cicd/__init__.py +50 -0
- code_atlas/domains/cicd/adapter.py +762 -0
- code_atlas/domains/cicd/classify.py +136 -0
- code_atlas/domains/cicd/workflow.py +504 -0
- code_atlas/domains/cicd/yamlmini.py +946 -0
- code_atlas/domains/cli_surface.py +739 -0
- code_atlas/domains/contracts.py +274 -0
- code_atlas/domains/crossresolve.py +1208 -0
- code_atlas/domains/evidence.py +57 -0
- code_atlas/domains/identity.py +149 -0
- code_atlas/domains/model.py +202 -0
- code_atlas/domains/registry.py +250 -0
- code_atlas/domains/resolution.py +65 -0
- code_atlas/domains/spark/__init__.py +31 -0
- code_atlas/domains/spark/adapter.py +146 -0
- code_atlas/domains/spark/emit.py +344 -0
- code_atlas/domains/spark/extract.py +108 -0
- code_atlas/domains/spark/sqllit.py +192 -0
- code_atlas/domains/spark/walk.py +1943 -0
- code_atlas/domains/sql/__init__.py +30 -0
- code_atlas/domains/sql/adapter.py +382 -0
- code_atlas/domains/sql/extract.py +1271 -0
- code_atlas/domains/sql/nodes.py +264 -0
- code_atlas/domains/sql/parser.py +1510 -0
- code_atlas/domains/sql/tokens.py +318 -0
- code_atlas/domains/summarize.py +126 -0
- code_atlas/domains/terraform/__init__.py +20 -0
- code_atlas/domains/terraform/adapter.py +127 -0
- code_atlas/domains/terraform/catalog.py +43 -0
- code_atlas/domains/terraform/extract.py +1277 -0
- code_atlas/domains/terraform/hcl.py +538 -0
- code_atlas/domains/terraform/refs.py +181 -0
- code_atlas/domains/terraform/tfjson.py +163 -0
- code_atlas/domains/terraform/tfvars.py +98 -0
- code_atlas/domains/validation.py +120 -0
- code_atlas/env_override.py +27 -0
- code_atlas/eval/__init__.py +26 -0
- code_atlas/eval/domain_eval.py +681 -0
- code_atlas/eval/ground_truth.py +688 -0
- code_atlas/eval/impact_truth.py +758 -0
- code_atlas/eval/metrics.py +178 -0
- code_atlas/eval/pack_value.py +610 -0
- code_atlas/federation/__init__.py +72 -0
- code_atlas/federation/federation.py +676 -0
- code_atlas/federation/model.py +355 -0
- code_atlas/federation/store.py +194 -0
- code_atlas/gitbin.py +48 -0
- code_atlas/graph/__init__.py +20 -0
- code_atlas/graph/builder.py +378 -0
- code_atlas/graph/identity.py +54 -0
- code_atlas/graph/queries.py +47 -0
- code_atlas/graph/validation.py +92 -0
- code_atlas/impact/__init__.py +37 -0
- code_atlas/impact/blast_radius.py +171 -0
- code_atlas/impact/contract_analyzer.py +104 -0
- code_atlas/impact/contracts.py +315 -0
- code_atlas/impact/diff_reader.py +142 -0
- code_atlas/impact/domain_traversal.py +768 -0
- code_atlas/impact/engine.py +392 -0
- code_atlas/impact/packs.py +128 -0
- code_atlas/impact/report.py +136 -0
- code_atlas/impact/revision_binding.py +220 -0
- code_atlas/impact/risk_classifier.py +83 -0
- code_atlas/impact/snapshot_resolver.py +136 -0
- code_atlas/impact/symbol_diff.py +210 -0
- code_atlas/impact/test_selector.py +166 -0
- code_atlas/indexing/__init__.py +5 -0
- code_atlas/indexing/cache.py +396 -0
- code_atlas/indexing/change_detector.py +278 -0
- code_atlas/indexing/coordinator.py +1032 -0
- code_atlas/indexing/dependency_planner.py +268 -0
- code_atlas/indexing/persist.py +436 -0
- code_atlas/indexing/reuse.py +85 -0
- code_atlas/install/__init__.py +7 -0
- code_atlas/install/cli.py +168 -0
- code_atlas/install/lifecycle.py +286 -0
- code_atlas/mcp.py +674 -0
- code_atlas/observability/__init__.py +19 -0
- code_atlas/observability/logging.py +199 -0
- code_atlas/parsers/__init__.py +43 -0
- code_atlas/parsers/contracts.py +209 -0
- code_atlas/parsers/engine.py +125 -0
- code_atlas/parsers/languages/__init__.py +1 -0
- code_atlas/parsers/languages/python.py +391 -0
- code_atlas/parsers/languages/tsx.py +28 -0
- code_atlas/parsers/languages/typescript.py +909 -0
- code_atlas/parsers/registry.py +51 -0
- code_atlas/parsers/tree_sitter_runtime.py +118 -0
- code_atlas/resolvers/__init__.py +33 -0
- code_atlas/resolvers/bootstrap.py +246 -0
- code_atlas/resolvers/capabilities.py +33 -0
- code_atlas/resolvers/contracts.py +133 -0
- code_atlas/resolvers/coordinator.py +96 -0
- code_atlas/resolvers/enrichment.py +318 -0
- code_atlas/resolvers/lsp/__init__.py +26 -0
- code_atlas/resolvers/lsp/adapter.py +256 -0
- code_atlas/resolvers/lsp/lifecycle.py +251 -0
- code_atlas/resolvers/lsp/spawn.py +92 -0
- code_atlas/resolvers/lsp/transport.py +240 -0
- code_atlas/resolvers/registry.py +33 -0
- code_atlas/resolvers/scip/__init__.py +49 -0
- code_atlas/resolvers/scip/decoder.py +216 -0
- code_atlas/resolvers/scip/importer.py +248 -0
- code_atlas/resolvers/scip/loader.py +90 -0
- code_atlas/resolvers/scip/mapper.py +271 -0
- code_atlas/resolvers/scip/provenance.py +211 -0
- code_atlas/resolvers/scip/resolver.py +289 -0
- code_atlas/resolvers/scip/validation.py +101 -0
- code_atlas/resolvers/syntactic/__init__.py +8 -0
- code_atlas/resolvers/syntactic/resolver.py +254 -0
- code_atlas/search/__init__.py +32 -0
- code_atlas/search/contracts.py +176 -0
- code_atlas/search/domain.py +561 -0
- code_atlas/search/engine.py +530 -0
- code_atlas/search/federated.py +276 -0
- code_atlas/search/graph.py +282 -0
- code_atlas/search/hybrid.py +151 -0
- code_atlas/search/intent.py +48 -0
- code_atlas/search/lexical.py +176 -0
- code_atlas/search/refs.py +359 -0
- code_atlas/search/structural.py +505 -0
- code_atlas/search/tokenizer.py +43 -0
- code_atlas/search/vector/__init__.py +35 -0
- code_atlas/search/vector/contracts.py +76 -0
- code_atlas/search/vector/embeddings.py +96 -0
- code_atlas/search/vector/index.py +169 -0
- code_atlas/search/vector/provider.py +50 -0
- code_atlas/storage/__init__.py +14 -0
- code_atlas/storage/db.py +47 -0
- code_atlas/storage/migrations.py +552 -0
- code_atlas/storage/store.py +1045 -0
- code_atlas/ui/__init__.py +29 -0
- code_atlas/ui/i18n.py +143 -0
- code_atlas/ui/kit.py +689 -0
- code_atlas/ui/wizard.py +317 -0
- code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
- code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
- code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
- code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
- code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
- code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
- 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
|
+
)
|