@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,21 @@
|
|
|
1
|
+
import { type Tok } from "./layoutNodes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Text built line by line with the column it has reached. A line comment forces the next token
|
|
4
|
+
* onto a new line at `indent`, the column the current layout continues at.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Writer {
|
|
7
|
+
#private;
|
|
8
|
+
out: string;
|
|
9
|
+
column: number;
|
|
10
|
+
indent: number;
|
|
11
|
+
constructor(column: number);
|
|
12
|
+
/** The text ends in a line comment, so nothing may follow it on its line. */
|
|
13
|
+
get endsInLineComment(): boolean;
|
|
14
|
+
newline(column?: number): void;
|
|
15
|
+
pad(width: number): void;
|
|
16
|
+
/** `text` (one token, or a comment that may span lines), a space before it when asked and the line has text. */
|
|
17
|
+
write(text: string, spaceBefore: boolean): void;
|
|
18
|
+
tok(tok: Tok, text: string, spaceBefore: boolean): void;
|
|
19
|
+
/** A comment on a line of its own. */
|
|
20
|
+
comment(comment: string, column: number): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isLineComment } from "./layoutNodes.js";
|
|
2
|
+
const LINE_BREAK = "\n";
|
|
3
|
+
const SPACE = " ";
|
|
4
|
+
/**
|
|
5
|
+
* Text built line by line with the column it has reached. A line comment forces the next token
|
|
6
|
+
* onto a new line at `indent`, the column the current layout continues at.
|
|
7
|
+
*/
|
|
8
|
+
export class Writer {
|
|
9
|
+
out = "";
|
|
10
|
+
column;
|
|
11
|
+
indent;
|
|
12
|
+
#content = false;
|
|
13
|
+
#pendingBreak = false;
|
|
14
|
+
constructor(column) {
|
|
15
|
+
this.column = column;
|
|
16
|
+
this.indent = column;
|
|
17
|
+
}
|
|
18
|
+
/** The text ends in a line comment, so nothing may follow it on its line. */
|
|
19
|
+
get endsInLineComment() {
|
|
20
|
+
return this.#pendingBreak;
|
|
21
|
+
}
|
|
22
|
+
newline(column = this.indent) {
|
|
23
|
+
this.#pendingBreak = false;
|
|
24
|
+
const lineStart = this.out.lastIndexOf(LINE_BREAK) + 1;
|
|
25
|
+
if (!this.#content) {
|
|
26
|
+
if (lineStart === 0)
|
|
27
|
+
return;
|
|
28
|
+
this.out = this.out.slice(0, lineStart) + SPACE.repeat(column);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.out += LINE_BREAK + SPACE.repeat(column);
|
|
32
|
+
}
|
|
33
|
+
this.column = column;
|
|
34
|
+
this.#content = false;
|
|
35
|
+
}
|
|
36
|
+
pad(width) {
|
|
37
|
+
if (width <= 0)
|
|
38
|
+
return;
|
|
39
|
+
this.out += SPACE.repeat(width);
|
|
40
|
+
this.column += width;
|
|
41
|
+
}
|
|
42
|
+
/** `text` (one token, or a comment that may span lines), a space before it when asked and the line has text. */
|
|
43
|
+
write(text, spaceBefore) {
|
|
44
|
+
if (this.#pendingBreak)
|
|
45
|
+
this.newline();
|
|
46
|
+
else if (spaceBefore && this.#content)
|
|
47
|
+
this.#append(SPACE);
|
|
48
|
+
this.#append(text);
|
|
49
|
+
}
|
|
50
|
+
tok(tok, text, spaceBefore) {
|
|
51
|
+
for (const comment of tok.leading) {
|
|
52
|
+
if (this.#content)
|
|
53
|
+
this.newline();
|
|
54
|
+
this.#append(comment);
|
|
55
|
+
this.newline();
|
|
56
|
+
}
|
|
57
|
+
this.write(text, spaceBefore);
|
|
58
|
+
for (const comment of tok.trailing) {
|
|
59
|
+
this.#append(SPACE + comment);
|
|
60
|
+
if (isLineComment(comment))
|
|
61
|
+
this.#pendingBreak = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** A comment on a line of its own. */
|
|
65
|
+
comment(comment, column) {
|
|
66
|
+
this.newline(column);
|
|
67
|
+
this.#append(comment);
|
|
68
|
+
if (isLineComment(comment))
|
|
69
|
+
this.#pendingBreak = true;
|
|
70
|
+
}
|
|
71
|
+
#append(text) {
|
|
72
|
+
this.out += text;
|
|
73
|
+
const lineBreak = text.lastIndexOf(LINE_BREAK);
|
|
74
|
+
this.column = lineBreak < 0 ? this.column + text.length : text.length - lineBreak - 1;
|
|
75
|
+
this.#content = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SavepointStatements, WireValue } from "@tablecraft-app/plugin-api";
|
|
2
|
+
/** How a dialect spells what the shared literal writer cannot decide for it. */
|
|
3
|
+
export interface SqlLiteralOptions {
|
|
4
|
+
/** `'` doubled is standard; MySQL also needs backslashes escaped unless `NO_BACKSLASH_ESCAPES` is set. */
|
|
5
|
+
readonly backslashEscapes: boolean;
|
|
6
|
+
/** A bytes literal from lower-case hex, such as `X'dead'` or `'\xdead'::bytea`. */
|
|
7
|
+
readonly bytes: (hex: string) => string;
|
|
8
|
+
/** Text holding a non-ASCII character is written `N'…'`, which SQL Server and Oracle need to keep it whole. */
|
|
9
|
+
readonly nationalText?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type IdentifierQuote = '"' | "`" | "[";
|
|
12
|
+
/**
|
|
13
|
+
* Quotes an identifier with `quote`, doubling the closing quote inside it: `"a""b"`, `` `a``b` ``
|
|
14
|
+
* or SQL Server's `[a]]b]`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function quoteIdentifierWith(quote: IdentifierQuote): (name: string) => string;
|
|
17
|
+
/** The standard savepoint statements, which PostgreSQL, MySQL and SQLite all accept (plan 026). */
|
|
18
|
+
export declare function sqlSavepoints(quoteIdentifier: (name: string) => string): (name: string) => SavepointStatements;
|
|
19
|
+
/** The standard insert of a row made only of defaults, which PostgreSQL and SQLite accept. */
|
|
20
|
+
export declare function defaultValuesInsert(qualifiedTable: string): string;
|
|
21
|
+
/** `DEFAULT` inside `SET`, which PostgreSQL and MySQL accept. */
|
|
22
|
+
export declare function defaultKeyword(): string;
|
|
23
|
+
/**
|
|
24
|
+
* A literal any of the three built-in dialects reads back as the same value. Decimals, dates and
|
|
25
|
+
* JSON travel as text (decision 017), so they are written as strings and the column's type
|
|
26
|
+
* converts them; `columnType` is accepted for dialects that need a cast.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sqlLiteral(value: WireValue, options: SqlLiteralOptions): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const CLOSING_QUOTE = { '"': '"', "`": "`", "[": "]" };
|
|
2
|
+
/**
|
|
3
|
+
* Quotes an identifier with `quote`, doubling the closing quote inside it: `"a""b"`, `` `a``b` ``
|
|
4
|
+
* or SQL Server's `[a]]b]`.
|
|
5
|
+
*/
|
|
6
|
+
export function quoteIdentifierWith(quote) {
|
|
7
|
+
const close = CLOSING_QUOTE[quote];
|
|
8
|
+
return (name) => `${quote}${name.replaceAll(close, close + close)}${close}`;
|
|
9
|
+
}
|
|
10
|
+
/** The standard savepoint statements, which PostgreSQL, MySQL and SQLite all accept (plan 026). */
|
|
11
|
+
export function sqlSavepoints(quoteIdentifier) {
|
|
12
|
+
return (name) => {
|
|
13
|
+
const quoted = quoteIdentifier(name);
|
|
14
|
+
return {
|
|
15
|
+
set: `SAVEPOINT ${quoted}`,
|
|
16
|
+
rollback: `ROLLBACK TO SAVEPOINT ${quoted}`,
|
|
17
|
+
release: `RELEASE SAVEPOINT ${quoted}`,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** The standard insert of a row made only of defaults, which PostgreSQL and SQLite accept. */
|
|
22
|
+
export function defaultValuesInsert(qualifiedTable) {
|
|
23
|
+
return `INSERT INTO ${qualifiedTable} DEFAULT VALUES`;
|
|
24
|
+
}
|
|
25
|
+
/** `DEFAULT` inside `SET`, which PostgreSQL and MySQL accept. */
|
|
26
|
+
export function defaultKeyword() {
|
|
27
|
+
return "DEFAULT";
|
|
28
|
+
}
|
|
29
|
+
const BACKSLASH_ESCAPES = new Map([
|
|
30
|
+
["\\", "\\\\"],
|
|
31
|
+
["'", "\\'"],
|
|
32
|
+
["\0", "\\0"],
|
|
33
|
+
["\n", "\\n"],
|
|
34
|
+
["\r", "\\r"],
|
|
35
|
+
["\u001a", "\\Z"],
|
|
36
|
+
]);
|
|
37
|
+
const NATIONAL_PREFIX = "N";
|
|
38
|
+
const NON_ASCII = /[\u0080-\uffff]/;
|
|
39
|
+
function quoteString(text, options) {
|
|
40
|
+
const prefix = options.nationalText === true && NON_ASCII.test(text) ? NATIONAL_PREFIX : "";
|
|
41
|
+
if (options.backslashEscapes) {
|
|
42
|
+
return `${prefix}'${Array.from(text, (char) => BACKSLASH_ESCAPES.get(char) ?? char).join("")}'`;
|
|
43
|
+
}
|
|
44
|
+
return `${prefix}'${text.replaceAll("'", "''")}'`;
|
|
45
|
+
}
|
|
46
|
+
const BYTES_PER_HEX_DIGIT = 0.5;
|
|
47
|
+
/**
|
|
48
|
+
* A literal any of the three built-in dialects reads back as the same value. Decimals, dates and
|
|
49
|
+
* JSON travel as text (decision 017), so they are written as strings and the column's type
|
|
50
|
+
* converts them; `columnType` is accepted for dialects that need a cast.
|
|
51
|
+
*/
|
|
52
|
+
export function sqlLiteral(value, options) {
|
|
53
|
+
if (value === null)
|
|
54
|
+
return "NULL";
|
|
55
|
+
switch (typeof value) {
|
|
56
|
+
case "boolean":
|
|
57
|
+
return value ? "TRUE" : "FALSE";
|
|
58
|
+
case "number":
|
|
59
|
+
return Number.isFinite(value) ? String(value) : quoteString(String(value), options);
|
|
60
|
+
case "string":
|
|
61
|
+
return quoteString(value, options);
|
|
62
|
+
default:
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
switch (value.kind) {
|
|
66
|
+
case "bigint":
|
|
67
|
+
return value.value;
|
|
68
|
+
case "json":
|
|
69
|
+
return quoteString(value.text, options);
|
|
70
|
+
case "date":
|
|
71
|
+
return quoteString(value.iso, options);
|
|
72
|
+
case "bytes": {
|
|
73
|
+
const literal = options.bytes(value.preview);
|
|
74
|
+
const shown = value.preview.length * BYTES_PER_HEX_DIGIT;
|
|
75
|
+
return shown < value.length ? `${literal} /* ${String(shown)} of ${String(value.length)} bytes */` : literal;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type NamespaceSwitch } from "@tablecraft-app/plugin-api";
|
|
2
|
+
import { type Identifier } from "./scope.js";
|
|
3
|
+
import { type NamespaceStatementForm, type SqlDialectOptions } from "./tokens.js";
|
|
4
|
+
/** Where a session-namespace statement points unqualified names. */
|
|
5
|
+
export interface NamespaceTarget {
|
|
6
|
+
readonly kind: "database" | "schema";
|
|
7
|
+
/** In the order the statement lists them; empty for `DEFAULT` and `RESET`, which restore the server's setting. */
|
|
8
|
+
readonly candidates: readonly Identifier[];
|
|
9
|
+
}
|
|
10
|
+
/** The caret's place in a session-namespace statement's target list. */
|
|
11
|
+
export interface NamespaceSlot {
|
|
12
|
+
readonly kind: NamespaceTarget["kind"];
|
|
13
|
+
/** What accepting an item replaces; inside a quote it starts after the quote. */
|
|
14
|
+
readonly from: number;
|
|
15
|
+
readonly to: number;
|
|
16
|
+
/** The quote the caret is inside, and whether the name already closes it. */
|
|
17
|
+
readonly quote: {
|
|
18
|
+
readonly char: string;
|
|
19
|
+
readonly closed: boolean;
|
|
20
|
+
} | null;
|
|
21
|
+
/** The names the statement lists before the caret's. */
|
|
22
|
+
readonly listed: readonly Identifier[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Where the caret at `offset` names a namespace a session-namespace statement of the dialect moves
|
|
26
|
+
* to: `USE |` or the elements of `SET search_path TO a, |` and `SET SCHEMA |`. Null anywhere else,
|
|
27
|
+
* and after a name whose closing quote is typed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function namespaceSlotAt(text: string, offset: number, dialect: SqlDialectOptions): NamespaceSlot | null;
|
|
30
|
+
/**
|
|
31
|
+
* Whether `sql` is a statement that moves the session's current namespace in a dialect declaring
|
|
32
|
+
* `namespaceStatements`, and where it points. It never asks the server, so a named namespace may
|
|
33
|
+
* not exist; the session's `currentNamespace()` is the truth once the statement ran.
|
|
34
|
+
*/
|
|
35
|
+
export declare function namespaceTarget(sql: string, dialect: SqlDialectOptions): NamespaceTarget | null;
|
|
36
|
+
/**
|
|
37
|
+
* The console's namespace switch for a dialect moving its namespace by `form` (decision 060):
|
|
38
|
+
* `USE db`, or a search path of exactly the one schema, so nothing unqualified resolves in a schema
|
|
39
|
+
* the console's label does not show. Only the search path is undone by a rollback.
|
|
40
|
+
*/
|
|
41
|
+
export declare function namespaceSwitcher(form: NamespaceStatementForm, quoteIdentifier: (name: string) => string): NamespaceSwitch;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { PluginError } from "@tablecraft-app/plugin-api";
|
|
2
|
+
import { closingQuote, unquote } from "./scope.js";
|
|
3
|
+
import { Lexer, significantTokens, wordOf, } from "./tokens.js";
|
|
4
|
+
const USE = "USE";
|
|
5
|
+
const SET = "SET";
|
|
6
|
+
const RESET = "RESET";
|
|
7
|
+
const SET_SCOPES = new Set(["SESSION", "LOCAL"]);
|
|
8
|
+
const SEARCH_PATH = "SEARCH_PATH";
|
|
9
|
+
const SCHEMA = "SCHEMA";
|
|
10
|
+
const ALL = "ALL";
|
|
11
|
+
const ASSIGNMENTS = new Set(["TO", "="]);
|
|
12
|
+
const DEFAULT = "DEFAULT";
|
|
13
|
+
const LIST_SEPARATOR = ",";
|
|
14
|
+
const STRING_QUOTE = "'";
|
|
15
|
+
const BLOCK_COMMENT_END = "*/";
|
|
16
|
+
function identifier(text, token) {
|
|
17
|
+
if (token === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
const raw = text.slice(token.from, token.to);
|
|
20
|
+
const { from, to } = token;
|
|
21
|
+
if (token.kind === "word")
|
|
22
|
+
return { name: raw, quoted: false, from, to };
|
|
23
|
+
if (token.kind === "quoted")
|
|
24
|
+
return { name: unquote(raw), quoted: true, from, to };
|
|
25
|
+
// search_path quotes each element (GUC_LIST_QUOTE), so a string element is an exact name.
|
|
26
|
+
if (token.kind === "string" && raw.startsWith(STRING_QUOTE))
|
|
27
|
+
return { name: unquote(raw), quoted: true, from, to };
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function wordAt(text, tokens, at) {
|
|
31
|
+
const token = tokens[at];
|
|
32
|
+
if (token === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
return token.kind === "word" || token.kind === "symbol" ? wordOf(text, token) : undefined;
|
|
35
|
+
}
|
|
36
|
+
function useTarget(text, tokens) {
|
|
37
|
+
if (tokens.length !== 2 || wordAt(text, tokens, 0) !== USE)
|
|
38
|
+
return null;
|
|
39
|
+
const name = identifier(text, tokens[1]);
|
|
40
|
+
return name === null || tokens[1]?.kind === "string" ? null : { kind: "database", candidates: [name] };
|
|
41
|
+
}
|
|
42
|
+
/** `a, "b", 'c'` from `at` to the end; null when it is not such a list. */
|
|
43
|
+
function list(text, tokens, at) {
|
|
44
|
+
const names = [];
|
|
45
|
+
for (let index = at; index < tokens.length; index += 2) {
|
|
46
|
+
const name = identifier(text, tokens[index]);
|
|
47
|
+
if (name === null)
|
|
48
|
+
return null;
|
|
49
|
+
names.push(name);
|
|
50
|
+
const separator = tokens[index + 1];
|
|
51
|
+
if (separator !== undefined && wordAt(text, tokens, index + 1) !== LIST_SEPARATOR)
|
|
52
|
+
return null;
|
|
53
|
+
if (separator !== undefined && index + 2 >= tokens.length)
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return names.length === 0 ? null : names;
|
|
57
|
+
}
|
|
58
|
+
function searchPathTarget(text, tokens) {
|
|
59
|
+
const first = wordAt(text, tokens, 0);
|
|
60
|
+
if (first === RESET) {
|
|
61
|
+
const what = wordAt(text, tokens, 1);
|
|
62
|
+
return tokens.length === 2 && (what === SEARCH_PATH || what === ALL) ? { kind: "schema", candidates: [] } : null;
|
|
63
|
+
}
|
|
64
|
+
if (first !== SET)
|
|
65
|
+
return null;
|
|
66
|
+
let at = SET_SCOPES.has(wordAt(text, tokens, 1) ?? "") ? 2 : 1;
|
|
67
|
+
const parameter = wordAt(text, tokens, at);
|
|
68
|
+
at += 1;
|
|
69
|
+
if (parameter === SEARCH_PATH) {
|
|
70
|
+
if (!ASSIGNMENTS.has(wordAt(text, tokens, at) ?? ""))
|
|
71
|
+
return null;
|
|
72
|
+
at += 1;
|
|
73
|
+
}
|
|
74
|
+
else if (parameter !== SCHEMA) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (tokens.length === at + 1 && tokens[at]?.kind === "word" && wordAt(text, tokens, at) === DEFAULT) {
|
|
78
|
+
return { kind: "schema", candidates: [] };
|
|
79
|
+
}
|
|
80
|
+
const names = list(text, tokens, at);
|
|
81
|
+
if (names === null || (parameter === SCHEMA && names.length !== 1))
|
|
82
|
+
return null;
|
|
83
|
+
return { kind: "schema", candidates: names };
|
|
84
|
+
}
|
|
85
|
+
const NAME_TOKENS = new Set(["word", "quoted", "string"]);
|
|
86
|
+
/** The statement's tokens around `offset`, comments dropped; null when the caret is in a comment. */
|
|
87
|
+
function tokensAround(text, offset, dialect) {
|
|
88
|
+
const lexer = new Lexer(text, dialect);
|
|
89
|
+
let tokens = [];
|
|
90
|
+
for (let token = lexer.next(); token !== null; token = lexer.next()) {
|
|
91
|
+
if (token.kind === "comment") {
|
|
92
|
+
if (token.from < offset && offset < token.to)
|
|
93
|
+
return null;
|
|
94
|
+
if (offset === token.to && !text.slice(token.from, token.to).endsWith(BLOCK_COMMENT_END))
|
|
95
|
+
return null;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (token.kind === "delimiter") {
|
|
99
|
+
if (token.to <= offset)
|
|
100
|
+
tokens = [];
|
|
101
|
+
else if (token.from >= offset)
|
|
102
|
+
break;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
tokens.push(token);
|
|
106
|
+
}
|
|
107
|
+
return tokens;
|
|
108
|
+
}
|
|
109
|
+
function closes(raw) {
|
|
110
|
+
return raw.length >= 2 && raw.endsWith(closingQuote(raw.charAt(0)));
|
|
111
|
+
}
|
|
112
|
+
/** The names of `name , name ,` from `at` to the end; null when the tokens are not such a list. */
|
|
113
|
+
function listedBefore(text, tokens, at) {
|
|
114
|
+
if ((tokens.length - at) % 2 !== 0)
|
|
115
|
+
return null;
|
|
116
|
+
const names = [];
|
|
117
|
+
for (let index = at; index < tokens.length; index += 2) {
|
|
118
|
+
const name = identifier(text, tokens[index]);
|
|
119
|
+
if (name === null || wordAt(text, tokens, index + 1) !== LIST_SEPARATOR)
|
|
120
|
+
return null;
|
|
121
|
+
names.push(name);
|
|
122
|
+
}
|
|
123
|
+
return names;
|
|
124
|
+
}
|
|
125
|
+
function searchPathListStart(text, tokens) {
|
|
126
|
+
if (wordAt(text, tokens, 0) !== SET)
|
|
127
|
+
return null;
|
|
128
|
+
let at = SET_SCOPES.has(wordAt(text, tokens, 1) ?? "") ? 2 : 1;
|
|
129
|
+
const parameter = wordAt(text, tokens, at);
|
|
130
|
+
at += 1;
|
|
131
|
+
if (parameter === SCHEMA)
|
|
132
|
+
return { at, single: true };
|
|
133
|
+
if (parameter !== SEARCH_PATH || !ASSIGNMENTS.has(wordAt(text, tokens, at) ?? ""))
|
|
134
|
+
return null;
|
|
135
|
+
return { at: at + 1, single: false };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Where the caret at `offset` names a namespace a session-namespace statement of the dialect moves
|
|
139
|
+
* to: `USE |` or the elements of `SET search_path TO a, |` and `SET SCHEMA |`. Null anywhere else,
|
|
140
|
+
* and after a name whose closing quote is typed.
|
|
141
|
+
*/
|
|
142
|
+
export function namespaceSlotAt(text, offset, dialect) {
|
|
143
|
+
const forms = dialect.namespaceStatements ?? [];
|
|
144
|
+
if (forms.length === 0)
|
|
145
|
+
return null;
|
|
146
|
+
const tokens = tokensAround(text, offset, dialect);
|
|
147
|
+
if (tokens === null)
|
|
148
|
+
return null;
|
|
149
|
+
const touched = tokens.findIndex((token) => token.from < offset && offset <= token.to);
|
|
150
|
+
const prefix = touched >= 0 && NAME_TOKENS.has(tokens[touched]?.kind ?? "symbol") ? tokens[touched] : undefined;
|
|
151
|
+
const before = tokens.filter((token) => (prefix === undefined ? token.to <= offset : token.to <= prefix.from));
|
|
152
|
+
let from = prefix?.from ?? offset;
|
|
153
|
+
let quote = null;
|
|
154
|
+
if (prefix !== undefined && prefix.kind !== "word") {
|
|
155
|
+
const raw = text.slice(prefix.from, prefix.to);
|
|
156
|
+
const char = raw.charAt(0);
|
|
157
|
+
if (prefix.kind === "string" && char !== STRING_QUOTE)
|
|
158
|
+
return null;
|
|
159
|
+
const closed = closes(raw);
|
|
160
|
+
if (closed && offset === prefix.to)
|
|
161
|
+
return null;
|
|
162
|
+
quote = { char, closed };
|
|
163
|
+
from = prefix.from + char.length;
|
|
164
|
+
}
|
|
165
|
+
for (const form of forms) {
|
|
166
|
+
if (form === "use") {
|
|
167
|
+
if (prefix?.kind === "string" || before.length !== 1 || wordAt(text, before, 0) !== USE)
|
|
168
|
+
continue;
|
|
169
|
+
return { kind: "database", from, to: offset, quote, listed: [] };
|
|
170
|
+
}
|
|
171
|
+
const start = searchPathListStart(text, before);
|
|
172
|
+
if (start === null)
|
|
173
|
+
continue;
|
|
174
|
+
if (start.single && before.length !== start.at)
|
|
175
|
+
continue;
|
|
176
|
+
const listed = listedBefore(text, before, start.at);
|
|
177
|
+
if (listed !== null)
|
|
178
|
+
return { kind: "schema", from, to: offset, quote, listed };
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Whether `sql` is a statement that moves the session's current namespace in a dialect declaring
|
|
184
|
+
* `namespaceStatements`, and where it points. It never asks the server, so a named namespace may
|
|
185
|
+
* not exist; the session's `currentNamespace()` is the truth once the statement ran.
|
|
186
|
+
*/
|
|
187
|
+
export function namespaceTarget(sql, dialect) {
|
|
188
|
+
const forms = dialect.namespaceStatements ?? [];
|
|
189
|
+
if (forms.length === 0)
|
|
190
|
+
return null;
|
|
191
|
+
const tokens = significantTokens(sql, dialect).filter((token) => token.kind !== "delimiter");
|
|
192
|
+
for (const form of forms) {
|
|
193
|
+
const target = form === "use" ? useTarget(sql, tokens) : searchPathTarget(sql, tokens);
|
|
194
|
+
if (target !== null)
|
|
195
|
+
return target;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const SWITCH_TARGETS = {
|
|
200
|
+
use: "topLevel",
|
|
201
|
+
searchPath: "schema",
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* The console's namespace switch for a dialect moving its namespace by `form` (decision 060):
|
|
205
|
+
* `USE db`, or a search path of exactly the one schema, so nothing unqualified resolves in a schema
|
|
206
|
+
* the console's label does not show. Only the search path is undone by a rollback.
|
|
207
|
+
*/
|
|
208
|
+
export function namespaceSwitcher(form, quoteIdentifier) {
|
|
209
|
+
return {
|
|
210
|
+
targets: SWITCH_TARGETS[form],
|
|
211
|
+
transactional: form === "searchPath",
|
|
212
|
+
statement(path) {
|
|
213
|
+
const name = path.at(-1);
|
|
214
|
+
if (name === undefined) {
|
|
215
|
+
throw new PluginError("statementFailed", "A namespace switch names no namespace; pick one from the list.");
|
|
216
|
+
}
|
|
217
|
+
return form === "use" ? `USE ${quoteIdentifier(name)}` : `SET search_path TO ${quoteIdentifier(name)}`;
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PluginError, type CheckDesign, type ColumnDesign, type DdlScript, type ForeignKeyDesign, type IndexDesign, type Relation, type TableDesign } from "@tablecraft-app/plugin-api";
|
|
2
|
+
/** The referential actions a design may name, as SQL spells them. */
|
|
3
|
+
export declare const REFERENTIAL_ACTIONS: readonly ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"];
|
|
4
|
+
/** A failure the user fixes in the dialog, so the host shows it as the dialog's error. */
|
|
5
|
+
export declare function designError(message: string): PluginError;
|
|
6
|
+
/**
|
|
7
|
+
* The design of a described table (decision 052). `columnDefault` turns the catalog's default into
|
|
8
|
+
* the expression a `DEFAULT` clause takes, for a dialect whose catalog stores it otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export declare function tableDesignOf(relation: Relation, columnDefault?: (column: Relation["columns"][number]) => string | null): TableDesign;
|
|
11
|
+
/** Throws a `designError` naming the first thing in `design` no dialect can create. */
|
|
12
|
+
export declare function validateDesign(design: TableDesign): void;
|
|
13
|
+
/** What an `alterTable` changes, each part matched by its `original` name. */
|
|
14
|
+
export interface TableDifference {
|
|
15
|
+
readonly renamedTo: string | null;
|
|
16
|
+
readonly commentChanged: boolean;
|
|
17
|
+
readonly droppedColumns: readonly ColumnDesign[];
|
|
18
|
+
readonly addedColumns: readonly ColumnDesign[];
|
|
19
|
+
/** A column whose name or definition differs; `before` is as the table has it now. */
|
|
20
|
+
readonly changedColumns: readonly {
|
|
21
|
+
readonly before: ColumnDesign;
|
|
22
|
+
readonly after: ColumnDesign;
|
|
23
|
+
}[];
|
|
24
|
+
readonly primaryKeyChanged: boolean;
|
|
25
|
+
readonly droppedIndexes: readonly IndexDesign[];
|
|
26
|
+
readonly addedIndexes: readonly IndexDesign[];
|
|
27
|
+
readonly droppedForeignKeys: readonly ForeignKeyDesign[];
|
|
28
|
+
readonly addedForeignKeys: readonly ForeignKeyDesign[];
|
|
29
|
+
readonly droppedChecks: readonly CheckDesign[];
|
|
30
|
+
readonly addedChecks: readonly CheckDesign[];
|
|
31
|
+
}
|
|
32
|
+
/** Whether a column's definition, everything but its name, differs. */
|
|
33
|
+
export declare function columnChanged(before: ColumnDesign, after: ColumnDesign): boolean;
|
|
34
|
+
export declare function tableDifference(before: TableDesign, after: TableDesign): TableDifference;
|
|
35
|
+
/** Whether a difference holds no change at all. */
|
|
36
|
+
export declare function unchanged(difference: TableDifference): boolean;
|
|
37
|
+
/** A script of `statements` with nothing around them. */
|
|
38
|
+
export declare function ddlScript(statements: readonly string[], options: Partial<Omit<DdlScript, "statements">> & Pick<DdlScript, "transactional">): DdlScript;
|
|
39
|
+
/** `NOTHING_TO_CHANGE` is the note an alter without a difference carries, so the dialog can say so. */
|
|
40
|
+
export declare const NOTHING_TO_CHANGE = "The design matches the table; there is nothing to run.";
|
|
41
|
+
/** The `REFERENCES` tail of a foreign key, actions only where they differ from the default. */
|
|
42
|
+
export declare function referencesClause(key: ForeignKeyDesign, qualifiedTarget: string, name: (identifier: string) => string): string;
|