@tablecraft-app/plugin-api 0.0.0-stage → 1.1.0
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.
- package/LICENSE +21 -0
- package/README.md +46 -2
- package/dist/catalog.d.ts +112 -0
- package/dist/catalog.js +5 -0
- package/dist/completion.d.ts +109 -0
- package/dist/completion.js +12 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.js +106 -0
- package/dist/explain.d.ts +58 -0
- package/dist/explain.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/inspection.d.ts +38 -0
- package/dist/inspection.js +21 -0
- package/dist/manifest.d.ts +186 -0
- package/dist/manifest.js +176 -0
- package/dist/objects.d.ts +140 -0
- package/dist/objects.js +2 -0
- package/dist/plugin.d.ts +460 -0
- package/dist/plugin.js +49 -0
- package/dist/sessions.d.ts +59 -0
- package/dist/sessions.js +2 -0
- package/dist/sql/classify.d.ts +19 -0
- package/dist/sql/classify.js +95 -0
- package/dist/sql/complete.d.ts +23 -0
- package/dist/sql/complete.js +480 -0
- package/dist/sql/explain.d.ts +42 -0
- package/dist/sql/explain.js +108 -0
- package/dist/sql/format.d.ts +26 -0
- package/dist/sql/format.js +117 -0
- package/dist/sql/functions.d.ts +47 -0
- package/dist/sql/functions.js +159 -0
- package/dist/sql/generate.d.ts +22 -0
- package/dist/sql/generate.js +64 -0
- package/dist/sql/index.d.ts +16 -0
- package/dist/sql/index.js +16 -0
- package/dist/sql/inspect.d.ts +10 -0
- package/dist/sql/inspect.js +134 -0
- package/dist/sql/inspectContext.d.ts +78 -0
- package/dist/sql/inspectContext.js +251 -0
- package/dist/sql/inspectResolve.d.ts +6 -0
- package/dist/sql/inspectResolve.js +391 -0
- package/dist/sql/inspectStyle.d.ts +3 -0
- package/dist/sql/inspectStyle.js +384 -0
- package/dist/sql/inspectSyntax.d.ts +10 -0
- package/dist/sql/inspectSyntax.js +179 -0
- package/dist/sql/layout.d.ts +8 -0
- package/dist/sql/layout.js +114 -0
- package/dist/sql/layoutNodes.d.ts +46 -0
- package/dist/sql/layoutNodes.js +317 -0
- package/dist/sql/layoutPrint.d.ts +60 -0
- package/dist/sql/layoutPrint.js +806 -0
- package/dist/sql/layoutWriter.d.ts +21 -0
- package/dist/sql/layoutWriter.js +77 -0
- package/dist/sql/literals.d.ts +28 -0
- package/dist/sql/literals.js +78 -0
- package/dist/sql/namespace.d.ts +41 -0
- package/dist/sql/namespace.js +220 -0
- package/dist/sql/objects.d.ts +42 -0
- package/dist/sql/objects.js +222 -0
- package/dist/sql/parameters.d.ts +20 -0
- package/dist/sql/parameters.js +88 -0
- package/dist/sql/resolution.d.ts +73 -0
- package/dist/sql/resolution.js +192 -0
- package/dist/sql/routines.d.ts +27 -0
- package/dist/sql/routines.js +42 -0
- package/dist/sql/scope.d.ts +117 -0
- package/dist/sql/scope.js +540 -0
- package/dist/sql/select.d.ts +41 -0
- package/dist/sql/select.js +287 -0
- package/dist/sql/signature.d.ts +18 -0
- package/dist/sql/signature.js +137 -0
- package/dist/sql/splitter.d.ts +15 -0
- package/dist/sql/splitter.js +217 -0
- package/dist/sql/suggest.d.ts +6 -0
- package/dist/sql/suggest.js +36 -0
- package/dist/sql/tokens.d.ts +62 -0
- package/dist/sql/tokens.js +227 -0
- package/dist/sql/words.d.ts +23 -0
- package/dist/sql/words.js +72 -0
- package/dist/testing/index.d.ts +177 -0
- package/dist/testing/index.js +2070 -0
- package/dist/tls.d.ts +33 -0
- package/dist/tls.js +118 -0
- package/dist/url.d.ts +52 -0
- package/dist/url.js +219 -0
- package/dist/values.d.ts +44 -0
- package/dist/values.js +44 -0
- package/dist/version.d.ts +22 -0
- package/dist/version.js +29 -0
- package/package.json +42 -4
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { problem, replacement } from "./inspectContext.js";
|
|
2
|
+
import { levels, MAX_EXPANSION_DEPTH, referenceName, visibleCtes } from "./resolution.js";
|
|
3
|
+
import { CLAUSE_WORDS, unquote } from "./scope.js";
|
|
4
|
+
import { suggestions } from "./suggest.js";
|
|
5
|
+
const RESOLVE = "SqlResolve";
|
|
6
|
+
const AMBIGUOUS = "SqlAmbiguousColumn";
|
|
7
|
+
const RESERVED = "SqlReservedWordAsName";
|
|
8
|
+
const OPEN = "(";
|
|
9
|
+
const DOT = ".";
|
|
10
|
+
const STAR = "*";
|
|
11
|
+
const COMMA = ",";
|
|
12
|
+
const AS = "AS";
|
|
13
|
+
const USING = "USING";
|
|
14
|
+
const NATURAL = "NATURAL";
|
|
15
|
+
/** Clauses whose names are column references. */
|
|
16
|
+
const COLUMN_CLAUSES = new Set([
|
|
17
|
+
"SELECT",
|
|
18
|
+
"WHERE",
|
|
19
|
+
"ON",
|
|
20
|
+
"HAVING",
|
|
21
|
+
"SET",
|
|
22
|
+
"RETURNING",
|
|
23
|
+
USING,
|
|
24
|
+
"ORDER BY",
|
|
25
|
+
"GROUP BY",
|
|
26
|
+
"PARTITION BY",
|
|
27
|
+
]);
|
|
28
|
+
/** Symbols right before a word that make it a parameter or variable: `:name`, `@var`, `x::type`. */
|
|
29
|
+
const PARAMETER_MARKS = new Set([":", "@", "?"]);
|
|
30
|
+
const PARAMETER_START = /^[$@:]/;
|
|
31
|
+
/** Rows a statement may name without declaring them: PostgreSQL's `EXCLUDED`, a trigger's `NEW` and `OLD`. */
|
|
32
|
+
const PSEUDO_RELATIONS = new Set(["EXCLUDED", "NEW", "OLD"]);
|
|
33
|
+
const RELATION_INTRODUCERS = new Set(["FROM", "JOIN", "UPDATE", "INTO", "STRAIGHT_JOIN"]);
|
|
34
|
+
/** Words that may follow a relation introducer without naming a relation. */
|
|
35
|
+
const NOT_RELATION_NAMES = new Set([
|
|
36
|
+
"DUAL",
|
|
37
|
+
"LATERAL",
|
|
38
|
+
"ONLY",
|
|
39
|
+
"IGNORE",
|
|
40
|
+
"LOW_PRIORITY",
|
|
41
|
+
"QUICK",
|
|
42
|
+
"DELAYED",
|
|
43
|
+
"HIGH_PRIORITY",
|
|
44
|
+
"OUTFILE",
|
|
45
|
+
"DUMPFILE",
|
|
46
|
+
]);
|
|
47
|
+
const NOT_TABLE_UPDATES = new Set(["KEY", "FOR"]);
|
|
48
|
+
/** How many other namespaces an unresolved table is looked for in, so one typo cannot describe a whole server. */
|
|
49
|
+
const MAX_QUALIFY_NAMESPACES = 8;
|
|
50
|
+
const FROM_CLAUSE = "FROM";
|
|
51
|
+
function suggestionFixes(view, name, candidates) {
|
|
52
|
+
return suggestions(name.name, candidates).map((candidate) => replacement(`Did you mean '${candidate}'?`, name, view.quoted(candidate)));
|
|
53
|
+
}
|
|
54
|
+
/** The columns a source exposes, or null when the catalog or the SQL leaves them unknown. */
|
|
55
|
+
async function knownColumns(view, source, query, depth = 0) {
|
|
56
|
+
if (depth > MAX_EXPANSION_DEPTH)
|
|
57
|
+
return null;
|
|
58
|
+
if (source.kind === "derived") {
|
|
59
|
+
if (view.isSymbol(view.nextIndex(source.to), OPEN))
|
|
60
|
+
return null;
|
|
61
|
+
return selectColumns(view, source.query, depth + 1);
|
|
62
|
+
}
|
|
63
|
+
if (view.roleOf(source) !== "relation")
|
|
64
|
+
return null;
|
|
65
|
+
if (source.qualifier.length === 0) {
|
|
66
|
+
const cte = view.rules.cteNamed(query, source.name);
|
|
67
|
+
if (cte !== undefined) {
|
|
68
|
+
if (cte.columns !== null)
|
|
69
|
+
return cte.columns.map((column) => column.name);
|
|
70
|
+
return selectColumns(view, cte.query, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const found = await view.lookup.relation(source.qualifier, source.name);
|
|
74
|
+
return found === null ? null : found.relation.columns.map((column) => column.name);
|
|
75
|
+
}
|
|
76
|
+
async function selectColumns(view, query, depth) {
|
|
77
|
+
if (query.opaqueSelect || query.select.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
const columns = [];
|
|
80
|
+
for (const item of query.select) {
|
|
81
|
+
if (item.kind === "named") {
|
|
82
|
+
columns.push(item.name.name);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const sources = query.sources.filter((source) => {
|
|
86
|
+
if (item.qualifier === null)
|
|
87
|
+
return true;
|
|
88
|
+
const reference = referenceName(source);
|
|
89
|
+
return reference !== null && view.rules.sameIdentifier(reference, item.qualifier);
|
|
90
|
+
});
|
|
91
|
+
for (const source of sources) {
|
|
92
|
+
const found = await knownColumns(view, source, query, depth);
|
|
93
|
+
if (found === null)
|
|
94
|
+
return null;
|
|
95
|
+
columns.push(...found);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return columns;
|
|
99
|
+
}
|
|
100
|
+
/** The sources whose columns a level's expressions read: not an `INTO` target, nor a misread `UPDATE`. */
|
|
101
|
+
function readSources(view, query) {
|
|
102
|
+
return query.sources.filter((source) => {
|
|
103
|
+
if (source.kind === "derived")
|
|
104
|
+
return true;
|
|
105
|
+
const role = view.roleOf(source);
|
|
106
|
+
return role === "relation" || role === "function";
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** A name token, keywords included: after a dot even a reserved word is a name. */
|
|
110
|
+
function anyName(view, index) {
|
|
111
|
+
const token = view.token(index);
|
|
112
|
+
if (token === undefined)
|
|
113
|
+
return null;
|
|
114
|
+
const raw = view.raw(index);
|
|
115
|
+
if (token.kind === "quoted")
|
|
116
|
+
return { name: unquote(raw), quoted: true, from: token.from, to: token.to };
|
|
117
|
+
if (token.kind !== "word" || /^[0-9]/.test(raw))
|
|
118
|
+
return null;
|
|
119
|
+
return { name: raw, quoted: false, from: token.from, to: token.to };
|
|
120
|
+
}
|
|
121
|
+
function isParameter(view, index) {
|
|
122
|
+
const before = view.token(index - 1);
|
|
123
|
+
return (PARAMETER_START.test(view.raw(index)) ||
|
|
124
|
+
(before !== undefined && before.kind === "symbol" && PARAMETER_MARKS.has(view.raw(index - 1))));
|
|
125
|
+
}
|
|
126
|
+
/** Whether the token before `index` ends an expression, so a name here is that expression's alias. */
|
|
127
|
+
function followsOperand(view, index) {
|
|
128
|
+
const before = view.token(index - 1);
|
|
129
|
+
if (before === undefined)
|
|
130
|
+
return false;
|
|
131
|
+
if (before.kind === "quoted" || before.kind === "string")
|
|
132
|
+
return true;
|
|
133
|
+
if (before.kind === "symbol")
|
|
134
|
+
return view.raw(index - 1) === ")";
|
|
135
|
+
if (before.kind !== "word")
|
|
136
|
+
return false;
|
|
137
|
+
const word = view.raw(index - 1);
|
|
138
|
+
return /^[0-9]/.test(word) || !view.isKeyword(word);
|
|
139
|
+
}
|
|
140
|
+
async function unresolvedRelation(view, query, source) {
|
|
141
|
+
if (view.roleOf(source) === "notRelation" || view.roleOf(source) === "function")
|
|
142
|
+
return [];
|
|
143
|
+
if (view.verb === "SELECT" && view.roleOf(source) === "target")
|
|
144
|
+
return [];
|
|
145
|
+
if (source.qualifier.length === 0 && view.rules.cteNamed(query, source.name) !== undefined)
|
|
146
|
+
return [];
|
|
147
|
+
if (!source.name.quoted && NOT_RELATION_NAMES.has(source.name.name.toUpperCase()))
|
|
148
|
+
return [];
|
|
149
|
+
const { lookup } = view;
|
|
150
|
+
let path = lookup.current;
|
|
151
|
+
if (source.qualifier.length > 0) {
|
|
152
|
+
const namespace = await lookup.namespace(source.qualifier);
|
|
153
|
+
if (namespace === undefined) {
|
|
154
|
+
const namespaces = (await lookup.namespaces()).filter((candidate) => !candidate.system);
|
|
155
|
+
const owner = source.qualifier.at(-1);
|
|
156
|
+
if (namespaces.length === 0 || owner === undefined)
|
|
157
|
+
return [];
|
|
158
|
+
const kind = namespaces[0]?.kind ?? "schema";
|
|
159
|
+
const fixes = suggestionFixes(view, owner, namespaces.map((candidate) => candidate.path.at(-1) ?? candidate.name));
|
|
160
|
+
return [problem(RESOLVE, "error", owner, `Unable to resolve ${kind} '${owner.name}'`, fixes)];
|
|
161
|
+
}
|
|
162
|
+
path = namespace.path;
|
|
163
|
+
}
|
|
164
|
+
if (path === null)
|
|
165
|
+
return [];
|
|
166
|
+
const relations = await lookup.relations(path);
|
|
167
|
+
if (relations.length === 0 || (await lookup.relation(source.qualifier, source.name)) !== null)
|
|
168
|
+
return [];
|
|
169
|
+
const candidates = [
|
|
170
|
+
...relations.map((relation) => relation.name),
|
|
171
|
+
...(source.qualifier.length === 0 ? visibleCtes(query).map((cte) => cte.name.name) : []),
|
|
172
|
+
];
|
|
173
|
+
const fixes = suggestionFixes(view, source.name, candidates);
|
|
174
|
+
if (source.qualifier.length === 0)
|
|
175
|
+
fixes.push(...(await qualifyFixes(view, source.name)));
|
|
176
|
+
return [problem(RESOLVE, "error", source.name, `Unable to resolve table '${source.name.name}'`, fixes)];
|
|
177
|
+
}
|
|
178
|
+
/** "Qualify with `s`" for each other namespace that holds the table. */
|
|
179
|
+
async function qualifyFixes(view, name) {
|
|
180
|
+
const { lookup } = view;
|
|
181
|
+
const current = JSON.stringify(lookup.current);
|
|
182
|
+
const others = (await lookup.namespaces())
|
|
183
|
+
.filter((namespace) => !namespace.system && JSON.stringify(namespace.path) !== current)
|
|
184
|
+
.slice(0, MAX_QUALIFY_NAMESPACES);
|
|
185
|
+
const fixes = [];
|
|
186
|
+
for (const namespace of others) {
|
|
187
|
+
const relations = await lookup.relations(namespace.path);
|
|
188
|
+
if (!relations.some((relation) => view.rules.denotes(name, relation.name)))
|
|
189
|
+
continue;
|
|
190
|
+
const owner = view.quoted(namespace.path.at(-1) ?? namespace.name);
|
|
191
|
+
fixes.push({ title: `Qualify with '${owner}'`, edits: [{ from: name.from, to: name.from, insert: `${owner}.` }] });
|
|
192
|
+
}
|
|
193
|
+
return fixes;
|
|
194
|
+
}
|
|
195
|
+
/** Whether the catalog knows the current namespace's tables; without it every name stays unreported. */
|
|
196
|
+
async function catalogAnswers(view) {
|
|
197
|
+
const current = view.lookup.current;
|
|
198
|
+
return current !== null && (await view.lookup.relations(current)).length > 0;
|
|
199
|
+
}
|
|
200
|
+
async function readingsOf(view, query) {
|
|
201
|
+
return Promise.all(levels(query).map((level) => Promise.all(readSources(view, level).map(async (source) => ({
|
|
202
|
+
source,
|
|
203
|
+
columns: await knownColumns(view, source, level),
|
|
204
|
+
})))));
|
|
205
|
+
}
|
|
206
|
+
/** Names the select lists give their expressions: `x AS name` or `x name`, not a bare column. */
|
|
207
|
+
function selectAliases(view, query) {
|
|
208
|
+
return levels(query).flatMap((level) => level.select.flatMap((item) => {
|
|
209
|
+
if (item.kind !== "named")
|
|
210
|
+
return [];
|
|
211
|
+
const index = view.indexAt(item.name.from);
|
|
212
|
+
return view.word(index - 1) === AS || followsOperand(view, index) ? [item.name] : [];
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
/** Whether a level merges same-named columns, so an unqualified one is not ambiguous. */
|
|
216
|
+
function mergesColumns(view, query) {
|
|
217
|
+
return view.places.some((place, index) => place.query === query && (view.word(index) === USING || view.word(index) === NATURAL));
|
|
218
|
+
}
|
|
219
|
+
async function unqualifiedColumn(view, index, name) {
|
|
220
|
+
const place = view.places[index];
|
|
221
|
+
if (place === undefined)
|
|
222
|
+
return [];
|
|
223
|
+
const { query } = place;
|
|
224
|
+
const { rules } = view;
|
|
225
|
+
if (rules.sourceNamed(query, name) !== undefined || rules.cteNamed(query, name) !== undefined)
|
|
226
|
+
return [];
|
|
227
|
+
const aliases = selectAliases(view, query);
|
|
228
|
+
const aliased = aliases.some((alias) => rules.sameIdentifier(alias, name));
|
|
229
|
+
const readings = await readingsOf(view, query);
|
|
230
|
+
const has = (reading) => reading.columns?.some((column) => rules.denotes(name, column)) === true;
|
|
231
|
+
for (const [depth, level] of readings.entries()) {
|
|
232
|
+
const owners = level.filter(has);
|
|
233
|
+
if (owners.length === 0)
|
|
234
|
+
continue;
|
|
235
|
+
const levelQuery = levels(query)[depth];
|
|
236
|
+
if (owners.length < 2 || aliased || levelQuery === undefined || mergesColumns(view, levelQuery))
|
|
237
|
+
return [];
|
|
238
|
+
const fixes = owners.flatMap(({ source }) => {
|
|
239
|
+
const reference = referenceName(source);
|
|
240
|
+
if (reference === null)
|
|
241
|
+
return [];
|
|
242
|
+
const owner = view.quoted(reference.name);
|
|
243
|
+
return [{ title: `Qualify with '${owner}'`, edits: [{ from: name.from, to: name.from, insert: `${owner}.` }] }];
|
|
244
|
+
});
|
|
245
|
+
return [problem(AMBIGUOUS, "error", name, `Column reference '${name.name}' is ambiguous`, fixes)];
|
|
246
|
+
}
|
|
247
|
+
const all = readings.flat();
|
|
248
|
+
if (aliased || all.length === 0 || all.some((reading) => reading.columns === null))
|
|
249
|
+
return [];
|
|
250
|
+
const candidates = [...all.flatMap((reading) => reading.columns ?? []), ...aliases.map((alias) => alias.name)];
|
|
251
|
+
const fixes = suggestionFixes(view, name, candidates);
|
|
252
|
+
return [problem(RESOLVE, "error", name, `Unable to resolve column '${name.name}'`, fixes)];
|
|
253
|
+
}
|
|
254
|
+
async function qualifiedColumn(view, index) {
|
|
255
|
+
const place = view.places[index];
|
|
256
|
+
const owner = view.name(index - 2) ?? anyName(view, index - 2);
|
|
257
|
+
if (place === undefined || owner === null || view.isSymbol(index - 3, DOT) || isParameter(view, index - 2))
|
|
258
|
+
return [];
|
|
259
|
+
const { rules } = view;
|
|
260
|
+
const source = rules.sourceNamed(place.query, owner);
|
|
261
|
+
if (source === undefined) {
|
|
262
|
+
if (PSEUDO_RELATIONS.has(owner.name.toUpperCase()) && !owner.quoted)
|
|
263
|
+
return [];
|
|
264
|
+
if (!(await catalogAnswers(view)))
|
|
265
|
+
return [];
|
|
266
|
+
const references = levels(place.query)
|
|
267
|
+
.flatMap((level) => readSources(view, level))
|
|
268
|
+
.map((candidate) => referenceName(candidate)?.name)
|
|
269
|
+
.filter((name) => name !== undefined);
|
|
270
|
+
const fixes = suggestionFixes(view, owner, references);
|
|
271
|
+
return [problem(RESOLVE, "error", owner, `Unable to resolve table '${owner.name}'`, fixes)];
|
|
272
|
+
}
|
|
273
|
+
const name = anyName(view, index);
|
|
274
|
+
if (name === null)
|
|
275
|
+
return [];
|
|
276
|
+
const columns = await knownColumns(view, source, place.query);
|
|
277
|
+
if (columns === null || columns.some((column) => rules.denotes(name, column)))
|
|
278
|
+
return [];
|
|
279
|
+
const fixes = suggestionFixes(view, name, columns);
|
|
280
|
+
return [problem(RESOLVE, "error", name, `Unable to resolve column '${name.name}'`, fixes)];
|
|
281
|
+
}
|
|
282
|
+
/** `INSERT INTO t (a, b)`: each listed name against the target's columns. */
|
|
283
|
+
async function insertColumns(view, query) {
|
|
284
|
+
const insert = query.insert;
|
|
285
|
+
if (insert === null)
|
|
286
|
+
return [];
|
|
287
|
+
const found = await view.lookup.relation(insert.source.qualifier, insert.source.name);
|
|
288
|
+
if (found === null)
|
|
289
|
+
return [];
|
|
290
|
+
const columns = found.relation.columns.map((column) => column.name);
|
|
291
|
+
const open = view.indexAt(insert.from - 1);
|
|
292
|
+
const problems = [];
|
|
293
|
+
for (const index of view.statement.level(open)) {
|
|
294
|
+
const name = view.name(index);
|
|
295
|
+
if (name === null || columns.some((column) => view.rules.denotes(name, column)))
|
|
296
|
+
continue;
|
|
297
|
+
const fixes = suggestionFixes(view, name, columns);
|
|
298
|
+
problems.push(problem(RESOLVE, "error", name, `Unable to resolve column '${name.name}'`, fixes));
|
|
299
|
+
}
|
|
300
|
+
return problems;
|
|
301
|
+
}
|
|
302
|
+
function columnReference(view, index) {
|
|
303
|
+
const place = view.places[index];
|
|
304
|
+
if (place === undefined || place.clause === null || !COLUMN_CLAUSES.has(place.clause))
|
|
305
|
+
return null;
|
|
306
|
+
if (place.clause === USING && place.group === place.queryOpen)
|
|
307
|
+
return null;
|
|
308
|
+
if (view.isSymbol(index + 1, OPEN) || view.isSymbol(index + 1, DOT))
|
|
309
|
+
return null;
|
|
310
|
+
if (view.isSymbol(index - 1, DOT)) {
|
|
311
|
+
const token = view.token(index);
|
|
312
|
+
return token?.kind === "symbol" && view.raw(index) !== STAR ? null : "qualified";
|
|
313
|
+
}
|
|
314
|
+
const next = view.token(index + 1);
|
|
315
|
+
if (next?.kind === "string" || view.word(index - 1) === AS)
|
|
316
|
+
return null;
|
|
317
|
+
if (isParameter(view, index) || followsOperand(view, index))
|
|
318
|
+
return null;
|
|
319
|
+
return "plain";
|
|
320
|
+
}
|
|
321
|
+
/** Unknown tables, schemas, columns and qualifiers, and columns two sources share (SqlResolve, SqlAmbiguousColumn). */
|
|
322
|
+
export async function resolveProblems(view) {
|
|
323
|
+
if (!view.inspected)
|
|
324
|
+
return [];
|
|
325
|
+
const problems = [];
|
|
326
|
+
for (const query of view.queries.keys()) {
|
|
327
|
+
for (const source of query.sources) {
|
|
328
|
+
if (source.kind === "relation")
|
|
329
|
+
problems.push(...(await unresolvedRelation(view, query, source)));
|
|
330
|
+
}
|
|
331
|
+
problems.push(...(await insertColumns(view, query)));
|
|
332
|
+
}
|
|
333
|
+
for (let index = 0; index < view.tokens.length; index += 1) {
|
|
334
|
+
const kind = columnReference(view, index);
|
|
335
|
+
if (kind === "qualified") {
|
|
336
|
+
problems.push(...(await qualifiedColumn(view, index)));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const name = kind === null ? null : view.name(index);
|
|
340
|
+
if (name !== null)
|
|
341
|
+
problems.push(...(await unqualifiedColumn(view, index, name)));
|
|
342
|
+
}
|
|
343
|
+
return problems;
|
|
344
|
+
}
|
|
345
|
+
function quoteFix(view, index) {
|
|
346
|
+
const token = view.token(index);
|
|
347
|
+
if (token === undefined)
|
|
348
|
+
return null;
|
|
349
|
+
const raw = view.raw(index);
|
|
350
|
+
const folded = view.rules.fold(raw);
|
|
351
|
+
const fix = replacement(`Quote '${raw}'`, token, view.options.quoteIdentifier(folded));
|
|
352
|
+
return problem(RESERVED, "error", token, `'${raw}' is a reserved word; quote it`, [fix]);
|
|
353
|
+
}
|
|
354
|
+
function namesRelation(view, index) {
|
|
355
|
+
const place = view.places[index];
|
|
356
|
+
if (place === undefined || !view.statement.isQuery(place.group) || view.isSymbol(index + 1, OPEN))
|
|
357
|
+
return false;
|
|
358
|
+
if (view.isSymbol(index - 1, COMMA))
|
|
359
|
+
return view.places[index - 1]?.clause === FROM_CLAUSE && !CLAUSE_WORDS.has(view.word(index) ?? "");
|
|
360
|
+
const before = view.word(index - 1);
|
|
361
|
+
if (before === null || !RELATION_INTRODUCERS.has(before))
|
|
362
|
+
return false;
|
|
363
|
+
return !(before === "UPDATE" && NOT_TABLE_UPDATES.has(view.word(index - 2) ?? ""));
|
|
364
|
+
}
|
|
365
|
+
/** A reserved word written bare where a table or an inserted column is named (SqlReservedWordAsName). */
|
|
366
|
+
export function reservedProblems(view) {
|
|
367
|
+
if (!view.inspected)
|
|
368
|
+
return [];
|
|
369
|
+
const listed = new Set();
|
|
370
|
+
for (const query of view.queries.keys()) {
|
|
371
|
+
const insert = query.insert;
|
|
372
|
+
if (insert === null)
|
|
373
|
+
continue;
|
|
374
|
+
for (const index of view.statement.level(view.indexAt(insert.from - 1)))
|
|
375
|
+
listed.add(index);
|
|
376
|
+
}
|
|
377
|
+
const problems = [];
|
|
378
|
+
view.tokens.forEach((token, index) => {
|
|
379
|
+
if (token.kind !== "word")
|
|
380
|
+
return;
|
|
381
|
+
const word = view.raw(index).toUpperCase();
|
|
382
|
+
if (!view.options.reservedWords.has(word) || NOT_RELATION_NAMES.has(word))
|
|
383
|
+
return;
|
|
384
|
+
if (!listed.has(index) && !namesRelation(view, index))
|
|
385
|
+
return;
|
|
386
|
+
const found = quoteFix(view, index);
|
|
387
|
+
if (found !== null)
|
|
388
|
+
problems.push(found);
|
|
389
|
+
});
|
|
390
|
+
return problems;
|
|
391
|
+
}
|