@tablecraft-app/plugin-api 0.0.0-stage → 1.2.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 (93) 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 +67 -0
  8. package/dist/errors.js +124 -0
  9. package/dist/explain.d.ts +58 -0
  10. package/dist/explain.js +1 -0
  11. package/dist/index.d.ts +16 -0
  12. package/dist/index.js +16 -0
  13. package/dist/inspection.d.ts +38 -0
  14. package/dist/inspection.js +21 -0
  15. package/dist/keyspace.d.ts +180 -0
  16. package/dist/keyspace.js +2 -0
  17. package/dist/manifest.d.ts +200 -0
  18. package/dist/manifest.js +238 -0
  19. package/dist/objects.d.ts +140 -0
  20. package/dist/objects.js +2 -0
  21. package/dist/plugin.d.ts +565 -0
  22. package/dist/plugin.js +58 -0
  23. package/dist/sessions.d.ts +59 -0
  24. package/dist/sessions.js +2 -0
  25. package/dist/sql/classify.d.ts +19 -0
  26. package/dist/sql/classify.js +95 -0
  27. package/dist/sql/complete.d.ts +23 -0
  28. package/dist/sql/complete.js +480 -0
  29. package/dist/sql/explain.d.ts +42 -0
  30. package/dist/sql/explain.js +108 -0
  31. package/dist/sql/format.d.ts +26 -0
  32. package/dist/sql/format.js +117 -0
  33. package/dist/sql/functions.d.ts +47 -0
  34. package/dist/sql/functions.js +159 -0
  35. package/dist/sql/generate.d.ts +22 -0
  36. package/dist/sql/generate.js +64 -0
  37. package/dist/sql/index.d.ts +16 -0
  38. package/dist/sql/index.js +16 -0
  39. package/dist/sql/inspect.d.ts +10 -0
  40. package/dist/sql/inspect.js +134 -0
  41. package/dist/sql/inspectContext.d.ts +78 -0
  42. package/dist/sql/inspectContext.js +251 -0
  43. package/dist/sql/inspectResolve.d.ts +6 -0
  44. package/dist/sql/inspectResolve.js +391 -0
  45. package/dist/sql/inspectStyle.d.ts +3 -0
  46. package/dist/sql/inspectStyle.js +384 -0
  47. package/dist/sql/inspectSyntax.d.ts +10 -0
  48. package/dist/sql/inspectSyntax.js +179 -0
  49. package/dist/sql/layout.d.ts +8 -0
  50. package/dist/sql/layout.js +114 -0
  51. package/dist/sql/layoutNodes.d.ts +46 -0
  52. package/dist/sql/layoutNodes.js +317 -0
  53. package/dist/sql/layoutPrint.d.ts +60 -0
  54. package/dist/sql/layoutPrint.js +806 -0
  55. package/dist/sql/layoutWriter.d.ts +21 -0
  56. package/dist/sql/layoutWriter.js +77 -0
  57. package/dist/sql/literals.d.ts +28 -0
  58. package/dist/sql/literals.js +78 -0
  59. package/dist/sql/namespace.d.ts +41 -0
  60. package/dist/sql/namespace.js +220 -0
  61. package/dist/sql/objects.d.ts +42 -0
  62. package/dist/sql/objects.js +222 -0
  63. package/dist/sql/parameters.d.ts +20 -0
  64. package/dist/sql/parameters.js +88 -0
  65. package/dist/sql/resolution.d.ts +73 -0
  66. package/dist/sql/resolution.js +192 -0
  67. package/dist/sql/routines.d.ts +27 -0
  68. package/dist/sql/routines.js +42 -0
  69. package/dist/sql/scope.d.ts +117 -0
  70. package/dist/sql/scope.js +540 -0
  71. package/dist/sql/select.d.ts +41 -0
  72. package/dist/sql/select.js +287 -0
  73. package/dist/sql/signature.d.ts +18 -0
  74. package/dist/sql/signature.js +137 -0
  75. package/dist/sql/splitter.d.ts +15 -0
  76. package/dist/sql/splitter.js +217 -0
  77. package/dist/sql/suggest.d.ts +6 -0
  78. package/dist/sql/suggest.js +36 -0
  79. package/dist/sql/tokens.d.ts +62 -0
  80. package/dist/sql/tokens.js +227 -0
  81. package/dist/sql/words.d.ts +23 -0
  82. package/dist/sql/words.js +72 -0
  83. package/dist/testing/index.d.ts +274 -0
  84. package/dist/testing/index.js +2398 -0
  85. package/dist/tls.d.ts +33 -0
  86. package/dist/tls.js +118 -0
  87. package/dist/url.d.ts +54 -0
  88. package/dist/url.js +228 -0
  89. package/dist/values.d.ts +44 -0
  90. package/dist/values.js +44 -0
  91. package/dist/version.d.ts +22 -0
  92. package/dist/version.js +29 -0
  93. package/package.json +42 -4
@@ -0,0 +1,78 @@
1
+ import type { InspectionId, Problem, ProblemSeverity, QuickFix } from "@tablecraft-app/plugin-api";
2
+ import type { CompleterOptions } from "./complete.js";
3
+ import type { Lookup, NameRules } from "./resolution.js";
4
+ import { Statement, type Identifier, type Query, type RelationSource } from "./scope.js";
5
+ import type { Token } from "./tokens.js";
6
+ /** What `sqlInspector` needs: the completer's dialect knowledge and the words a name must be quoted to spell. */
7
+ export interface InspectorOptions extends CompleterOptions {
8
+ /** Upper case. A name spelling one of these is an error unless quoted. */
9
+ readonly reservedWords: ReadonlySet<string>;
10
+ }
11
+ /**
12
+ * What a relation source really is: a table read by the query, the target of an `INTO`, a table
13
+ * function, or no relation at all (`FOR UPDATE NOWAIT` and `ON DUPLICATE KEY UPDATE col`, which the
14
+ * scope reader takes for one).
15
+ */
16
+ export type SourceRole = "relation" | "target" | "function" | "notRelation";
17
+ /** Where a token stands: its innermost parentheses, the query level that owns it, and the clause it is in. */
18
+ export interface Place {
19
+ readonly group: number;
20
+ readonly queryOpen: number;
21
+ readonly query: Query;
22
+ /** The clause keyword before it at its own level, `ORDER BY` for a `BY`; null before any. */
23
+ readonly clause: string | null;
24
+ }
25
+ /** One statement's tokens read once, shared by every inspection that runs on it. */
26
+ export declare class StatementView {
27
+ #private;
28
+ readonly text: string;
29
+ readonly tokens: readonly Token[];
30
+ readonly options: InspectorOptions;
31
+ readonly rules: NameRules;
32
+ readonly lookup: Lookup;
33
+ readonly statement: Statement;
34
+ readonly places: Place[];
35
+ /** The `(` of each query level, `NO_GROUP` for the statement's own. */
36
+ readonly queries: Map<Query, number>;
37
+ /** The statement's main verb, upper case, after any `WITH` or `EXPLAIN`. */
38
+ readonly verb: string | null;
39
+ /** Whether the statement is a query or DML the clause inspections understand. */
40
+ readonly inspected: boolean;
41
+ constructor(text: string, tokens: readonly Token[], options: InspectorOptions, rules: NameRules, lookup: Lookup);
42
+ token(index: number): Token | undefined;
43
+ word(index: number): string | null;
44
+ isSymbol(index: number, symbol: string): boolean;
45
+ raw(index: number): string;
46
+ /** The first token starting at or after `offset`; the token count when none does. */
47
+ nextIndex(offset: number): number;
48
+ /** The token starting at `offset`, or -1. */
49
+ indexAt(offset: number): number;
50
+ /** A name token: quoted, or an unquoted word that is no keyword, reserved word or function of the dialect. */
51
+ name(index: number): Identifier | null;
52
+ roleOf(source: RelationSource): SourceRole;
53
+ /** `name` as the dialect reads it back: bare when it can be, quoted otherwise. */
54
+ quoted(name: string): string;
55
+ isKeyword(word: string): boolean;
56
+ /** Where the statement's last token ends, so an insertion lands before its delimiter. */
57
+ get end(): number;
58
+ /** The text from the end of the token before `index` to the end of `last`: a span removed with its leading space. */
59
+ withLeadingSpace(index: number, last?: number): {
60
+ from: number;
61
+ to: number;
62
+ };
63
+ }
64
+ export declare function problem(inspection: InspectionId, severity: ProblemSeverity, span: {
65
+ readonly from: number;
66
+ readonly to: number;
67
+ }, message: string, fixes?: readonly QuickFix[]): Problem;
68
+ /** A fix that removes `span`. */
69
+ export declare function removal(title: string, span: {
70
+ readonly from: number;
71
+ readonly to: number;
72
+ }): QuickFix;
73
+ export declare function replacement(title: string, span: {
74
+ readonly from: number;
75
+ readonly to: number;
76
+ }, insert: string): QuickFix;
77
+ /** An inspection over one statement; it may read the catalog through the view's lookup. */
78
+ export type StatementInspection = (view: StatementView) => Problem[] | Promise<Problem[]>;
@@ -0,0 +1,251 @@
1
+ import { functionName } from "./functions.js";
2
+ import { CLAUSE_WORDS, NO_GROUP, Statement, unquote, } from "./scope.js";
3
+ import { explainPrefix, mainVerb, words } from "./words.js";
4
+ const OPEN = "(";
5
+ const CLOSE = ")";
6
+ const BY = "BY";
7
+ const BY_OWNERS = new Set(["ORDER", "GROUP", "PARTITION"]);
8
+ const ON = "ON";
9
+ /** `ON DUPLICATE KEY UPDATE` and `ON CONFLICT`: an `ON` that starts an action, not a join condition. */
10
+ const ON_ACTIONS = new Set(["DUPLICATE", "CONFLICT"]);
11
+ const WITH = "WITH";
12
+ const CREATE = "CREATE";
13
+ const AS = "AS";
14
+ /** Statements whose clauses the inspections understand; anything else gets only the lexical checks. */
15
+ const INSPECTED_VERBS = new Set(["SELECT", "INSERT", "UPDATE", "DELETE", "REPLACE"]);
16
+ const CREATED_FROM_QUERY = new Set(["TABLE", "VIEW"]);
17
+ const QUERY_STARTS = new Set(["SELECT", WITH]);
18
+ /** Words an expression may hold that no built-in keyword list names, so they are never taken for columns. */
19
+ const EXPRESSION_WORDS = [
20
+ "INTERVAL",
21
+ "CURRENT_DATE",
22
+ "CURRENT_TIME",
23
+ "CURRENT_TIMESTAMP",
24
+ "CURRENT_USER",
25
+ "CURRENT_SCHEMA",
26
+ "CURRENT_CATALOG",
27
+ "CURRENT_ROLE",
28
+ "SESSION_USER",
29
+ "SYSTEM_USER",
30
+ "USER",
31
+ "LOCALTIME",
32
+ "LOCALTIMESTAMP",
33
+ "UNKNOWN",
34
+ "ANY",
35
+ "SOME",
36
+ "ARRAY",
37
+ "ROW",
38
+ "DATE",
39
+ "TIME",
40
+ "TIMESTAMP",
41
+ "BINARY",
42
+ "COLLATE",
43
+ "ESCAPE",
44
+ "SIMILAR",
45
+ "OVERLAPS",
46
+ "ISNULL",
47
+ "NOTNULL",
48
+ "DIV",
49
+ "MOD",
50
+ "XOR",
51
+ "REGEXP",
52
+ "RLIKE",
53
+ "GLOB",
54
+ "MATCH",
55
+ "AGAINST",
56
+ "NULLS",
57
+ "FIRST",
58
+ "LAST",
59
+ "ROWS",
60
+ "RANGE",
61
+ "GROUPS",
62
+ "PRECEDING",
63
+ "FOLLOWING",
64
+ "UNBOUNDED",
65
+ "CURRENT",
66
+ "WITHIN",
67
+ "FILTER",
68
+ "OVER",
69
+ "PARTITION",
70
+ "SEPARATOR",
71
+ "DEFAULT",
72
+ "UNSIGNED",
73
+ "SIGNED",
74
+ ];
75
+ const INTO = "INTO";
76
+ const UPDATE = "UPDATE";
77
+ /** `ON DUPLICATE KEY UPDATE` and `FOR UPDATE`: an `UPDATE` after these introduces no table. */
78
+ const NOT_TABLE_UPDATES = new Set(["KEY", "FOR"]);
79
+ /** One statement's tokens read once, shared by every inspection that runs on it. */
80
+ export class StatementView {
81
+ text;
82
+ tokens;
83
+ options;
84
+ rules;
85
+ lookup;
86
+ statement;
87
+ places = [];
88
+ /** The `(` of each query level, `NO_GROUP` for the statement's own. */
89
+ queries = new Map();
90
+ /** The statement's main verb, upper case, after any `WITH` or `EXPLAIN`. */
91
+ verb;
92
+ /** Whether the statement is a query or DML the clause inspections understand. */
93
+ inspected;
94
+ #keywords;
95
+ #quoteWorthy;
96
+ constructor(text, tokens, options, rules, lookup) {
97
+ this.text = text;
98
+ this.tokens = tokens;
99
+ this.options = options;
100
+ this.rules = rules;
101
+ this.lookup = lookup;
102
+ this.statement = new Statement(text, tokens);
103
+ this.#quoteWorthy = new Set([
104
+ ...options.keywords.map((keyword) => keyword.toUpperCase()),
105
+ ...options.reservedWords,
106
+ ]);
107
+ this.#keywords = new Set([
108
+ ...this.#quoteWorthy,
109
+ ...EXPRESSION_WORDS,
110
+ ...options.functions.map((spec) => functionName(spec).toUpperCase()),
111
+ ]);
112
+ this.#readPlaces();
113
+ const list = words(text, tokens);
114
+ const explained = explainPrefix(list);
115
+ const rest = explained === null ? list : list.slice(explained.statementAt);
116
+ const verb = rest[mainVerb(rest)]?.word ?? null;
117
+ this.verb = verb;
118
+ const startsWithQuery = this.isSymbol(0, OPEN) && QUERY_STARTS.has(this.word(1) ?? "");
119
+ this.inspected =
120
+ (verb !== null && INSPECTED_VERBS.has(verb)) ||
121
+ startsWithQuery ||
122
+ (verb === CREATE &&
123
+ rest.some((entry) => CREATED_FROM_QUERY.has(entry.word)) &&
124
+ rest.some((entry, at) => entry.depth === 0 && entry.word === AS && QUERY_STARTS.has(rest[at + 1]?.word ?? "")));
125
+ }
126
+ #readPlaces() {
127
+ const statement = this.statement;
128
+ const stack = [];
129
+ const clauses = new Map([[NO_GROUP, null]]);
130
+ for (let index = 0; index < this.tokens.length; index += 1) {
131
+ const group = stack.at(-1) ?? NO_GROUP;
132
+ const word = this.word(index);
133
+ if (word !== null && CLAUSE_WORDS.has(word)) {
134
+ const owner = this.word(index - 1);
135
+ const following = this.word(index + 1);
136
+ const key = word === BY && owner !== null && BY_OWNERS.has(owner)
137
+ ? `${owner} ${BY}`
138
+ : word === ON && following !== null && ON_ACTIONS.has(following)
139
+ ? `${ON} ${following}`
140
+ : word;
141
+ clauses.set(group, key);
142
+ }
143
+ const queryOpen = statement.enclosingQuery(group);
144
+ const query = statement.query(queryOpen);
145
+ this.queries.set(query, queryOpen);
146
+ this.places.push({ group, queryOpen, query, clause: clauses.get(group) ?? null });
147
+ if (this.isSymbol(index, OPEN)) {
148
+ clauses.set(index, statement.isQuery(index) ? null : (clauses.get(group) ?? null));
149
+ stack.push(index);
150
+ }
151
+ else if (this.isSymbol(index, CLOSE)) {
152
+ stack.pop();
153
+ }
154
+ }
155
+ for (const query of [...this.queries.keys()])
156
+ this.#collect(query);
157
+ }
158
+ /** CTEs and derived tables that hold no token of their own still need a level. */
159
+ #collect(query) {
160
+ for (const nested of [...query.ctes.map((cte) => cte.query), ...query.sources.flatMap(derivedQuery)]) {
161
+ if (this.queries.has(nested))
162
+ continue;
163
+ this.queries.set(nested, NO_GROUP);
164
+ this.#collect(nested);
165
+ }
166
+ }
167
+ token(index) {
168
+ return this.tokens[index];
169
+ }
170
+ word(index) {
171
+ return this.statement.word(index);
172
+ }
173
+ isSymbol(index, symbol) {
174
+ return this.statement.isSymbol(index, symbol);
175
+ }
176
+ raw(index) {
177
+ const token = this.tokens[index];
178
+ return token === undefined ? "" : this.text.slice(token.from, token.to);
179
+ }
180
+ /** The first token starting at or after `offset`; the token count when none does. */
181
+ nextIndex(offset) {
182
+ let low = 0;
183
+ let high = this.tokens.length;
184
+ while (low < high) {
185
+ const middle = (low + high) >> 1;
186
+ if ((this.tokens[middle]?.from ?? 0) < offset)
187
+ low = middle + 1;
188
+ else
189
+ high = middle;
190
+ }
191
+ return low;
192
+ }
193
+ /** The token starting at `offset`, or -1. */
194
+ indexAt(offset) {
195
+ const index = this.nextIndex(offset);
196
+ return this.tokens[index]?.from === offset ? index : -1;
197
+ }
198
+ /** A name token: quoted, or an unquoted word that is no keyword, reserved word or function of the dialect. */
199
+ name(index) {
200
+ const token = this.tokens[index];
201
+ if (token === undefined)
202
+ return null;
203
+ if (token.kind === "quoted")
204
+ return { name: unquote(this.raw(index)), quoted: true, from: token.from, to: token.to };
205
+ const identifier = this.statement.identifier(index);
206
+ if (identifier === null || this.#keywords.has(identifier.name.toUpperCase()))
207
+ return null;
208
+ return identifier;
209
+ }
210
+ roleOf(source) {
211
+ const first = this.indexAt(source.from);
212
+ const before = this.word(first - 1);
213
+ if (before === INTO)
214
+ return "target";
215
+ if (before === UPDATE && NOT_TABLE_UPDATES.has(this.word(first - 2) ?? ""))
216
+ return "notRelation";
217
+ return this.isSymbol(this.indexAt(source.name.from) + 1, OPEN) ? "function" : "relation";
218
+ }
219
+ /** `name` as the dialect reads it back: bare when it can be, quoted otherwise. */
220
+ quoted(name) {
221
+ return this.options.plainIdentifier.test(name) && !this.#quoteWorthy.has(name.toUpperCase())
222
+ ? name
223
+ : this.options.quoteIdentifier(name);
224
+ }
225
+ isKeyword(word) {
226
+ return this.#keywords.has(word.toUpperCase());
227
+ }
228
+ /** Where the statement's last token ends, so an insertion lands before its delimiter. */
229
+ get end() {
230
+ return this.tokens.at(-1)?.to ?? 0;
231
+ }
232
+ /** The text from the end of the token before `index` to the end of `last`: a span removed with its leading space. */
233
+ withLeadingSpace(index, last = index) {
234
+ const before = this.tokens[index - 1];
235
+ const own = this.tokens[index];
236
+ return { from: before?.to ?? own?.from ?? 0, to: this.tokens[last]?.to ?? own?.to ?? 0 };
237
+ }
238
+ }
239
+ function derivedQuery(source) {
240
+ return source.kind === "derived" ? [source.query] : [];
241
+ }
242
+ export function problem(inspection, severity, span, message, fixes = []) {
243
+ return { from: span.from, to: span.to, severity, inspection, message, fixes };
244
+ }
245
+ /** A fix that removes `span`. */
246
+ export function removal(title, span) {
247
+ return { title, edits: [{ from: span.from, to: span.to, insert: "" }] };
248
+ }
249
+ export function replacement(title, span, insert) {
250
+ return { title, edits: [{ from: span.from, to: span.to, insert }] };
251
+ }
@@ -0,0 +1,6 @@
1
+ import type { Problem } from "@tablecraft-app/plugin-api";
2
+ import { type StatementView } from "./inspectContext.js";
3
+ /** Unknown tables, schemas, columns and qualifiers, and columns two sources share (SqlResolve, SqlAmbiguousColumn). */
4
+ export declare function resolveProblems(view: StatementView): Promise<Problem[]>;
5
+ /** A reserved word written bare where a table or an inserted column is named (SqlReservedWordAsName). */
6
+ export declare function reservedProblems(view: StatementView): Problem[];