@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,117 @@
1
+ import { formatDialect, mysql, plsql, postgresql, sqlite, transactsql, } from "sql-formatter";
2
+ import { formatAligned } from "./layout.js";
3
+ import { Lexer, separatorLineAt, wordOf } from "./tokens.js";
4
+ /** The `sql-formatter` dialects plugins may name (decisions 042 and 100). */
5
+ export const FORMAT_DIALECTS = { postgresql, mysql, sqlite, plsql, transactsql };
6
+ const DELIMITER_COMMAND = "DELIMITER";
7
+ const DELIMITER_REFUSAL = "a DELIMITER line changes what ends a statement; reformat the statements between them.";
8
+ const SEPARATOR_REFUSAL = "a batch separator line splits the script; reformat the batches between them.";
9
+ const UNREADABLE = "the text is not SQL the formatter can read.";
10
+ const NAMED_PREFIX = ":";
11
+ const NUMBERED_PREFIX = "$";
12
+ const BLOCK_COMMENT_OPEN = "/*";
13
+ const STAND_IN_TAG = "table_craft_comment_";
14
+ function paramTypes(styles) {
15
+ return {
16
+ named: styles.includes("named") ? [NAMED_PREFIX] : [],
17
+ numbered: styles.includes("numbered") ? [NUMBERED_PREFIX] : [],
18
+ positional: styles.includes("positional"),
19
+ };
20
+ }
21
+ function startsALine(text, from) {
22
+ const lineStart = text.lastIndexOf("\n", from - 1) + 1;
23
+ return text.slice(lineStart, from).trim() === "";
24
+ }
25
+ function hasDelimiterCommand(text, dialect) {
26
+ const lexer = new Lexer(text, dialect);
27
+ for (let token = lexer.next(); token !== null; token = lexer.next()) {
28
+ if (token.kind === "word" && wordOf(text, token) === DELIMITER_COMMAND && startsALine(text, token.from))
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ function hasSeparatorLine(text, dialect) {
34
+ const separator = dialect.batchSeparator;
35
+ if (separator === undefined)
36
+ return false;
37
+ const lexer = new Lexer(text, dialect);
38
+ for (let token = lexer.next(); token !== null; token = lexer.next()) {
39
+ if (separatorLineAt(text, token, separator) !== null)
40
+ return true;
41
+ }
42
+ return false;
43
+ }
44
+ /**
45
+ * The library re-indents the continuation lines of a block comment. Each multi-line one goes in as
46
+ * a one-line stand-in, which it leaves alone, and comes back as written.
47
+ */
48
+ function shieldBlockComments(text, dialect) {
49
+ const lexer = new Lexer(text, dialect);
50
+ const comments = [];
51
+ let shielded = "";
52
+ let at = 0;
53
+ let salt = 0;
54
+ while (text.includes(standIn(salt, 0)))
55
+ salt += 1;
56
+ for (let token = lexer.next(); token !== null; token = lexer.next()) {
57
+ const content = text.slice(token.from, token.to);
58
+ if (token.kind !== "comment" || !content.startsWith(BLOCK_COMMENT_OPEN) || !content.includes("\n"))
59
+ continue;
60
+ shielded += text.slice(at, token.from) + standIn(salt, comments.length);
61
+ comments.push(content);
62
+ at = token.to;
63
+ }
64
+ if (comments.length === 0)
65
+ return { text, restore: (formatted) => formatted };
66
+ return {
67
+ text: shielded + text.slice(at),
68
+ restore: (formatted) => comments.reduce((result, comment, index) => result.replace(standIn(salt, index), () => comment), formatted),
69
+ };
70
+ }
71
+ function standIn(salt, index) {
72
+ return `/*${STAND_IN_TAG}${String(salt)}_${String(index)}*/`;
73
+ }
74
+ /** The library's parse errors run to a page of grammar; the first line says where. */
75
+ function reasonOf(cause) {
76
+ const message = cause instanceof Error ? cause.message : String(cause);
77
+ const firstLine = message.split("\n", 1)[0]?.trim() ?? "";
78
+ return firstLine === "" ? UNREADABLE : firstLine;
79
+ }
80
+ /**
81
+ * `SqlLanguage.format`: the aligned layout (decision 061) or `sql-formatter`'s (decision 042), which
82
+ * also takes every statement the aligned one does not read (decision 062). Throws an `Error` with a
83
+ * one-line reason rather than returning half-formatted text; comments and literals pass through as
84
+ * written.
85
+ */
86
+ export function sqlFormatter(options) {
87
+ const dialect = FORMAT_DIALECTS[options.dialect];
88
+ const params = paramTypes(options.parameters);
89
+ const standard = (text, format) => {
90
+ const shielded = shieldBlockComments(text, options.lexer);
91
+ try {
92
+ return shielded.restore(formatDialect(shielded.text, {
93
+ dialect,
94
+ keywordCase: format.keywordCase,
95
+ dataTypeCase: format.keywordCase,
96
+ tabWidth: format.indentWidth,
97
+ linesBetweenQueries: format.linesBetweenStatements,
98
+ paramTypes: params,
99
+ }));
100
+ }
101
+ catch (cause) {
102
+ throw new Error(reasonOf(cause), { cause });
103
+ }
104
+ };
105
+ return {
106
+ format(text, format) {
107
+ if (options.lexer.delimiterCommand === true && hasDelimiterCommand(text, options.lexer)) {
108
+ throw new Error(DELIMITER_REFUSAL);
109
+ }
110
+ if (hasSeparatorLine(text, options.lexer))
111
+ throw new Error(SEPARATOR_REFUSAL);
112
+ if (format.style === "standard")
113
+ return standard(text, format);
114
+ return formatAligned(text, format, options.lexer, (statement) => standard(statement, format));
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,47 @@
1
+ import type { CompletionItem, FunctionSignature } from "@tablecraft-app/plugin-api";
2
+ /** Where a function belongs in the server's manual; it groups the data, the popup does not show it. */
3
+ export declare const FUNCTION_CATEGORIES: readonly ["string", "numeric", "datetime", "json", "aggregate", "window", "information", "encryption", "locking", "spatial", "xml", "control", "misc", "server"];
4
+ export type FunctionCategory = (typeof FUNCTION_CATEGORIES)[number];
5
+ /**
6
+ * The versions of each server flavour a function exists in: space-separated comparators all of which
7
+ * hold, such as `">=5.7.22 <8.0"`, or `false` for none. A flavour it does not name has it in every
8
+ * version (decision 097).
9
+ */
10
+ export type ServerRange = Readonly<Record<string, string | false>>;
11
+ /** A server as completion reads it: the plugin's name for its flavour and the version in it. */
12
+ export interface ServerIdentity {
13
+ readonly flavour: string;
14
+ readonly version: readonly number[];
15
+ }
16
+ /** A built-in function a plugin documents (decision 097). */
17
+ export interface FunctionSpec {
18
+ /** As the manual writes it, upper case; the popup offers it lower case. */
19
+ readonly name: string;
20
+ readonly signatures: readonly FunctionSignature[];
21
+ readonly summary: string;
22
+ readonly category: FunctionCategory;
23
+ readonly available?: ServerRange;
24
+ }
25
+ /** `[name, signatures, summary, available?]`, a signature written `"date, format -> string"`. */
26
+ export type FunctionEntry = readonly [
27
+ name: string,
28
+ signatures: string | readonly string[],
29
+ summary: string,
30
+ available?: ServerRange
31
+ ];
32
+ /** Reads `"str, len? -> string"`: `?` marks an optional parameter, `...` one that repeats. */
33
+ export declare function parseSignature(written: string): FunctionSignature;
34
+ /** Specs for one category, from compact entries a plugin's data file lists. */
35
+ export declare function functionSpecs(category: FunctionCategory, entries: readonly FunctionEntry[]): FunctionSpec[];
36
+ /** Whether `spec` exists on `server`; an unknown server has every function any flavour has. */
37
+ export declare function isAvailable(spec: FunctionSpec, server: ServerIdentity | null): boolean;
38
+ /** `(date, format) → string`, the popup's detail for one signature. */
39
+ export declare function signatureLabel(signature: FunctionSignature): string;
40
+ /** Every signature on its own line, then the summary. */
41
+ export declare function functionInfo(spec: FunctionSpec): string;
42
+ /** A spec as an item: `name()` with the caret between the parentheses when it takes arguments. */
43
+ export declare function functionItem(spec: FunctionSpec | string, boost: number): CompletionItem;
44
+ /** The name a function entry is offered under, either form. */
45
+ export declare function functionName(spec: FunctionSpec | string): string;
46
+ /** Aggregate and scalar functions every built-in dialect has; a plugin adds its own. Written in our own words. */
47
+ export declare const SQL_FUNCTIONS: readonly FunctionSpec[];
@@ -0,0 +1,159 @@
1
+ /** Where a function belongs in the server's manual; it groups the data, the popup does not show it. */
2
+ export const FUNCTION_CATEGORIES = [
3
+ "string",
4
+ "numeric",
5
+ "datetime",
6
+ "json",
7
+ "aggregate",
8
+ "window",
9
+ "information",
10
+ "encryption",
11
+ "locking",
12
+ "spatial",
13
+ "xml",
14
+ "control",
15
+ "misc",
16
+ "server",
17
+ ];
18
+ const RETURNS_ARROW = "->";
19
+ const SHOWN_ARROW = "→";
20
+ const OPTIONAL_MARK = "?";
21
+ const VARIADIC_MARK = "...";
22
+ const COMPARATOR = /^(>=|<=|>|<|=)?(\d+(?:\.\d+)*)$/;
23
+ function parameter(written) {
24
+ let name = written.trim();
25
+ const variadic = name.endsWith(VARIADIC_MARK);
26
+ if (variadic)
27
+ name = name.slice(0, -VARIADIC_MARK.length).trim();
28
+ const optional = name.endsWith(OPTIONAL_MARK);
29
+ if (optional)
30
+ name = name.slice(0, -OPTIONAL_MARK.length).trim();
31
+ return { name, ...(optional ? { optional } : {}), ...(variadic ? { variadic } : {}) };
32
+ }
33
+ /** Reads `"str, len? -> string"`: `?` marks an optional parameter, `...` one that repeats. */
34
+ export function parseSignature(written) {
35
+ const arrow = written.lastIndexOf(RETURNS_ARROW);
36
+ if (arrow < 0)
37
+ throw new Error(`Signature "${written}" has no "${RETURNS_ARROW} type"; add what it returns.`);
38
+ const list = written.slice(0, arrow).trim();
39
+ return {
40
+ parameters: list === "" ? [] : list.split(",").map(parameter),
41
+ returns: written.slice(arrow + RETURNS_ARROW.length).trim(),
42
+ };
43
+ }
44
+ /** Specs for one category, from compact entries a plugin's data file lists. */
45
+ export function functionSpecs(category, entries) {
46
+ return entries.map(([name, signatures, summary, available]) => ({
47
+ name,
48
+ signatures: (typeof signatures === "string" ? [signatures] : signatures).map(parseSignature),
49
+ summary,
50
+ category,
51
+ ...(available === undefined ? {} : { available }),
52
+ }));
53
+ }
54
+ function compareVersions(left, right) {
55
+ for (let at = 0; at < Math.max(left.length, right.length); at += 1) {
56
+ const difference = (left[at] ?? 0) - (right[at] ?? 0);
57
+ if (difference !== 0)
58
+ return difference;
59
+ }
60
+ return 0;
61
+ }
62
+ function satisfies(version, comparator) {
63
+ const match = COMPARATOR.exec(comparator);
64
+ if (match === null)
65
+ throw new Error(`Version comparator "${comparator}" is not like ">=8.0.4"; fix the range.`);
66
+ const order = compareVersions(version, (match[2] ?? "").split(".").map(Number));
67
+ switch (match[1]) {
68
+ case ">=":
69
+ return order >= 0;
70
+ case "<=":
71
+ return order <= 0;
72
+ case ">":
73
+ return order > 0;
74
+ case "<":
75
+ return order < 0;
76
+ default:
77
+ return order === 0;
78
+ }
79
+ }
80
+ /** Whether `spec` exists on `server`; an unknown server has every function any flavour has. */
81
+ export function isAvailable(spec, server) {
82
+ if (spec.available === undefined || server === null)
83
+ return true;
84
+ const range = spec.available[server.flavour];
85
+ if (range === undefined)
86
+ return true;
87
+ if (range === false)
88
+ return false;
89
+ return range
90
+ .split(/\s+/)
91
+ .filter((comparator) => comparator !== "")
92
+ .every((comparator) => satisfies(server.version, comparator));
93
+ }
94
+ function shownParameter(parameter) {
95
+ const name = parameter.variadic === true ? `${parameter.name}${VARIADIC_MARK}` : parameter.name;
96
+ return parameter.optional === true ? `[${name}]` : name;
97
+ }
98
+ /** `(date, format) → string`, the popup's detail for one signature. */
99
+ export function signatureLabel(signature) {
100
+ return `(${signature.parameters.map(shownParameter).join(", ")}) ${SHOWN_ARROW} ${signature.returns}`;
101
+ }
102
+ /** Every signature on its own line, then the summary. */
103
+ export function functionInfo(spec) {
104
+ const label = spec.name.toLowerCase();
105
+ const lines = spec.signatures.map((signature) => `${label}${signatureLabel(signature)}`);
106
+ return `${lines.join("\n")}\n\n${spec.summary}`;
107
+ }
108
+ /** A spec as an item: `name()` with the caret between the parentheses when it takes arguments. */
109
+ export function functionItem(spec, boost) {
110
+ if (typeof spec === "string")
111
+ return { label: spec, kind: "function", detail: null, apply: spec, boost, info: null, caret: null };
112
+ const label = spec.name.toLowerCase();
113
+ const first = spec.signatures[0];
114
+ const takesArguments = spec.signatures.some((signature) => signature.parameters.length > 0);
115
+ return {
116
+ label,
117
+ kind: "function",
118
+ detail: first === undefined ? null : signatureLabel(first),
119
+ apply: `${label}()`,
120
+ boost,
121
+ info: functionInfo(spec),
122
+ caret: takesArguments ? label.length + 1 : null,
123
+ };
124
+ }
125
+ /** The name a function entry is offered under, either form. */
126
+ export function functionName(spec) {
127
+ return typeof spec === "string" ? spec : spec.name;
128
+ }
129
+ /** Aggregate and scalar functions every built-in dialect has; a plugin adds its own. Written in our own words. */
130
+ export const SQL_FUNCTIONS = [
131
+ ...functionSpecs("aggregate", [
132
+ ["AVG", "expr -> number", "The mean of the non-null values in the group."],
133
+ ["COUNT", ["* -> integer", "expr -> integer"], "How many rows the group has, or how many give a non-null value."],
134
+ ["MAX", "expr -> value", "The largest value in the group."],
135
+ ["MIN", "expr -> value", "The smallest value in the group."],
136
+ ["SUM", "expr -> number", "The total of the values in the group."],
137
+ ]),
138
+ ...functionSpecs("numeric", [
139
+ ["ABS", "x -> number", "The value without its sign."],
140
+ ["ROUND", "x, digits? -> number", "Rounds to the given number of decimal places, zero by default."],
141
+ ]),
142
+ ...functionSpecs("string", [
143
+ ["LENGTH", "str -> integer", "How long the string is."],
144
+ ["LOWER", "str -> string", "The string in lower case."],
145
+ ["REPLACE", "str, from, to -> string", "Swaps every occurrence of one substring for another."],
146
+ [
147
+ "SUBSTR",
148
+ "str, start, length? -> string",
149
+ "Part of the string from a 1-based position, optionally of a set length.",
150
+ ],
151
+ ["TRIM", "str -> string", "The string without spaces at either end."],
152
+ ["UPPER", "str -> string", "The string in upper case."],
153
+ ]),
154
+ ...functionSpecs("control", [
155
+ ["CAST", "expr AS type -> value", "Converts a value to another type."],
156
+ ["COALESCE", "value... -> value", "The first argument that is not null."],
157
+ ["NULLIF", "a, b -> value", "Null when both arguments are equal, else the first."],
158
+ ]),
159
+ ];
@@ -0,0 +1,22 @@
1
+ import type { ImportKind, InferredColumn, SqlLanguage } from "@tablecraft-app/plugin-api";
2
+ /** What the shared generator needs to know about a dialect (decision 029). */
3
+ export interface GeneratorOptions {
4
+ readonly quoteIdentifier: (name: string) => string;
5
+ readonly updateDefault: (defaultExpression: string | null) => string;
6
+ /** A name the dialect reads back unchanged without quotes; any other is quoted. */
7
+ readonly plainIdentifier: RegExp;
8
+ /** A name that spells one of these is quoted. Upper case. */
9
+ readonly keywords: readonly string[];
10
+ }
11
+ /** The completer's rule, so a generated statement spells a name as completion inserts it. */
12
+ export declare function identifierWriter(options: Omit<GeneratorOptions, "updateDefault">): (identifier: string) => string;
13
+ /** DROP, SELECT, INSERT and UPDATE for a catalog relation, which every built-in plugin spreads into its `SqlLanguage`. */
14
+ export declare function sqlGenerator(options: GeneratorOptions): Pick<SqlLanguage, "generate">;
15
+ /** What `sqlImporter` needs: the generator's naming rule, and the dialect's column type per inferred kind. */
16
+ export interface ImporterOptions extends Omit<GeneratorOptions, "updateDefault"> {
17
+ /** The type for a kind; `decimal` also gets the sample's digits, so a dialect can size it. */
18
+ readonly types: Readonly<Record<Exclude<ImportKind, "decimal">, string>>;
19
+ readonly decimal: (column: InferredColumn) => string;
20
+ }
21
+ /** `importType` and `createTable` for a plugin that declares `bulkInsert` (decision 046). */
22
+ export declare function sqlImporter(options: ImporterOptions): Required<Pick<SqlLanguage, "importType" | "createTable">>;
@@ -0,0 +1,64 @@
1
+ const NULL = "NULL";
2
+ const INDENT = " ";
3
+ const DROP_KEYWORDS = {
4
+ table: "DROP TABLE",
5
+ view: "DROP VIEW",
6
+ materializedView: "DROP MATERIALIZED VIEW",
7
+ };
8
+ /** The completer's rule, so a generated statement spells a name as completion inserts it. */
9
+ export function identifierWriter(options) {
10
+ const reserved = new Set(options.keywords.map((keyword) => keyword.toUpperCase()));
11
+ return (identifier) => options.plainIdentifier.test(identifier) && !reserved.has(identifier.toUpperCase())
12
+ ? identifier
13
+ : options.quoteIdentifier(identifier);
14
+ }
15
+ /** DROP, SELECT, INSERT and UPDATE for a catalog relation, which every built-in plugin spreads into its `SqlLanguage`. */
16
+ export function sqlGenerator(options) {
17
+ const name = identifierWriter(options);
18
+ function list(columns) {
19
+ return columns.map((column) => name(column.name)).join(", ");
20
+ }
21
+ function insertValue(column) {
22
+ return column.default === null ? NULL : options.updateDefault(column.default);
23
+ }
24
+ function update(relation, qualifiedName) {
25
+ const keyNames = new Set(relation.primaryKey?.columns ?? []);
26
+ const keyed = keyNames.size > 0;
27
+ const unkeyed = relation.columns.filter((column) => !keyNames.has(column.name));
28
+ // A table that is all key still needs a SET, so the key columns fill it.
29
+ const set = unkeyed.length > 0 ? unkeyed : relation.columns;
30
+ const where = keyed ? relation.columns.filter((column) => keyNames.has(column.name)) : relation.columns;
31
+ return [
32
+ `UPDATE ${qualifiedName}`,
33
+ `SET ${set.map((column) => `${name(column.name)} = ${NULL}`).join(`,\n${INDENT}${INDENT}`)}`,
34
+ `WHERE ${where.map((column) => `${name(column.name)} = ${NULL}`).join(`\n${INDENT}AND `)};`,
35
+ ].join("\n");
36
+ }
37
+ return {
38
+ generate(statement, relation, qualifiedName) {
39
+ switch (statement) {
40
+ case "drop":
41
+ return `${DROP_KEYWORDS[relation.kind]} ${qualifiedName};`;
42
+ case "select":
43
+ return `SELECT ${list(relation.columns)}\nFROM ${qualifiedName};`;
44
+ case "insert": {
45
+ const assigned = relation.columns.filter((column) => !column.autoIncrement);
46
+ const columns = assigned.length > 0 ? assigned : relation.columns;
47
+ return `INSERT INTO ${qualifiedName} (${list(columns)})\nVALUES (${columns.map(insertValue).join(", ")});`;
48
+ }
49
+ case "update":
50
+ return update(relation, qualifiedName);
51
+ }
52
+ },
53
+ };
54
+ }
55
+ /** `importType` and `createTable` for a plugin that declares `bulkInsert` (decision 046). */
56
+ export function sqlImporter(options) {
57
+ const name = identifierWriter(options);
58
+ return {
59
+ importType: (column) => (column.kind === "decimal" ? options.decimal(column) : options.types[column.kind]),
60
+ createTable: (qualifiedName, columns) => `CREATE TABLE ${qualifiedName} (\n${columns
61
+ .map((column) => `${INDENT}${name(column.name)} ${column.type}`)
62
+ .join(",\n")}\n);`,
63
+ };
64
+ }
@@ -0,0 +1,16 @@
1
+ export { SQL_KEYWORDS, sqlCompleter, type CompleterOptions, type IdentifierFolding } from "./complete.js";
2
+ export { sqlInspector, type InspectorOptions } from "./inspect.js";
3
+ export { FUNCTION_CATEGORIES, functionInfo, functionItem, functionSpecs, isAvailable, parseSignature, signatureLabel, SQL_FUNCTIONS, type FunctionCategory, type FunctionEntry, type FunctionSpec, type ServerIdentity, type ServerRange, } from "./functions.js";
4
+ export { callAt, type CallAt } from "./signature.js";
5
+ export { classifyTokens, EXPLAIN_ANALYZE_REASON_PREFIX, transactionEffect, type TransactionEffect, } from "./classify.js";
6
+ export { FORMAT_DIALECTS, sqlFormatter, type FormatDialect, type FormatterOptions } from "./format.js";
7
+ export { sqlParameters, type NativePlaceholder, type ParameterOptions } from "./parameters.js";
8
+ export { sqlSplitter, sqlStatementStream } from "./splitter.js";
9
+ export { parseSimpleSelect, SELECT_READ_ONLY_REASONS, type SimpleSelect, type SimpleSelectItem } from "./select.js";
10
+ export { namespaceSlotAt, namespaceSwitcher, namespaceTarget, type NamespaceSlot, type NamespaceTarget, } from "./namespace.js";
11
+ export { type BatchSeparator, type NamespaceStatementForm, type SqlDialectOptions } from "./tokens.js";
12
+ export { defaultKeyword, defaultValuesInsert, quoteIdentifierWith, sqlSavepoints, sqlLiteral, type IdentifierQuote, type SqlLiteralOptions, } from "./literals.js";
13
+ export { identifierWriter, sqlGenerator, sqlImporter, type GeneratorOptions, type ImporterOptions, } from "./generate.js";
14
+ export { columnChanged, ddlScript, designError, NOTHING_TO_CHANGE, REFERENTIAL_ACTIONS, referencesClause, tableDesignOf, tableDifference, unchanged, validateDesign, type TableDifference, } from "./objects.js";
15
+ export { ALREADY_EXPLAIN, commonExplainRefusal, CREATE_TABLE_AS, EXPLAIN_ONE_STATEMENT, explainedVerb, NOTHING_TO_EXPLAIN, planPropertyValue, planText, sqlExplainable, type ExplainableOptions, } from "./explain.js";
16
+ export { routineSlots, type RoutineSlot, type RoutineSlots } from "./routines.js";
@@ -0,0 +1,16 @@
1
+ export { SQL_KEYWORDS, sqlCompleter } from "./complete.js";
2
+ export { sqlInspector } from "./inspect.js";
3
+ export { FUNCTION_CATEGORIES, functionInfo, functionItem, functionSpecs, isAvailable, parseSignature, signatureLabel, SQL_FUNCTIONS, } from "./functions.js";
4
+ export { callAt } from "./signature.js";
5
+ export { classifyTokens, EXPLAIN_ANALYZE_REASON_PREFIX, transactionEffect, } from "./classify.js";
6
+ export { FORMAT_DIALECTS, sqlFormatter } from "./format.js";
7
+ export { sqlParameters } from "./parameters.js";
8
+ export { sqlSplitter, sqlStatementStream } from "./splitter.js";
9
+ export { parseSimpleSelect, SELECT_READ_ONLY_REASONS } from "./select.js";
10
+ export { namespaceSlotAt, namespaceSwitcher, namespaceTarget, } from "./namespace.js";
11
+ export {} from "./tokens.js";
12
+ export { defaultKeyword, defaultValuesInsert, quoteIdentifierWith, sqlSavepoints, sqlLiteral, } from "./literals.js";
13
+ export { identifierWriter, sqlGenerator, sqlImporter, } from "./generate.js";
14
+ export { columnChanged, ddlScript, designError, NOTHING_TO_CHANGE, REFERENTIAL_ACTIONS, referencesClause, tableDesignOf, tableDifference, unchanged, validateDesign, } from "./objects.js";
15
+ export { ALREADY_EXPLAIN, commonExplainRefusal, CREATE_TABLE_AS, EXPLAIN_ONE_STATEMENT, explainedVerb, NOTHING_TO_EXPLAIN, planPropertyValue, planText, sqlExplainable, } from "./explain.js";
16
+ export { routineSlots } from "./routines.js";
@@ -0,0 +1,10 @@
1
+ import type { SqlLanguage } from "@tablecraft-app/plugin-api";
2
+ import { type InspectorOptions } from "./inspectContext.js";
3
+ export type { InspectorOptions } from "./inspectContext.js";
4
+ /**
5
+ * The shared inspector every built-in plugin spreads into its `SqlLanguage` (plan 108): it reads
6
+ * each statement with the completer's lexer, scope and catalog lookups and reports what DataGrip's
7
+ * inspections of the same ids would, with their quick fixes. It never rejects: a statement or an
8
+ * inspection that throws only reports less.
9
+ */
10
+ export declare function sqlInspector(options: InspectorOptions): Required<Pick<SqlLanguage, "inspect">>;
@@ -0,0 +1,134 @@
1
+ import { StatementView } from "./inspectContext.js";
2
+ import { reservedProblems, resolveProblems } from "./inspectResolve.js";
3
+ import { STYLE_INSPECTIONS } from "./inspectStyle.js";
4
+ import { syntaxProblems, unterminatedProblem } from "./inspectSyntax.js";
5
+ import { namespaceTarget } from "./namespace.js";
6
+ import { nameRules } from "./resolution.js";
7
+ import { sqlSplitter } from "./splitter.js";
8
+ import { Lexer } from "./tokens.js";
9
+ /** DataGrip's suppression comment: `-- noinspection SqlResolve,SqlSyntax` on the line above a statement. */
10
+ const NOINSPECTION = /^(?:--|#)\s*noinspection\s+([\w\s,]+)$/i;
11
+ const ID_SEPARATOR = /[\s,]+/;
12
+ const NEWLINE = "\n";
13
+ const INSPECTIONS = [
14
+ syntaxProblems,
15
+ resolveProblems,
16
+ reservedProblems,
17
+ ...STYLE_INSPECTIONS,
18
+ ];
19
+ function lex(text, options) {
20
+ const lexer = new Lexer(text, options.dialect);
21
+ const tokens = [];
22
+ for (let token = lexer.next(); token !== null; token = lexer.next())
23
+ tokens.push(token);
24
+ return tokens;
25
+ }
26
+ function newlines(text) {
27
+ let count = 0;
28
+ for (let at = text.indexOf(NEWLINE); at >= 0; at = text.indexOf(NEWLINE, at + 1))
29
+ count += 1;
30
+ return count;
31
+ }
32
+ /** The inspections the comments directly above a statement silence; a blank line ends the run. */
33
+ function suppressedBy(text, comments, statementFrom) {
34
+ const ids = new Set();
35
+ let reach = statementFrom;
36
+ for (let at = comments.length - 1; at >= 0; at -= 1) {
37
+ const comment = comments[at];
38
+ if (comment === undefined || newlines(text.slice(comment.to, reach)) > 1)
39
+ break;
40
+ const match = NOINSPECTION.exec(text.slice(comment.from, comment.to).trim());
41
+ for (const id of match?.[1]?.split(ID_SEPARATOR) ?? [])
42
+ if (id !== "")
43
+ ids.add(id);
44
+ reach = comment.from;
45
+ }
46
+ return ids;
47
+ }
48
+ /**
49
+ * The shared inspector every built-in plugin spreads into its `SqlLanguage` (plan 108): it reads
50
+ * each statement with the completer's lexer, scope and catalog lookups and reports what DataGrip's
51
+ * inspections of the same ids would, with their quick fixes. It never rejects: a statement or an
52
+ * inspection that throws only reports less.
53
+ */
54
+ export function sqlInspector(options) {
55
+ const rules = nameRules(options);
56
+ const split = sqlSplitter(options.dialect);
57
+ async function inspectStatement(text, tokens, lookup) {
58
+ const view = new StatementView(text, tokens, options, rules, lookup);
59
+ const problems = [];
60
+ for (const inspection of INSPECTIONS) {
61
+ try {
62
+ problems.push(...(await inspection(view)));
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ }
68
+ return problems;
69
+ }
70
+ async function follow(span, lookup) {
71
+ const target = namespaceTarget(span.sql, options.dialect);
72
+ if (target !== null)
73
+ await lookup.switchTo(target);
74
+ }
75
+ async function inspectAll(request, catalog) {
76
+ const { text } = request;
77
+ const disabled = new Set(request.disabled ?? []);
78
+ const tokens = lex(text, options);
79
+ const lookup = new rules.Lookup(catalog);
80
+ const problems = [];
81
+ const suppressions = [];
82
+ let cursor = 0;
83
+ for (const span of split(text)) {
84
+ const comments = [];
85
+ while (cursor < tokens.length && (tokens[cursor]?.from ?? 0) < span.from) {
86
+ const token = tokens[cursor];
87
+ if (token?.kind === "comment")
88
+ comments.push(token);
89
+ cursor += 1;
90
+ }
91
+ const own = [];
92
+ while (cursor < tokens.length && (tokens[cursor]?.from ?? 0) < span.to) {
93
+ const token = tokens[cursor];
94
+ if (token !== undefined && token.kind !== "comment" && token.kind !== "delimiter")
95
+ own.push(token);
96
+ cursor += 1;
97
+ }
98
+ const ids = suppressedBy(text, comments, span.from);
99
+ suppressions.push({ span, ids });
100
+ try {
101
+ const found = await inspectStatement(text, own, lookup);
102
+ problems.push(...found.filter((entry) => !ids.has(entry.inspection)));
103
+ }
104
+ catch {
105
+ // A statement the reader cannot follow reports nothing; the others still do.
106
+ }
107
+ try {
108
+ await follow(span, lookup);
109
+ }
110
+ catch {
111
+ // The namespace stays where it was.
112
+ }
113
+ }
114
+ const unterminated = unterminatedProblem(text, tokens.at(-1), options.dialect);
115
+ if (unterminated !== null) {
116
+ const owner = suppressions.find(({ span }) => span.from <= unterminated.from && unterminated.from < span.to);
117
+ if (owner === undefined || !owner.ids.has(unterminated.inspection))
118
+ problems.push(unterminated);
119
+ }
120
+ return problems
121
+ .filter((entry) => !disabled.has(entry.inspection))
122
+ .sort((left, right) => left.from - right.from || left.to - right.to);
123
+ }
124
+ return {
125
+ inspect: async (request, catalog) => {
126
+ try {
127
+ return await inspectAll(request, catalog);
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ },
133
+ };
134
+ }