@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.
- 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 +67 -0
- package/dist/errors.js +124 -0
- package/dist/explain.d.ts +58 -0
- package/dist/explain.js +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/inspection.d.ts +38 -0
- package/dist/inspection.js +21 -0
- package/dist/keyspace.d.ts +180 -0
- package/dist/keyspace.js +2 -0
- package/dist/manifest.d.ts +200 -0
- package/dist/manifest.js +238 -0
- package/dist/objects.d.ts +140 -0
- package/dist/objects.js +2 -0
- package/dist/plugin.d.ts +565 -0
- package/dist/plugin.js +58 -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 +274 -0
- package/dist/testing/index.js +2398 -0
- package/dist/tls.d.ts +33 -0
- package/dist/tls.js +118 -0
- package/dist/url.d.ts +54 -0
- package/dist/url.js +228 -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,36 @@
|
|
|
1
|
+
/** How many "Did you mean" fixes an unresolved name offers. */
|
|
2
|
+
export const MAX_SUGGESTIONS = 3;
|
|
3
|
+
/** A candidate this close is always offered; longer names allow a third of their length. */
|
|
4
|
+
const MIN_SUGGESTION_DISTANCE = 2;
|
|
5
|
+
const SUGGESTION_LENGTH_RATIO = 3;
|
|
6
|
+
/** The edit distance between two names, compared case-blind. */
|
|
7
|
+
export function editDistance(left, right) {
|
|
8
|
+
const a = left.toLowerCase();
|
|
9
|
+
const b = right.toLowerCase();
|
|
10
|
+
let previous = Array.from({ length: b.length + 1 }, (_, at) => at);
|
|
11
|
+
for (let row = 1; row <= a.length; row += 1) {
|
|
12
|
+
const current = [row];
|
|
13
|
+
for (let column = 1; column <= b.length; column += 1) {
|
|
14
|
+
const substitution = (previous[column - 1] ?? 0) + (a.charAt(row - 1) === b.charAt(column - 1) ? 0 : 1);
|
|
15
|
+
current.push(Math.min((previous[column] ?? 0) + 1, (current[column - 1] ?? 0) + 1, substitution));
|
|
16
|
+
}
|
|
17
|
+
previous = current;
|
|
18
|
+
}
|
|
19
|
+
return previous[b.length] ?? 0;
|
|
20
|
+
}
|
|
21
|
+
/** The closest `candidates` to `name`, nearest first, within max(2, a third of its length). */
|
|
22
|
+
export function suggestions(name, candidates) {
|
|
23
|
+
const threshold = Math.max(MIN_SUGGESTION_DISTANCE, Math.floor(name.length / SUGGESTION_LENGTH_RATIO));
|
|
24
|
+
const scored = new Map();
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
if (scored.has(candidate) || candidate.toLowerCase() === name.toLowerCase())
|
|
27
|
+
continue;
|
|
28
|
+
const distance = editDistance(name, candidate);
|
|
29
|
+
if (distance <= threshold)
|
|
30
|
+
scored.set(candidate, distance);
|
|
31
|
+
}
|
|
32
|
+
return [...scored]
|
|
33
|
+
.sort(([leftName, left], [rightName, right]) => left - right || leftName.localeCompare(rightName))
|
|
34
|
+
.slice(0, MAX_SUGGESTIONS)
|
|
35
|
+
.map(([candidate]) => candidate);
|
|
36
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** What a dialect needs the shared splitter and classifier to know (decision 023). */
|
|
2
|
+
export interface SqlDialectOptions {
|
|
3
|
+
/** PostgreSQL `$tag$ … $tag$` strings. */
|
|
4
|
+
readonly dollarQuotes?: boolean;
|
|
5
|
+
/** MySQL `'\''`: a backslash escapes the next character in strings and double-quoted text. */
|
|
6
|
+
readonly backslashEscapes?: boolean;
|
|
7
|
+
/** The MySQL client's `DELIMITER //` line, which changes what ends a statement. */
|
|
8
|
+
readonly delimiterCommand?: boolean;
|
|
9
|
+
/** PostgreSQL block comments nest: a comment inside a comment needs its own close. */
|
|
10
|
+
readonly nestedBlockComments?: boolean;
|
|
11
|
+
/** MySQL `# comment`; PostgreSQL uses `#` as an operator. */
|
|
12
|
+
readonly hashComments?: boolean;
|
|
13
|
+
/** Keywords that open a body holding semicolons, closed by `END`, inside `CREATE TRIGGER`, `FUNCTION` and the like. */
|
|
14
|
+
readonly blockKeywords?: readonly string[];
|
|
15
|
+
/** The statements that move the session's current namespace in this dialect (plan 022). */
|
|
16
|
+
readonly namespaceStatements?: readonly NamespaceStatementForm[];
|
|
17
|
+
/** SQL Server `[name]`, where `]]` is a `]` inside the name (decision 100). */
|
|
18
|
+
readonly bracketIdentifiers?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* A line that ends a batch and belongs to no statement: SQL Server's `GO` (optionally `GO 3`) or
|
|
21
|
+
* Oracle's `/`. A routine definition, and under `slash` an anonymous block, keeps its own `;` and
|
|
22
|
+
* runs to that line (decision 100).
|
|
23
|
+
*/
|
|
24
|
+
readonly batchSeparator?: BatchSeparator;
|
|
25
|
+
/** Oracle `q'[…]'` strings, whose delimiter is chosen per string. */
|
|
26
|
+
readonly qQuotes?: boolean;
|
|
27
|
+
/** `N'…'` national character strings. */
|
|
28
|
+
readonly nationalStrings?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export type BatchSeparator = "go" | "slash";
|
|
31
|
+
/**
|
|
32
|
+
* `use` is MySQL's `USE db`; `searchPath` is PostgreSQL's `SET [SESSION | LOCAL] search_path`,
|
|
33
|
+
* `SET SCHEMA` and `RESET search_path` / `RESET ALL`.
|
|
34
|
+
*/
|
|
35
|
+
export type NamespaceStatementForm = "use" | "searchPath";
|
|
36
|
+
export type TokenKind = "word" | "quoted" | "string" | "comment" | "symbol" | "delimiter";
|
|
37
|
+
export interface Token {
|
|
38
|
+
readonly kind: TokenKind;
|
|
39
|
+
readonly from: number;
|
|
40
|
+
readonly to: number;
|
|
41
|
+
}
|
|
42
|
+
export declare const DEFAULT_DELIMITER = ";";
|
|
43
|
+
/** A hand-written scanner: it knows quoting and comments, never grammar, so it cannot reject a statement. */
|
|
44
|
+
export declare class Lexer {
|
|
45
|
+
#private;
|
|
46
|
+
readonly text: string;
|
|
47
|
+
readonly options: SqlDialectOptions;
|
|
48
|
+
position: number;
|
|
49
|
+
constructor(text: string, options: SqlDialectOptions);
|
|
50
|
+
/** The next token after whitespace, or null at the end. `delimiter` ends a statement wherever it starts outside quotes. */
|
|
51
|
+
next(delimiter?: string): Token | null;
|
|
52
|
+
}
|
|
53
|
+
/** A batch separator line: where it ends, and `GO <n>`'s count. */
|
|
54
|
+
export interface SeparatorLine {
|
|
55
|
+
readonly to: number;
|
|
56
|
+
readonly repeat: number | undefined;
|
|
57
|
+
}
|
|
58
|
+
/** The separator line `token` starts, or null when `token` is not alone on its line as one. */
|
|
59
|
+
export declare function separatorLineAt(text: string, token: Token, separator: BatchSeparator): SeparatorLine | null;
|
|
60
|
+
/** Every token of `text` but comments, as one dialect reads it. */
|
|
61
|
+
export declare function significantTokens(text: string, options: SqlDialectOptions): Token[];
|
|
62
|
+
export declare function wordOf(text: string, token: Token): string;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
export const DEFAULT_DELIMITER = ";";
|
|
2
|
+
const NEWLINE = "\n";
|
|
3
|
+
const FIRST_NON_ASCII = 0x80;
|
|
4
|
+
const DOLLAR_TAG = /\$(?:[A-Za-z_\u0080-\uffff][A-Za-z0-9_\u0080-\uffff]*)?\$/y;
|
|
5
|
+
/** PostgreSQL's `E'…'` string, where backslashes escape whatever the dialect's default. */
|
|
6
|
+
const ESCAPE_STRING_PREFIX = /^[eE]$/;
|
|
7
|
+
const NATIONAL_STRING_PREFIX = /^[nN]$/;
|
|
8
|
+
/** Oracle's `q'…'` and `Nq'…'`. */
|
|
9
|
+
const Q_QUOTE_PREFIX = /^[nN]?[qQ]$/;
|
|
10
|
+
const Q_QUOTE_PAIRS = new Map([
|
|
11
|
+
["[", "]"],
|
|
12
|
+
["{", "}"],
|
|
13
|
+
["(", ")"],
|
|
14
|
+
["<", ">"],
|
|
15
|
+
]);
|
|
16
|
+
const STRING_QUOTE = "'";
|
|
17
|
+
const BRACKET_OPEN = "[";
|
|
18
|
+
const BRACKET_CLOSE = "]";
|
|
19
|
+
function isSpace(char) {
|
|
20
|
+
return char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f" || char === "\v";
|
|
21
|
+
}
|
|
22
|
+
function isWordChar(char) {
|
|
23
|
+
const code = char.charCodeAt(0);
|
|
24
|
+
if (code >= FIRST_NON_ASCII)
|
|
25
|
+
return true;
|
|
26
|
+
return ((char >= "a" && char <= "z") ||
|
|
27
|
+
(char >= "A" && char <= "Z") ||
|
|
28
|
+
(char >= "0" && char <= "9") ||
|
|
29
|
+
char === "_" ||
|
|
30
|
+
char === "$");
|
|
31
|
+
}
|
|
32
|
+
/** A hand-written scanner: it knows quoting and comments, never grammar, so it cannot reject a statement. */
|
|
33
|
+
export class Lexer {
|
|
34
|
+
text;
|
|
35
|
+
options;
|
|
36
|
+
position = 0;
|
|
37
|
+
constructor(text, options) {
|
|
38
|
+
this.text = text;
|
|
39
|
+
this.options = options;
|
|
40
|
+
}
|
|
41
|
+
/** The next token after whitespace, or null at the end. `delimiter` ends a statement wherever it starts outside quotes. */
|
|
42
|
+
next(delimiter = DEFAULT_DELIMITER) {
|
|
43
|
+
const text = this.text;
|
|
44
|
+
let at = this.position;
|
|
45
|
+
while (at < text.length && isSpace(text.charAt(at)))
|
|
46
|
+
at += 1;
|
|
47
|
+
if (at >= text.length) {
|
|
48
|
+
this.position = at;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const char = text.charAt(at);
|
|
52
|
+
const following = text.charAt(at + 1);
|
|
53
|
+
let token;
|
|
54
|
+
if (char === "-" && following === "-")
|
|
55
|
+
token = this.#lineComment(at);
|
|
56
|
+
else if (char === "#" && this.options.hashComments === true)
|
|
57
|
+
token = this.#lineComment(at);
|
|
58
|
+
else if (char === "/" && following === "*")
|
|
59
|
+
token = this.#blockComment(at);
|
|
60
|
+
else if (text.startsWith(delimiter, at))
|
|
61
|
+
token = { kind: "delimiter", from: at, to: at + delimiter.length };
|
|
62
|
+
else if (char === "'")
|
|
63
|
+
token = this.#quoted(at, "'", "string", this.options.backslashEscapes === true);
|
|
64
|
+
else if (char === '"')
|
|
65
|
+
token = this.#quoted(at, '"', "quoted", this.options.backslashEscapes === true);
|
|
66
|
+
else if (char === "`")
|
|
67
|
+
token = this.#quoted(at, "`", "quoted", false);
|
|
68
|
+
else if (char === BRACKET_OPEN && this.options.bracketIdentifiers === true)
|
|
69
|
+
token = this.#bracketed(at);
|
|
70
|
+
else if (char === "$" && this.options.dollarQuotes === true && this.#dollarTag(at) !== null) {
|
|
71
|
+
token = this.#dollarQuoted(at, this.#dollarTag(at) ?? "");
|
|
72
|
+
}
|
|
73
|
+
else if (isWordChar(char))
|
|
74
|
+
token = this.#word(at, delimiter);
|
|
75
|
+
else
|
|
76
|
+
token = { kind: "symbol", from: at, to: at + 1 };
|
|
77
|
+
this.position = token.to;
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
#lineComment(from) {
|
|
81
|
+
const end = this.text.indexOf(NEWLINE, from);
|
|
82
|
+
return { kind: "comment", from, to: end < 0 ? this.text.length : end };
|
|
83
|
+
}
|
|
84
|
+
#blockComment(from) {
|
|
85
|
+
const text = this.text;
|
|
86
|
+
const nested = this.options.nestedBlockComments === true;
|
|
87
|
+
let depth = 1;
|
|
88
|
+
let at = from + 2;
|
|
89
|
+
while (at < text.length && depth > 0) {
|
|
90
|
+
if (text.startsWith("*/", at)) {
|
|
91
|
+
depth -= 1;
|
|
92
|
+
at += 2;
|
|
93
|
+
}
|
|
94
|
+
else if (nested && text.startsWith("/*", at)) {
|
|
95
|
+
depth += 1;
|
|
96
|
+
at += 2;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
at += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { kind: "comment", from, to: Math.min(at, text.length) };
|
|
103
|
+
}
|
|
104
|
+
/** A doubled quote is the quote itself; an unterminated one runs to the end of the text. */
|
|
105
|
+
#quoted(from, quote, kind, backslashEscapes) {
|
|
106
|
+
const text = this.text;
|
|
107
|
+
let at = from + 1;
|
|
108
|
+
while (at < text.length) {
|
|
109
|
+
const char = text.charAt(at);
|
|
110
|
+
if (backslashEscapes && char === "\\") {
|
|
111
|
+
at += 2;
|
|
112
|
+
}
|
|
113
|
+
else if (char === quote) {
|
|
114
|
+
if (text.charAt(at + 1) !== quote)
|
|
115
|
+
return { kind, from, to: at + 1 };
|
|
116
|
+
at += 2;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
at += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { kind, from, to: text.length };
|
|
123
|
+
}
|
|
124
|
+
/** `]]` is a `]` inside the name; `[` needs no escape. */
|
|
125
|
+
#bracketed(from) {
|
|
126
|
+
const text = this.text;
|
|
127
|
+
let at = from + 1;
|
|
128
|
+
while (at < text.length) {
|
|
129
|
+
if (text.charAt(at) === BRACKET_CLOSE) {
|
|
130
|
+
if (text.charAt(at + 1) !== BRACKET_CLOSE)
|
|
131
|
+
return { kind: "quoted", from, to: at + 1 };
|
|
132
|
+
at += 2;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
at += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { kind: "quoted", from, to: text.length };
|
|
139
|
+
}
|
|
140
|
+
/** `q'<open>…<close>'`, where `[`, `{`, `(` and `<` close with their pair and any other character with itself. */
|
|
141
|
+
#qQuoted(from, quote) {
|
|
142
|
+
const open = this.text.charAt(quote + 1);
|
|
143
|
+
if (open === "" || isSpace(open))
|
|
144
|
+
return null;
|
|
145
|
+
const terminator = (Q_QUOTE_PAIRS.get(open) ?? open) + STRING_QUOTE;
|
|
146
|
+
const end = this.text.indexOf(terminator, quote + 2);
|
|
147
|
+
return { kind: "string", from, to: end < 0 ? this.text.length : end + terminator.length };
|
|
148
|
+
}
|
|
149
|
+
#dollarTag(at) {
|
|
150
|
+
if (at > 0 && isWordChar(this.text.charAt(at - 1)))
|
|
151
|
+
return null;
|
|
152
|
+
DOLLAR_TAG.lastIndex = at;
|
|
153
|
+
return DOLLAR_TAG.exec(this.text)?.[0] ?? null;
|
|
154
|
+
}
|
|
155
|
+
#dollarQuoted(from, tag) {
|
|
156
|
+
const end = this.text.indexOf(tag, from + tag.length);
|
|
157
|
+
return { kind: "string", from, to: end < 0 ? this.text.length : end + tag.length };
|
|
158
|
+
}
|
|
159
|
+
#word(from, delimiter) {
|
|
160
|
+
const text = this.text;
|
|
161
|
+
const customDelimiter = delimiter !== DEFAULT_DELIMITER;
|
|
162
|
+
let at = from;
|
|
163
|
+
while (at < text.length && isWordChar(text.charAt(at))) {
|
|
164
|
+
if (customDelimiter && at > from && text.startsWith(delimiter, at))
|
|
165
|
+
break;
|
|
166
|
+
at += 1;
|
|
167
|
+
}
|
|
168
|
+
if (text.charAt(at) !== STRING_QUOTE)
|
|
169
|
+
return { kind: "word", from, to: at };
|
|
170
|
+
const prefix = text.slice(from, at);
|
|
171
|
+
if (ESCAPE_STRING_PREFIX.test(prefix)) {
|
|
172
|
+
const string = this.#quoted(at, STRING_QUOTE, "string", true);
|
|
173
|
+
return { kind: "string", from, to: string.to };
|
|
174
|
+
}
|
|
175
|
+
if (this.options.qQuotes === true && Q_QUOTE_PREFIX.test(prefix)) {
|
|
176
|
+
const string = this.#qQuoted(from, at);
|
|
177
|
+
if (string !== null)
|
|
178
|
+
return string;
|
|
179
|
+
}
|
|
180
|
+
if (this.options.nationalStrings === true && NATIONAL_STRING_PREFIX.test(prefix)) {
|
|
181
|
+
const string = this.#quoted(at, STRING_QUOTE, "string", this.options.backslashEscapes === true);
|
|
182
|
+
return { kind: "string", from, to: string.to };
|
|
183
|
+
}
|
|
184
|
+
return { kind: "word", from, to: at };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const GO = "GO";
|
|
188
|
+
const SLASH = "/";
|
|
189
|
+
/** What may follow `GO` on its line: a count, then a comment, as `sqlcmd` reads it. */
|
|
190
|
+
const GO_REST = /^(?:[ \t]+([1-9]\d*))?[ \t]*(?:--.*)?\r?$/;
|
|
191
|
+
const SLASH_REST = /^[ \t]*\r?$/;
|
|
192
|
+
const LINE_INDENT = /^[ \t]*$/;
|
|
193
|
+
/** The separator line `token` starts, or null when `token` is not alone on its line as one. */
|
|
194
|
+
export function separatorLineAt(text, token, separator) {
|
|
195
|
+
const lineStart = text.lastIndexOf(NEWLINE, token.from - 1) + 1;
|
|
196
|
+
if (!LINE_INDENT.test(text.slice(lineStart, token.from)))
|
|
197
|
+
return null;
|
|
198
|
+
const newline = text.indexOf(NEWLINE, token.to);
|
|
199
|
+
const lineEnd = newline < 0 ? text.length : newline;
|
|
200
|
+
const rest = text.slice(token.to, lineEnd);
|
|
201
|
+
const word = text.slice(token.from, token.to);
|
|
202
|
+
if (separator === "slash") {
|
|
203
|
+
return token.kind === "symbol" && word === SLASH && SLASH_REST.test(rest)
|
|
204
|
+
? { to: lineEnd, repeat: undefined }
|
|
205
|
+
: null;
|
|
206
|
+
}
|
|
207
|
+
if (token.kind !== "word" || word.toUpperCase() !== GO)
|
|
208
|
+
return null;
|
|
209
|
+
const match = GO_REST.exec(rest);
|
|
210
|
+
if (match === null)
|
|
211
|
+
return null;
|
|
212
|
+
const count = match[1];
|
|
213
|
+
return { to: lineEnd, repeat: count === undefined ? undefined : Number(count) };
|
|
214
|
+
}
|
|
215
|
+
/** Every token of `text` but comments, as one dialect reads it. */
|
|
216
|
+
export function significantTokens(text, options) {
|
|
217
|
+
const lexer = new Lexer(text, options);
|
|
218
|
+
const tokens = [];
|
|
219
|
+
for (let token = lexer.next(); token !== null; token = lexer.next()) {
|
|
220
|
+
if (token.kind !== "comment")
|
|
221
|
+
tokens.push(token);
|
|
222
|
+
}
|
|
223
|
+
return tokens;
|
|
224
|
+
}
|
|
225
|
+
export function wordOf(text, token) {
|
|
226
|
+
return text.slice(token.from, token.to).toUpperCase();
|
|
227
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Token } from "./tokens.js";
|
|
2
|
+
export interface Word {
|
|
3
|
+
readonly word: string;
|
|
4
|
+
/** Parentheses around it; 0 is the statement's own level. */
|
|
5
|
+
readonly depth: number;
|
|
6
|
+
}
|
|
7
|
+
/** The statement's words, upper-cased, each with its parenthesis depth. */
|
|
8
|
+
export declare function words(sql: string, tokens: readonly Token[]): Word[];
|
|
9
|
+
/** The verb that decides what a `WITH …` statement does: the first one outside the CTEs' parentheses. */
|
|
10
|
+
export declare function mainVerb(list: readonly Word[]): number;
|
|
11
|
+
/** MySQL reads `DESCRIBE` and `DESC` before a statement as `EXPLAIN`, `ANALYZE` included. */
|
|
12
|
+
export declare const EXPLAIN_VERBS: ReadonlySet<string>;
|
|
13
|
+
export interface ExplainPrefix {
|
|
14
|
+
/** Whether the statement runs: `ANALYZE` bare, or on inside the parenthesised options. */
|
|
15
|
+
readonly analyze: boolean;
|
|
16
|
+
/** The index in the word list where the explained statement starts. */
|
|
17
|
+
readonly statementAt: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reads `EXPLAIN [ANALYZE | ANALYSE] [VERBOSE] [( … )] [FORMAT = x] [QUERY PLAN] <statement>`
|
|
21
|
+
* (decision 031); null when the list does not start with `EXPLAIN`, `DESCRIBE` or `DESC`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function explainPrefix(list: readonly Word[]): ExplainPrefix | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { wordOf } from "./tokens.js";
|
|
2
|
+
const OPEN_PAREN = "(";
|
|
3
|
+
const CLOSE_PAREN = ")";
|
|
4
|
+
const WITH = "WITH";
|
|
5
|
+
const MAIN_VERBS = new Set(["SELECT", "INSERT", "UPDATE", "DELETE", "MERGE"]);
|
|
6
|
+
/** The statement's words, upper-cased, each with its parenthesis depth. */
|
|
7
|
+
export function words(sql, tokens) {
|
|
8
|
+
const found = [];
|
|
9
|
+
let depth = 0;
|
|
10
|
+
for (const token of tokens) {
|
|
11
|
+
if (token.kind === "symbol") {
|
|
12
|
+
const char = sql.charAt(token.from);
|
|
13
|
+
if (char === OPEN_PAREN)
|
|
14
|
+
depth += 1;
|
|
15
|
+
else if (char === CLOSE_PAREN)
|
|
16
|
+
depth = Math.max(0, depth - 1);
|
|
17
|
+
}
|
|
18
|
+
else if (token.kind === "word") {
|
|
19
|
+
found.push({ word: wordOf(sql, token), depth });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
/** The verb that decides what a `WITH …` statement does: the first one outside the CTEs' parentheses. */
|
|
25
|
+
export function mainVerb(list) {
|
|
26
|
+
if (list[0]?.word !== WITH)
|
|
27
|
+
return 0;
|
|
28
|
+
const index = list.findIndex((entry, at) => at > 0 && entry.depth === 0 && MAIN_VERBS.has(entry.word));
|
|
29
|
+
return index < 0 ? 0 : index;
|
|
30
|
+
}
|
|
31
|
+
/** MySQL reads `DESCRIBE` and `DESC` before a statement as `EXPLAIN`, `ANALYZE` included. */
|
|
32
|
+
export const EXPLAIN_VERBS = new Set(["EXPLAIN", "DESCRIBE", "DESC"]);
|
|
33
|
+
const ANALYZE_WORDS = new Set(["ANALYZE", "ANALYSE"]);
|
|
34
|
+
/** PostgreSQL's boolean option values that turn `ANALYZE` off inside `EXPLAIN ( … )`. */
|
|
35
|
+
const FALSE_WORDS = new Set(["FALSE", "OFF", "0"]);
|
|
36
|
+
/** Words between `EXPLAIN` and the statement that take no value. */
|
|
37
|
+
const FLAG_WORDS = new Set(["VERBOSE", "QUERY", "PLAN", "EXTENDED", "PARTITIONS"]);
|
|
38
|
+
const FORMAT = "FORMAT";
|
|
39
|
+
/**
|
|
40
|
+
* Reads `EXPLAIN [ANALYZE | ANALYSE] [VERBOSE] [( … )] [FORMAT = x] [QUERY PLAN] <statement>`
|
|
41
|
+
* (decision 031); null when the list does not start with `EXPLAIN`, `DESCRIBE` or `DESC`.
|
|
42
|
+
*/
|
|
43
|
+
export function explainPrefix(list) {
|
|
44
|
+
const first = list[0]?.word;
|
|
45
|
+
if (first === undefined || !EXPLAIN_VERBS.has(first))
|
|
46
|
+
return null;
|
|
47
|
+
let analyze = false;
|
|
48
|
+
let at = 1;
|
|
49
|
+
for (let entry = list[at]; entry !== undefined; entry = list[at]) {
|
|
50
|
+
if (entry.depth > 0) {
|
|
51
|
+
if (ANALYZE_WORDS.has(entry.word)) {
|
|
52
|
+
const value = list[at + 1];
|
|
53
|
+
analyze = !(value !== undefined && value.depth > 0 && FALSE_WORDS.has(value.word));
|
|
54
|
+
}
|
|
55
|
+
at += 1;
|
|
56
|
+
}
|
|
57
|
+
else if (ANALYZE_WORDS.has(entry.word)) {
|
|
58
|
+
analyze = true;
|
|
59
|
+
at += 1;
|
|
60
|
+
}
|
|
61
|
+
else if (entry.word === FORMAT) {
|
|
62
|
+
at += 2;
|
|
63
|
+
}
|
|
64
|
+
else if (FLAG_WORDS.has(entry.word)) {
|
|
65
|
+
at += 1;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { analyze, statementAt: Math.min(at, list.length) };
|
|
72
|
+
}
|