@tablecraft-app/plugin-api 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/catalog.d.ts +112 -0
  4. package/dist/catalog.js +5 -0
  5. package/dist/completion.d.ts +109 -0
  6. package/dist/completion.js +12 -0
  7. package/dist/errors.d.ts +58 -0
  8. package/dist/errors.js +106 -0
  9. package/dist/explain.d.ts +58 -0
  10. package/dist/explain.js +1 -0
  11. package/dist/index.d.ts +15 -0
  12. package/dist/index.js +15 -0
  13. package/dist/inspection.d.ts +38 -0
  14. package/dist/inspection.js +21 -0
  15. package/dist/manifest.d.ts +186 -0
  16. package/dist/manifest.js +176 -0
  17. package/dist/objects.d.ts +140 -0
  18. package/dist/objects.js +2 -0
  19. package/dist/plugin.d.ts +460 -0
  20. package/dist/plugin.js +49 -0
  21. package/dist/sessions.d.ts +59 -0
  22. package/dist/sessions.js +2 -0
  23. package/dist/sql/classify.d.ts +19 -0
  24. package/dist/sql/classify.js +95 -0
  25. package/dist/sql/complete.d.ts +23 -0
  26. package/dist/sql/complete.js +480 -0
  27. package/dist/sql/explain.d.ts +42 -0
  28. package/dist/sql/explain.js +108 -0
  29. package/dist/sql/format.d.ts +26 -0
  30. package/dist/sql/format.js +117 -0
  31. package/dist/sql/functions.d.ts +47 -0
  32. package/dist/sql/functions.js +159 -0
  33. package/dist/sql/generate.d.ts +22 -0
  34. package/dist/sql/generate.js +64 -0
  35. package/dist/sql/index.d.ts +16 -0
  36. package/dist/sql/index.js +16 -0
  37. package/dist/sql/inspect.d.ts +10 -0
  38. package/dist/sql/inspect.js +134 -0
  39. package/dist/sql/inspectContext.d.ts +78 -0
  40. package/dist/sql/inspectContext.js +251 -0
  41. package/dist/sql/inspectResolve.d.ts +6 -0
  42. package/dist/sql/inspectResolve.js +391 -0
  43. package/dist/sql/inspectStyle.d.ts +3 -0
  44. package/dist/sql/inspectStyle.js +384 -0
  45. package/dist/sql/inspectSyntax.d.ts +10 -0
  46. package/dist/sql/inspectSyntax.js +179 -0
  47. package/dist/sql/layout.d.ts +8 -0
  48. package/dist/sql/layout.js +114 -0
  49. package/dist/sql/layoutNodes.d.ts +46 -0
  50. package/dist/sql/layoutNodes.js +317 -0
  51. package/dist/sql/layoutPrint.d.ts +60 -0
  52. package/dist/sql/layoutPrint.js +806 -0
  53. package/dist/sql/layoutWriter.d.ts +21 -0
  54. package/dist/sql/layoutWriter.js +77 -0
  55. package/dist/sql/literals.d.ts +28 -0
  56. package/dist/sql/literals.js +78 -0
  57. package/dist/sql/namespace.d.ts +41 -0
  58. package/dist/sql/namespace.js +220 -0
  59. package/dist/sql/objects.d.ts +42 -0
  60. package/dist/sql/objects.js +222 -0
  61. package/dist/sql/parameters.d.ts +20 -0
  62. package/dist/sql/parameters.js +88 -0
  63. package/dist/sql/resolution.d.ts +73 -0
  64. package/dist/sql/resolution.js +192 -0
  65. package/dist/sql/routines.d.ts +27 -0
  66. package/dist/sql/routines.js +42 -0
  67. package/dist/sql/scope.d.ts +117 -0
  68. package/dist/sql/scope.js +540 -0
  69. package/dist/sql/select.d.ts +41 -0
  70. package/dist/sql/select.js +287 -0
  71. package/dist/sql/signature.d.ts +18 -0
  72. package/dist/sql/signature.js +137 -0
  73. package/dist/sql/splitter.d.ts +15 -0
  74. package/dist/sql/splitter.js +217 -0
  75. package/dist/sql/suggest.d.ts +6 -0
  76. package/dist/sql/suggest.js +36 -0
  77. package/dist/sql/tokens.d.ts +62 -0
  78. package/dist/sql/tokens.js +227 -0
  79. package/dist/sql/words.d.ts +23 -0
  80. package/dist/sql/words.js +72 -0
  81. package/dist/testing/index.d.ts +177 -0
  82. package/dist/testing/index.js +2070 -0
  83. package/dist/tls.d.ts +33 -0
  84. package/dist/tls.js +118 -0
  85. package/dist/url.d.ts +52 -0
  86. package/dist/url.js +219 -0
  87. package/dist/values.d.ts +44 -0
  88. package/dist/values.js +44 -0
  89. package/dist/version.d.ts +22 -0
  90. package/dist/version.js +29 -0
  91. package/package.json +42 -4
@@ -0,0 +1,108 @@
1
+ import { classifyTokens } from "./classify.js";
2
+ import { sqlSplitter } from "./splitter.js";
3
+ import { significantTokens } from "./tokens.js";
4
+ import { EXPLAIN_VERBS, mainVerb, words } from "./words.js";
5
+ export const NOTHING_TO_EXPLAIN = "There is no statement to explain; put the caret in one.";
6
+ export const EXPLAIN_ONE_STATEMENT = "Explain one statement at a time.";
7
+ export const ALREADY_EXPLAIN = "This statement is already an EXPLAIN; run it with Run to see its rows.";
8
+ const CREATE = "CREATE";
9
+ const TABLE = "TABLE";
10
+ const AS = "AS";
11
+ /** How `ExplainableOptions` names `CREATE TABLE … AS`, which `CREATE TABLE` alone is not. */
12
+ export const CREATE_TABLE_AS = "CREATE TABLE … AS";
13
+ function anyOf(items) {
14
+ if (items.length <= 1)
15
+ return items.join("");
16
+ return `${items.slice(0, -1).join(", ")} or ${items.at(-1) ?? ""}`;
17
+ }
18
+ function isCreateTableAs(list) {
19
+ if (list[0]?.word !== CREATE)
20
+ return false;
21
+ const table = list.findIndex((entry) => entry.depth === 0 && entry.word === TABLE);
22
+ return table > 0 && list.some((entry, at) => at > table && entry.depth === 0 && entry.word === AS);
23
+ }
24
+ /**
25
+ * The refusals every dialect shares: nothing to explain, more than one statement, and a statement
26
+ * that already is an EXPLAIN. Null when none applies.
27
+ */
28
+ export function commonExplainRefusal(sql, dialect) {
29
+ const spans = sqlSplitter(dialect)(sql);
30
+ if (spans.length === 0)
31
+ return NOTHING_TO_EXPLAIN;
32
+ if (spans.length > 1)
33
+ return EXPLAIN_ONE_STATEMENT;
34
+ const first = words(sql, significantTokens(sql, dialect))[0]?.word;
35
+ return first !== undefined && EXPLAIN_VERBS.has(first) ? ALREADY_EXPLAIN : null;
36
+ }
37
+ /**
38
+ * The verb that decides what `sql` does, upper case: `SELECT`, the verb after `WITH …`, or
39
+ * `CREATE_TABLE_AS`; empty for no statement. A plugin picks its EXPLAIN format by it.
40
+ */
41
+ export function explainedVerb(sql, dialect) {
42
+ const list = words(sql, significantTokens(sql, dialect));
43
+ return isCreateTableAs(list) ? CREATE_TABLE_AS : (list[mainVerb(list)]?.word ?? "");
44
+ }
45
+ /**
46
+ * `SqlLanguage.explainable` from a dialect's verb lists: the shared refusals, then the statement's
47
+ * main verb against what the dialect explains and analyses. A statement writes when it is not a read.
48
+ */
49
+ export function sqlExplainable(options) {
50
+ const verbs = new Set(options.verbs);
51
+ const analyzeVerbs = new Set(options.analyzeVerbs);
52
+ const explainable = (sql, { analyze }) => {
53
+ const common = commonExplainRefusal(sql, options.dialect);
54
+ if (common !== null)
55
+ return { ok: false, reason: common };
56
+ const statement = sqlSplitter(options.dialect)(sql)[0]?.sql ?? sql;
57
+ const verb = explainedVerb(statement, options.dialect);
58
+ if (!verbs.has(verb)) {
59
+ return {
60
+ ok: false,
61
+ reason: `${options.name} cannot explain a ${verb} statement. Explain ${anyOf(options.verbs)} instead.`,
62
+ };
63
+ }
64
+ if (analyze && !analyzeVerbs.has(verb)) {
65
+ return {
66
+ ok: false,
67
+ reason: analyzeVerbs.size === 0
68
+ ? `${options.name} cannot run Explain Analyse; use Explain Plan.`
69
+ : `${options.name} analyses only ${anyOf(options.analyzeVerbs)} statements; use Explain Plan for this ${verb}.`,
70
+ };
71
+ }
72
+ return { ok: true, writes: classifyTokens(statement, options.dialect).kind !== "read" };
73
+ };
74
+ return { explainable };
75
+ }
76
+ const LINE_BREAK = "\n";
77
+ /** The first cell of every row as text, one row per line: what an EXPLAIN printed. */
78
+ export function planText(rows) {
79
+ return rows
80
+ .map((row) => {
81
+ const cell = row[0];
82
+ if (cell === null || cell === undefined)
83
+ return "";
84
+ if (typeof cell === "object")
85
+ return cell.kind === "json" ? cell.text : JSON.stringify(cell);
86
+ return String(cell);
87
+ })
88
+ .join(LINE_BREAK);
89
+ }
90
+ /**
91
+ * A plan property's value as one line of text: strings as they are, lists joined with commas,
92
+ * anything nested as JSON. Null for nothing worth showing: null, or an empty list.
93
+ */
94
+ export function planPropertyValue(value) {
95
+ if (value === null || value === undefined)
96
+ return null;
97
+ if (typeof value === "string")
98
+ return value;
99
+ if (typeof value === "number" || typeof value === "boolean")
100
+ return String(value);
101
+ if (Array.isArray(value)) {
102
+ if (value.length === 0)
103
+ return null;
104
+ const flat = value.every((item) => item === null || typeof item !== "object");
105
+ return flat ? value.map((item) => String(item)).join(", ") : JSON.stringify(value);
106
+ }
107
+ return JSON.stringify(value);
108
+ }
@@ -0,0 +1,26 @@
1
+ import type { ParameterStyle, SqlLanguage } from "@tablecraft-app/plugin-api";
2
+ import { type DialectOptions } from "sql-formatter";
3
+ import { type SqlDialectOptions } from "./tokens.js";
4
+ /** The `sql-formatter` dialects plugins may name (decisions 042 and 100). */
5
+ export declare const FORMAT_DIALECTS: {
6
+ readonly postgresql: DialectOptions;
7
+ readonly mysql: DialectOptions;
8
+ readonly sqlite: DialectOptions;
9
+ readonly plsql: DialectOptions;
10
+ readonly transactsql: DialectOptions;
11
+ };
12
+ export type FormatDialect = keyof typeof FORMAT_DIALECTS;
13
+ export interface FormatterOptions {
14
+ readonly dialect: FormatDialect;
15
+ /** The same dialect options the splitter gets; `delimiterCommand` makes a `DELIMITER` line refuse. */
16
+ readonly lexer: SqlDialectOptions;
17
+ /** The placeholders `SqlLanguage.parameters` reads, so the formatter keeps `:id` and `$1` whole. */
18
+ readonly parameters: readonly ParameterStyle[];
19
+ }
20
+ /**
21
+ * `SqlLanguage.format`: the aligned layout (decision 061) or `sql-formatter`'s (decision 042), which
22
+ * also takes every statement the aligned one does not read (decision 062). Throws an `Error` with a
23
+ * one-line reason rather than returning half-formatted text; comments and literals pass through as
24
+ * written.
25
+ */
26
+ export declare function sqlFormatter(options: FormatterOptions): Pick<SqlLanguage, "format">;
@@ -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">>;