@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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/catalog.d.ts +112 -0
  4. package/dist/catalog.js +5 -0
  5. package/dist/completion.d.ts +109 -0
  6. package/dist/completion.js +12 -0
  7. package/dist/errors.d.ts +58 -0
  8. package/dist/errors.js +106 -0
  9. package/dist/explain.d.ts +58 -0
  10. package/dist/explain.js +1 -0
  11. package/dist/index.d.ts +15 -0
  12. package/dist/index.js +15 -0
  13. package/dist/inspection.d.ts +38 -0
  14. package/dist/inspection.js +21 -0
  15. package/dist/manifest.d.ts +186 -0
  16. package/dist/manifest.js +176 -0
  17. package/dist/objects.d.ts +140 -0
  18. package/dist/objects.js +2 -0
  19. package/dist/plugin.d.ts +460 -0
  20. package/dist/plugin.js +49 -0
  21. package/dist/sessions.d.ts +59 -0
  22. package/dist/sessions.js +2 -0
  23. package/dist/sql/classify.d.ts +19 -0
  24. package/dist/sql/classify.js +95 -0
  25. package/dist/sql/complete.d.ts +23 -0
  26. package/dist/sql/complete.js +480 -0
  27. package/dist/sql/explain.d.ts +42 -0
  28. package/dist/sql/explain.js +108 -0
  29. package/dist/sql/format.d.ts +26 -0
  30. package/dist/sql/format.js +117 -0
  31. package/dist/sql/functions.d.ts +47 -0
  32. package/dist/sql/functions.js +159 -0
  33. package/dist/sql/generate.d.ts +22 -0
  34. package/dist/sql/generate.js +64 -0
  35. package/dist/sql/index.d.ts +16 -0
  36. package/dist/sql/index.js +16 -0
  37. package/dist/sql/inspect.d.ts +10 -0
  38. package/dist/sql/inspect.js +134 -0
  39. package/dist/sql/inspectContext.d.ts +78 -0
  40. package/dist/sql/inspectContext.js +251 -0
  41. package/dist/sql/inspectResolve.d.ts +6 -0
  42. package/dist/sql/inspectResolve.js +391 -0
  43. package/dist/sql/inspectStyle.d.ts +3 -0
  44. package/dist/sql/inspectStyle.js +384 -0
  45. package/dist/sql/inspectSyntax.d.ts +10 -0
  46. package/dist/sql/inspectSyntax.js +179 -0
  47. package/dist/sql/layout.d.ts +8 -0
  48. package/dist/sql/layout.js +114 -0
  49. package/dist/sql/layoutNodes.d.ts +46 -0
  50. package/dist/sql/layoutNodes.js +317 -0
  51. package/dist/sql/layoutPrint.d.ts +60 -0
  52. package/dist/sql/layoutPrint.js +806 -0
  53. package/dist/sql/layoutWriter.d.ts +21 -0
  54. package/dist/sql/layoutWriter.js +77 -0
  55. package/dist/sql/literals.d.ts +28 -0
  56. package/dist/sql/literals.js +78 -0
  57. package/dist/sql/namespace.d.ts +41 -0
  58. package/dist/sql/namespace.js +220 -0
  59. package/dist/sql/objects.d.ts +42 -0
  60. package/dist/sql/objects.js +222 -0
  61. package/dist/sql/parameters.d.ts +20 -0
  62. package/dist/sql/parameters.js +88 -0
  63. package/dist/sql/resolution.d.ts +73 -0
  64. package/dist/sql/resolution.js +192 -0
  65. package/dist/sql/routines.d.ts +27 -0
  66. package/dist/sql/routines.js +42 -0
  67. package/dist/sql/scope.d.ts +117 -0
  68. package/dist/sql/scope.js +540 -0
  69. package/dist/sql/select.d.ts +41 -0
  70. package/dist/sql/select.js +287 -0
  71. package/dist/sql/signature.d.ts +18 -0
  72. package/dist/sql/signature.js +137 -0
  73. package/dist/sql/splitter.d.ts +15 -0
  74. package/dist/sql/splitter.js +217 -0
  75. package/dist/sql/suggest.d.ts +6 -0
  76. package/dist/sql/suggest.js +36 -0
  77. package/dist/sql/tokens.d.ts +62 -0
  78. package/dist/sql/tokens.js +227 -0
  79. package/dist/sql/words.d.ts +23 -0
  80. package/dist/sql/words.js +72 -0
  81. package/dist/testing/index.d.ts +177 -0
  82. package/dist/testing/index.js +2070 -0
  83. package/dist/tls.d.ts +33 -0
  84. package/dist/tls.js +118 -0
  85. package/dist/url.d.ts +52 -0
  86. package/dist/url.js +219 -0
  87. package/dist/values.d.ts +44 -0
  88. package/dist/values.js +44 -0
  89. package/dist/version.d.ts +22 -0
  90. package/dist/version.js +29 -0
  91. package/package.json +42 -4
@@ -0,0 +1,95 @@
1
+ import { namespaceTarget } from "./namespace.js";
2
+ import { significantTokens } from "./tokens.js";
3
+ import { explainPrefix, mainVerb, words } from "./words.js";
4
+ const KINDS = new Map([
5
+ ...["SELECT", "SHOW", "EXPLAIN", "DESCRIBE", "DESC", "VALUES", "TABLE"].map((word) => [word, "read"]),
6
+ ...["INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE", "UPSERT", "COPY", "LOAD"].map((word) => [word, "write"]),
7
+ ...["CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME", "COMMENT"].map((word) => [word, "ddl"]),
8
+ ...["BEGIN", "COMMIT", "ROLLBACK", "SAVEPOINT", "RELEASE", "END", "ABORT"].map((word) => [word, "transaction"]),
9
+ ]);
10
+ const START = "START";
11
+ const TRANSACTION = "TRANSACTION";
12
+ const WHERE = "WHERE";
13
+ const LIMIT = "LIMIT";
14
+ const WITH = "WITH";
15
+ /** Prefixes the explained statement's destructive reason, so the confirmation says why it matters. */
16
+ export const EXPLAIN_ANALYZE_REASON_PREFIX = "EXPLAIN ANALYZE runs it: ";
17
+ function hasTopLevel(list, from, word) {
18
+ return list.some((entry, at) => at > from && entry.depth === 0 && entry.word === word);
19
+ }
20
+ function destructiveReason(list, verbAt) {
21
+ const verb = list[verbAt]?.word;
22
+ switch (verb) {
23
+ case "DELETE":
24
+ if (hasTopLevel(list, verbAt, WHERE))
25
+ return null;
26
+ return hasTopLevel(list, verbAt, LIMIT)
27
+ ? "DELETE without WHERE (LIMIT does not choose the rows)"
28
+ : "DELETE without WHERE";
29
+ case "UPDATE":
30
+ return hasTopLevel(list, verbAt, WHERE) ? null : "UPDATE without WHERE";
31
+ case "DROP": {
32
+ const object = list[verbAt + 1]?.word;
33
+ return object === undefined ? "DROP" : `DROP ${object}`;
34
+ }
35
+ case "TRUNCATE":
36
+ return "TRUNCATE";
37
+ default:
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * A token-level classifier plugins may build `SqlLanguage.classify` on (decision 023). The first
43
+ * keyword picks the kind; `UPDATE` or `DELETE` without a top-level `WHERE`, `DROP` and `TRUNCATE`
44
+ * are destructive (decision 011). `EXPLAIN` is a read unless it analyses, which runs the statement,
45
+ * so `EXPLAIN ANALYZE` takes the statement's kind and destructive reason (decision 031). It never
46
+ * parses, so it never rejects a statement.
47
+ */
48
+ export function classifyTokens(sql, options = {}) {
49
+ const statementClass = classifyWords(words(sql, significantTokens(sql, options)));
50
+ return namespaceTarget(sql, options) === null ? statementClass : { ...statementClass, setsNamespace: true };
51
+ }
52
+ function classifyWords(list) {
53
+ const first = list[0]?.word;
54
+ if (first === undefined)
55
+ return { kind: "other", destructive: null };
56
+ if (first === START) {
57
+ return { kind: list[1]?.word === TRANSACTION ? "transaction" : "other", destructive: null };
58
+ }
59
+ const explain = explainPrefix(list);
60
+ if (explain !== null) {
61
+ if (!explain.analyze)
62
+ return { kind: "read", destructive: null };
63
+ const inner = classifyWords(list.slice(explain.statementAt));
64
+ return {
65
+ kind: inner.kind,
66
+ destructive: inner.destructive === null ? null : `${EXPLAIN_ANALYZE_REASON_PREFIX}${inner.destructive}`,
67
+ };
68
+ }
69
+ const verbAt = mainVerb(list);
70
+ const verb = list[verbAt]?.word ?? first;
71
+ const kind = first === WITH && verbAt === 0 ? "read" : (KINDS.get(verb) ?? "other");
72
+ return { kind, destructive: destructiveReason(list, verbAt) };
73
+ }
74
+ const ENDS = new Set(["COMMIT", "END", "ABORT"]);
75
+ const ROLLBACK = "ROLLBACK";
76
+ const TO = "TO";
77
+ const PREPARE = "PREPARE";
78
+ /**
79
+ * Whether a statement opens or ends a transaction, for plugins that track `Transactions.open` in
80
+ * auto mode: `BEGIN` and `START TRANSACTION` open one; `COMMIT`, `ROLLBACK` (not `ROLLBACK TO`),
81
+ * `END`, `ABORT` and `PREPARE TRANSACTION` end it. Null for anything else.
82
+ */
83
+ export function transactionEffect(sql, options = {}) {
84
+ const list = words(sql, significantTokens(sql, options)).map((entry) => entry.word);
85
+ const [first, second] = list;
86
+ if (first === "BEGIN" || (first === START && second === TRANSACTION))
87
+ return "begin";
88
+ if (first !== undefined && ENDS.has(first))
89
+ return "end";
90
+ if (first === PREPARE && second === TRANSACTION)
91
+ return "end";
92
+ if (first === ROLLBACK)
93
+ return list.includes(TO) ? null : "end";
94
+ return null;
95
+ }
@@ -0,0 +1,23 @@
1
+ import type { SqlLanguage } from "@tablecraft-app/plugin-api";
2
+ import { type FunctionSpec, type ServerIdentity } from "./functions.js";
3
+ import { type IdentifierFolding } from "./resolution.js";
4
+ import type { SqlDialectOptions } from "./tokens.js";
5
+ export type { IdentifierFolding } from "./resolution.js";
6
+ /** What the shared completer needs to know about a dialect (decision 028). */
7
+ export interface CompleterOptions {
8
+ readonly dialect: SqlDialectOptions;
9
+ readonly identifierFolding: IdentifierFolding;
10
+ /** A name the dialect reads back unchanged without quotes; any other is quoted when inserted. */
11
+ readonly plainIdentifier: RegExp;
12
+ readonly quoteIdentifier: (name: string) => string;
13
+ /** Offered where a keyword fits; a name that spells one is quoted. Upper case. */
14
+ readonly keywords: readonly string[];
15
+ /** Built-in functions; a spec brings its signatures, summary and server range (decision 097). */
16
+ readonly functions: readonly (string | FunctionSpec)[];
17
+ /** Reads the catalog's server version into the flavour and version the specs' ranges name; null when it cannot. */
18
+ readonly server?: (version: string) => ServerIdentity | null;
19
+ }
20
+ /** Reserved words every built-in dialect shares; a plugin adds its own. */
21
+ export declare const SQL_KEYWORDS: readonly string[];
22
+ /** The shared completion and navigation every built-in plugin spreads into its `SqlLanguage` (decision 028). */
23
+ export declare function sqlCompleter(options: CompleterOptions): Pick<SqlLanguage, "complete" | "resolve"> & Required<Pick<SqlLanguage, "selectSource" | "signatureAt">>;
@@ -0,0 +1,480 @@
1
+ import { functionItem, functionName, isAvailable } from "./functions.js";
2
+ import { callAt } from "./signature.js";
3
+ import { parseSimpleSelect, SELECT_READ_ONLY_REASONS } from "./select.js";
4
+ import { scopeAt } from "./scope.js";
5
+ import { namespaceSlotAt } from "./namespace.js";
6
+ import { levels, nameRules, referenceName, samePath, targetable, visibleCtes, } from "./resolution.js";
7
+ /** Reserved words every built-in dialect shares; a plugin adds its own. */
8
+ export const SQL_KEYWORDS = [
9
+ "ALL",
10
+ "AND",
11
+ "AS",
12
+ "ASC",
13
+ "BETWEEN",
14
+ "BY",
15
+ "CASE",
16
+ "CREATE",
17
+ "CROSS",
18
+ "DELETE",
19
+ "DESC",
20
+ "DISTINCT",
21
+ "DROP",
22
+ "ELSE",
23
+ "END",
24
+ "EXISTS",
25
+ "FALSE",
26
+ "FROM",
27
+ "FULL",
28
+ "GROUP",
29
+ "HAVING",
30
+ "IN",
31
+ "INNER",
32
+ "INSERT",
33
+ "INTO",
34
+ "IS",
35
+ "JOIN",
36
+ "LEFT",
37
+ "LIKE",
38
+ "LIMIT",
39
+ "NOT",
40
+ "NULL",
41
+ "OFFSET",
42
+ "ON",
43
+ "OR",
44
+ "ORDER",
45
+ "OUTER",
46
+ "RIGHT",
47
+ "SELECT",
48
+ "SET",
49
+ "TABLE",
50
+ "THEN",
51
+ "TRUE",
52
+ "UNION",
53
+ "UPDATE",
54
+ "USING",
55
+ "VALUES",
56
+ "WHEN",
57
+ "WHERE",
58
+ "WITH",
59
+ ];
60
+ /** Ranks items the typed prefix matches equally well (plan 009 decision 12). */
61
+ const BOOST = {
62
+ join: 50,
63
+ column: 40,
64
+ cte: 35,
65
+ table: 30,
66
+ view: 30,
67
+ alias: 20,
68
+ namespace: 10,
69
+ function: 0,
70
+ keyword: -10,
71
+ };
72
+ const UNQUOTED_WORD = "^[A-Za-z0-9_$]*$";
73
+ const ALIAS_WORD_BREAK = /[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])/;
74
+ const FALLBACK_ALIAS = "t";
75
+ const QUOTES = new Set(['"', "`", "["]);
76
+ /** Below every other namespace: `USE mysql` is legitimate but rarely meant. */
77
+ const SYSTEM_NAMESPACE_BOOST = -20;
78
+ const CURRENT_DETAIL = "current";
79
+ function relationKind(relation) {
80
+ return relation.kind === "table" ? "table" : "view";
81
+ }
82
+ /** The shared completion and navigation every built-in plugin spreads into its `SqlLanguage` (decision 028). */
83
+ export function sqlCompleter(options) {
84
+ const reserved = new Set(options.keywords.map((keyword) => keyword.toUpperCase()));
85
+ const { denotes, sameIdentifier, cteNamed, sourceNamed, serverOf, Lookup } = nameRules(options);
86
+ const specs = new Map();
87
+ for (const spec of options.functions) {
88
+ if (typeof spec === "string")
89
+ continue;
90
+ const key = spec.name.toUpperCase();
91
+ specs.set(key, [...(specs.get(key) ?? []), spec]);
92
+ }
93
+ function specNamed(name, server) {
94
+ return specs.get(name.toUpperCase())?.find((spec) => isAvailable(spec, server));
95
+ }
96
+ function quoted(name) {
97
+ return options.plainIdentifier.test(name) && !reserved.has(name.toUpperCase())
98
+ ? name
99
+ : options.quoteIdentifier(name);
100
+ }
101
+ function item(kind, label, apply, detail) {
102
+ return { label, kind, detail, apply, boost: BOOST[kind], info: null, caret: null };
103
+ }
104
+ function relationItems(relations, namespace) {
105
+ return relations.map((relation) => item(relationKind(relation), relation.name, quoted(relation.name), namespace));
106
+ }
107
+ function columnItems(columns) {
108
+ const seen = new Set();
109
+ return columns
110
+ .filter((column) => !seen.has(column.name) && seen.add(column.name))
111
+ .map((column) => item("column", column.name, quoted(column.name), column.type));
112
+ }
113
+ function keywordItems(except = new Set()) {
114
+ return options.keywords
115
+ .filter((keyword) => !except.has(keyword.toUpperCase()))
116
+ .map((keyword) => item("keyword", keyword, keyword, null));
117
+ }
118
+ function availableFunctions(server) {
119
+ const offered = new Set();
120
+ return options.functions.filter((spec) => {
121
+ const name = functionName(spec).toUpperCase();
122
+ if (offered.has(name) || (typeof spec !== "string" && !isAvailable(spec, server)))
123
+ return false;
124
+ offered.add(name);
125
+ return true;
126
+ });
127
+ }
128
+ async function qualifiedItems(scope, lookup) {
129
+ const { qualifier, query, clause } = scope;
130
+ const last = qualifier.at(-1);
131
+ if (last === undefined)
132
+ return [];
133
+ const relational = clause === "relation" || clause === "join";
134
+ if (qualifier.length === 1 && !relational) {
135
+ const source = sourceNamed(query, last);
136
+ if (source !== undefined)
137
+ return columnItems(await lookup.columnsOf(source, query));
138
+ const cte = cteNamed(query, last);
139
+ if (cte !== undefined)
140
+ return columnItems(await lookup.cteColumns(cte, 1));
141
+ }
142
+ const namespace = await lookup.namespace(qualifier);
143
+ if (namespace !== undefined)
144
+ return relationItems(await lookup.relations(namespace.path), null);
145
+ if (relational)
146
+ return [];
147
+ const found = await lookup.relation(qualifier.slice(0, -1), last);
148
+ return found === null ? [] : columnItems(found.relation.columns);
149
+ }
150
+ async function relationContextItems(scope, lookup) {
151
+ const current = lookup.current;
152
+ const namespaces = await lookup.namespaces();
153
+ const currentName = namespaces.find((namespace) => samePath(namespace.path, current))?.name ?? null;
154
+ const relations = current === null ? [] : await lookup.relations(current);
155
+ const items = [
156
+ ...visibleCtes(scope.query).map((cte) => item("cte", cte.name.name, quoted(cte.name.name), null)),
157
+ ...relationItems(relations, currentName),
158
+ ...namespaces
159
+ .filter((namespace) => !namespace.system && !samePath(namespace.path, current))
160
+ .map((namespace) => item("namespace", namespace.name, quoted(namespace.name), namespace.kind)),
161
+ ];
162
+ if (scope.clause !== "join")
163
+ return items;
164
+ return [...(await joinItems(scope, lookup)), ...items];
165
+ }
166
+ function aliasFor(name, taken) {
167
+ const initials = name
168
+ .split(ALIAS_WORD_BREAK)
169
+ .filter((word) => word !== "")
170
+ .map((word) => word.charAt(0).toLowerCase())
171
+ .join("");
172
+ const base = initials === "" || !options.plainIdentifier.test(initials) ? FALLBACK_ALIAS : initials;
173
+ let alias = base;
174
+ for (let suffix = 1; taken.has(alias) || reserved.has(alias.toUpperCase()); suffix += 1)
175
+ alias = `${base}${String(suffix)}`;
176
+ taken.add(alias);
177
+ return alias;
178
+ }
179
+ function relationReference(path, name, current) {
180
+ const namespace = path.at(-1);
181
+ return samePath(path, current) || namespace === undefined ? quoted(name) : `${quoted(namespace)}.${quoted(name)}`;
182
+ }
183
+ /** `customers c ON c.id = o.customer_id` for each foreign key to or from a relation already in scope (plan 009 decision 7). */
184
+ async function joinItems(scope, lookup) {
185
+ const before = scope.query.sources.filter((source) => source.kind === "relation" && source.to <= scope.prefixFrom);
186
+ const taken = new Set(scope.query.sources
187
+ .map((source) => referenceName(source))
188
+ .filter((name) => name !== null)
189
+ .map((name) => name.name.toLowerCase()));
190
+ const items = [];
191
+ const seen = new Set();
192
+ const add = (label, detail) => {
193
+ if (seen.has(label))
194
+ return;
195
+ seen.add(label);
196
+ items.push(item("join", label, label, detail));
197
+ };
198
+ for (const source of before) {
199
+ if (source.qualifier.length === 0 && cteNamed(scope.query, source.name) !== undefined)
200
+ continue;
201
+ const found = await lookup.relation(source.qualifier, source.name);
202
+ if (found === null)
203
+ continue;
204
+ const near = source.alias === null
205
+ ? relationReference(found.path, found.relation.name, lookup.current)
206
+ : quoted(source.alias.name);
207
+ for (const key of found.relation.foreignKeys) {
208
+ const alias = aliasFor(key.target.relation, new Set(taken));
209
+ const on = key.columns
210
+ .map((column, at) => `${alias}.${quoted(key.target.columns[at] ?? column)} = ${near}.${quoted(column)}`)
211
+ .join(" AND ");
212
+ add(`${relationReference(key.target.path, key.target.relation, lookup.current)} ${alias} ON ${on}`, key.name);
213
+ }
214
+ for (const incoming of await lookup.referencing(found.path, found.relation.name)) {
215
+ const key = incoming.foreignKey;
216
+ const alias = aliasFor(incoming.relation, new Set(taken));
217
+ const on = key.columns
218
+ .map((column, at) => `${alias}.${quoted(column)} = ${near}.${quoted(key.target.columns[at] ?? column)}`)
219
+ .join(" AND ");
220
+ add(`${relationReference(incoming.path, incoming.relation, lookup.current)} ${alias} ON ${on}`, key.name);
221
+ }
222
+ }
223
+ return items;
224
+ }
225
+ async function inScopeColumnItems(scope, lookup) {
226
+ const { query } = scope;
227
+ const perSource = await Promise.all(query.sources.map((source) => lookup.columnsOf(source, query)));
228
+ const owners = new Map();
229
+ for (const columns of perSource) {
230
+ for (const name of new Set(columns.map((column) => column.name)))
231
+ owners.set(name, (owners.get(name) ?? 0) + 1);
232
+ }
233
+ const items = [];
234
+ const seen = new Set();
235
+ query.sources.forEach((source, at) => {
236
+ const reference = referenceName(source);
237
+ for (const column of perSource[at] ?? []) {
238
+ const shared = (owners.get(column.name) ?? 0) > 1 && reference !== null;
239
+ const prefix = shared ? `${quoted(reference.name)}.` : "";
240
+ const label = shared ? `${reference.name}.${column.name}` : column.name;
241
+ if (seen.has(label))
242
+ continue;
243
+ seen.add(label);
244
+ items.push(item("column", label, `${prefix}${quoted(column.name)}`, column.type));
245
+ }
246
+ });
247
+ const aliases = query.sources
248
+ .filter((source) => source.alias !== null)
249
+ .map((source) => item("alias", source.alias?.name ?? "", quoted(source.alias?.name ?? ""), source.kind === "relation" ? source.name.name : null));
250
+ const functions = availableFunctions(lookup.server);
251
+ const functionNames = new Set(functions.map((spec) => functionName(spec).toUpperCase()));
252
+ return [
253
+ ...items,
254
+ ...aliases,
255
+ ...functions.map((spec) => functionItem(spec, BOOST.function)),
256
+ ...keywordItems(functionNames),
257
+ ];
258
+ }
259
+ async function itemsFor(scope, lookup) {
260
+ if (scope.clause === "none")
261
+ return [];
262
+ if (scope.qualifier.length > 0)
263
+ return qualifiedItems(scope, lookup);
264
+ switch (scope.clause) {
265
+ case "relation":
266
+ case "join":
267
+ return relationContextItems(scope, lookup);
268
+ case "columns":
269
+ return inScopeColumnItems(scope, lookup);
270
+ case "insertColumns": {
271
+ const target = scope.query.insert?.source;
272
+ const found = target === undefined ? null : await lookup.relation(target.qualifier, target.name);
273
+ return found === null ? [] : columnItems(found.relation.columns);
274
+ }
275
+ case "keywords":
276
+ return keywordItems();
277
+ }
278
+ }
279
+ /** Inside a quoted name every name is offered quoted, since the editor filters on what was typed. */
280
+ function quotedLabels(items) {
281
+ return items.map((entry) => entry.kind === "keyword" || entry.kind === "function" || entry.kind === "join"
282
+ ? entry
283
+ : { ...entry, label: options.quoteIdentifier(entry.label) });
284
+ }
285
+ /** A clause body with `scope.relation` in scope reads as the WHERE of a `SELECT` from it. */
286
+ function wrap(request) {
287
+ if (request.scope === undefined)
288
+ return { text: request.text, offset: request.offset, shift: 0 };
289
+ const { path, relation } = request.scope;
290
+ const namespace = path.at(-1);
291
+ const table = namespace === undefined
292
+ ? options.quoteIdentifier(relation)
293
+ : `${options.quoteIdentifier(namespace)}.${options.quoteIdentifier(relation)}`;
294
+ const head = `SELECT * FROM ${table} WHERE `;
295
+ return { text: head + request.text, offset: request.offset + head.length, shift: head.length };
296
+ }
297
+ /** Decision 2 and 3 of plan 029: what a `USE` or search path element can name, as the caret's quote needs it. */
298
+ async function namespaceItems(slot, lookup) {
299
+ const target = { kind: slot.kind, candidates: [] };
300
+ const namespaces = (await lookup.namespaces()).filter((namespace) => targetable(target, namespace) &&
301
+ !slot.listed.some((listed) => denotes(listed, namespace.path.at(-1) ?? namespace.name)));
302
+ const ordered = [
303
+ ...namespaces.filter((namespace) => !namespace.system),
304
+ ...namespaces.filter((namespace) => namespace.system),
305
+ ];
306
+ const quote = slot.quote;
307
+ return ordered.map((namespace) => {
308
+ const name = namespace.path.at(-1) ?? namespace.name;
309
+ const apply = quote === null
310
+ ? quoted(name)
311
+ : name.replaceAll(quote.char, quote.char + quote.char) + (quote.closed ? "" : quote.char);
312
+ const detail = samePath(namespace.path, lookup.current) ? CURRENT_DETAIL : null;
313
+ const entry = item("namespace", name, apply, detail);
314
+ return namespace.system ? { ...entry, boost: SYSTEM_NAMESPACE_BOOST } : entry;
315
+ });
316
+ }
317
+ async function complete(request, catalog) {
318
+ if (request.scope === undefined) {
319
+ const slot = namespaceSlotAt(request.text, request.offset, options.dialect);
320
+ if (slot !== null) {
321
+ const lookup = new Lookup(catalog);
322
+ await lookup.follow(request.text, request.offset);
323
+ const items = await namespaceItems(slot, lookup);
324
+ return { from: slot.from, to: slot.to, items, validFor: slot.quote === null ? UNQUOTED_WORD : null };
325
+ }
326
+ }
327
+ const { text, offset, shift } = wrap(request);
328
+ const scope = scopeAt(text, offset, options.dialect);
329
+ const lookup = new Lookup(catalog);
330
+ if (request.scope === undefined)
331
+ await lookup.follow(text, offset);
332
+ const items = await itemsFor(scope, lookup);
333
+ const typedQuote = QUOTES.has(text.charAt(scope.prefixFrom)) && scope.prefixFrom < offset;
334
+ return {
335
+ from: scope.prefixFrom - shift,
336
+ to: offset - shift,
337
+ items: typedQuote ? quotedLabels(items) : items,
338
+ validFor: typedQuote ? null : UNQUOTED_WORD,
339
+ };
340
+ }
341
+ function sourceDeclaring(query, word) {
342
+ for (const level of levels(query)) {
343
+ for (const source of level.sources) {
344
+ if (source.kind === "relation" && (source.name.from === word.from || source.alias?.from === word.from))
345
+ return source;
346
+ }
347
+ }
348
+ return undefined;
349
+ }
350
+ async function relationDeclaration(lookup, qualifier, name) {
351
+ const found = await lookup.relation(qualifier, name);
352
+ return found === null ? null : { kind: "relation", path: found.path, relation: found.relation.name };
353
+ }
354
+ async function columnDeclaration(lookup, source, query, column) {
355
+ if (source.kind !== "relation")
356
+ return null;
357
+ if (source.qualifier.length === 0 && cteNamed(query, source.name) !== undefined)
358
+ return null;
359
+ const found = await lookup.relation(source.qualifier, source.name);
360
+ const meta = found?.relation.columns.find((candidate) => denotes(column, candidate.name));
361
+ return found === null || meta === undefined
362
+ ? null
363
+ : { kind: "column", path: found.path, relation: found.relation.name, column: meta.name };
364
+ }
365
+ async function resolve(request, catalog) {
366
+ const scope = scopeAt(request.text, request.offset, options.dialect, "resolve");
367
+ const { word, qualifier, query } = scope;
368
+ if (word === null || scope.clause === "none")
369
+ return null;
370
+ const lookup = new Lookup(catalog);
371
+ await lookup.follow(request.text, request.offset);
372
+ const cte = visibleCtes(query).find((candidate) => candidate.name.from === word.from);
373
+ if (cte !== undefined)
374
+ return { kind: "cte", from: cte.name.from, to: cte.name.to };
375
+ const declaring = sourceDeclaring(query, word);
376
+ if (declaring !== undefined) {
377
+ if (declaring.alias?.from === word.from)
378
+ return { kind: "alias", from: declaring.from, to: declaring.to };
379
+ if (declaring.qualifier.length === 0) {
380
+ const named = cteNamed(query, word);
381
+ if (named !== undefined)
382
+ return { kind: "cte", from: named.name.from, to: named.name.to };
383
+ }
384
+ return relationDeclaration(lookup, declaring.qualifier, declaring.name);
385
+ }
386
+ if (qualifier.length === 0) {
387
+ const source = sourceNamed(query, word);
388
+ if (source !== undefined && (scope.dotAfter || source.alias !== null)) {
389
+ if (source.alias !== null)
390
+ return { kind: "alias", from: source.from, to: source.to };
391
+ if (source.kind === "relation")
392
+ return relationDeclaration(lookup, source.qualifier, source.name);
393
+ }
394
+ const named = cteNamed(query, word);
395
+ if (named !== undefined)
396
+ return { kind: "cte", from: named.name.from, to: named.name.to };
397
+ if (!scope.dotAfter && scope.clause === "columns") {
398
+ for (const candidate of query.sources) {
399
+ const declaration = await columnDeclaration(lookup, candidate, query, word);
400
+ if (declaration !== null)
401
+ return declaration;
402
+ }
403
+ }
404
+ return relationDeclaration(lookup, [], word);
405
+ }
406
+ if (qualifier.length === 1 && qualifier[0] !== undefined) {
407
+ const source = sourceNamed(query, qualifier[0]);
408
+ if (source !== undefined)
409
+ return columnDeclaration(lookup, source, query, word);
410
+ }
411
+ const asRelation = await relationDeclaration(lookup, qualifier, word);
412
+ if (asRelation !== null)
413
+ return asRelation;
414
+ const owner = qualifier.at(-1);
415
+ if (owner === undefined)
416
+ return null;
417
+ const found = await lookup.relation(qualifier.slice(0, -1), owner);
418
+ const meta = found?.relation.columns.find((candidate) => denotes(word, candidate.name));
419
+ return found === null || meta === undefined
420
+ ? null
421
+ : { kind: "column", path: found.path, relation: found.relation.name, column: meta.name };
422
+ }
423
+ /** Whether a qualifier written before a column or `*` names the one table the SELECT reads. */
424
+ function namesTable(qualifier, select) {
425
+ const last = qualifier.at(-1);
426
+ if (last === undefined)
427
+ return true;
428
+ if (select.alias !== null)
429
+ return qualifier.length === 1 && sameIdentifier(last, select.alias);
430
+ const written = [...select.qualifier, select.name];
431
+ const tail = written.slice(-qualifier.length);
432
+ return (tail.length === qualifier.length &&
433
+ qualifier.every((part, at) => {
434
+ const other = tail[at];
435
+ return other !== undefined && sameIdentifier(part, other);
436
+ }));
437
+ }
438
+ async function selectSource(sql, catalog) {
439
+ const select = parseSimpleSelect(sql, options.dialect);
440
+ if ("readOnly" in select)
441
+ return { kind: "readOnly", reason: select.readOnly };
442
+ const found = await new Lookup(catalog).relation(select.qualifier, select.name);
443
+ if (found === null)
444
+ return { kind: "readOnly", reason: `the catalog does not know ${select.name.name}` };
445
+ const tableColumns = found.relation.columns.map((column) => column.name);
446
+ const columns = [];
447
+ for (const item of select.items) {
448
+ if (item.kind === "expression") {
449
+ columns.push(null);
450
+ continue;
451
+ }
452
+ const qualifier = item.kind === "star" ? (item.qualifier === null ? [] : [item.qualifier]) : item.qualifier;
453
+ if (!namesTable(qualifier, select))
454
+ return { kind: "readOnly", reason: SELECT_READ_ONLY_REASONS.notSelect };
455
+ if (item.kind === "star") {
456
+ columns.push(...tableColumns);
457
+ continue;
458
+ }
459
+ columns.push(tableColumns.find((name) => denotes(item.name, name)) ?? null);
460
+ }
461
+ return { kind: "table", path: found.path, relation: found.relation.name, columns };
462
+ }
463
+ function signatureAt(request, catalog) {
464
+ const { text, offset, shift } = wrap(request);
465
+ const server = serverOf(catalog);
466
+ const call = callAt(text, offset, options.dialect, reserved, (name) => specNamed(name, server) !== undefined);
467
+ const spec = call === null ? undefined : specNamed(call.name, server);
468
+ if (call === null || spec === undefined)
469
+ return Promise.resolve(null);
470
+ return Promise.resolve({
471
+ name: spec.name.toLowerCase(),
472
+ summary: spec.summary,
473
+ signatures: spec.signatures,
474
+ argument: call.argument,
475
+ from: call.from - shift,
476
+ to: call.to - shift,
477
+ });
478
+ }
479
+ return { complete, resolve, selectSource, signatureAt };
480
+ }
@@ -0,0 +1,42 @@
1
+ import type { Row, SqlLanguage } from "@tablecraft-app/plugin-api";
2
+ import { type SqlDialectOptions } from "./tokens.js";
3
+ /** What `sqlExplainable` needs to know about a dialect's EXPLAIN (decision 031). */
4
+ export interface ExplainableOptions {
5
+ readonly dialect: SqlDialectOptions;
6
+ /** The database's name, as a refusal names it: `PostgreSQL`. */
7
+ readonly name: string;
8
+ /**
9
+ * The verbs EXPLAIN accepts, as the verb that decides what a statement does (`SELECT`, or the one
10
+ * after `WITH …`); `CREATE_TABLE_AS` stands for `CREATE TABLE … AS`.
11
+ */
12
+ readonly verbs: readonly string[];
13
+ /** The verbs Explain Analyse accepts, a subset of `verbs`; empty where the dialect cannot analyse. */
14
+ readonly analyzeVerbs: readonly string[];
15
+ }
16
+ export declare const NOTHING_TO_EXPLAIN = "There is no statement to explain; put the caret in one.";
17
+ export declare const EXPLAIN_ONE_STATEMENT = "Explain one statement at a time.";
18
+ export declare const ALREADY_EXPLAIN = "This statement is already an EXPLAIN; run it with Run to see its rows.";
19
+ /** How `ExplainableOptions` names `CREATE TABLE … AS`, which `CREATE TABLE` alone is not. */
20
+ export declare const CREATE_TABLE_AS = "CREATE TABLE \u2026 AS";
21
+ /**
22
+ * The refusals every dialect shares: nothing to explain, more than one statement, and a statement
23
+ * that already is an EXPLAIN. Null when none applies.
24
+ */
25
+ export declare function commonExplainRefusal(sql: string, dialect: SqlDialectOptions): string | null;
26
+ /**
27
+ * The verb that decides what `sql` does, upper case: `SELECT`, the verb after `WITH …`, or
28
+ * `CREATE_TABLE_AS`; empty for no statement. A plugin picks its EXPLAIN format by it.
29
+ */
30
+ export declare function explainedVerb(sql: string, dialect: SqlDialectOptions): string;
31
+ /**
32
+ * `SqlLanguage.explainable` from a dialect's verb lists: the shared refusals, then the statement's
33
+ * main verb against what the dialect explains and analyses. A statement writes when it is not a read.
34
+ */
35
+ export declare function sqlExplainable(options: ExplainableOptions): Required<Pick<SqlLanguage, "explainable">>;
36
+ /** The first cell of every row as text, one row per line: what an EXPLAIN printed. */
37
+ export declare function planText(rows: readonly Row[]): string;
38
+ /**
39
+ * A plan property's value as one line of text: strings as they are, lists joined with commas,
40
+ * anything nested as JSON. Null for nothing worth showing: null, or an empty list.
41
+ */
42
+ export declare function planPropertyValue(value: unknown): string | null;