@tablecraft-app/plugin-api 1.4.0 → 1.5.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/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +5 -0
- package/dist/plugin.d.ts +8 -1
- package/dist/sql/complete.js +11 -4
- package/dist/sql/inspectResolve.js +4 -0
- package/dist/sql/namespace.d.ts +2 -1
- package/dist/sql/namespace.js +33 -3
- package/dist/sql/tokens.d.ts +5 -2
- package/dist/sql/tokens.js +6 -2
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.js +73 -39
- package/dist/url.d.ts +1 -0
- package/dist/url.js +88 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/manifest.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export declare const connectionUrlSchema: z.ZodObject<{
|
|
|
74
74
|
values: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
75
75
|
}, z.core.$strict>>>;
|
|
76
76
|
sets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>>;
|
|
77
|
+
semicolonParams: z.ZodOptional<z.ZodBoolean>;
|
|
77
78
|
}, z.core.$strict>;
|
|
78
79
|
export type ConnectionUrl = z.infer<typeof connectionUrlSchema>;
|
|
79
80
|
/**
|
|
@@ -181,6 +182,7 @@ export declare const pluginManifestSchema: z.ZodObject<{
|
|
|
181
182
|
values: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
182
183
|
}, z.core.$strict>>>;
|
|
183
184
|
sets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>>;
|
|
185
|
+
semicolonParams: z.ZodOptional<z.ZodBoolean>;
|
|
184
186
|
}, z.core.$strict>>;
|
|
185
187
|
network: z.ZodOptional<z.ZodObject<{
|
|
186
188
|
host: z.ZodString;
|
package/dist/manifest.js
CHANGED
|
@@ -68,6 +68,11 @@ export const connectionUrlSchema = z.strictObject({
|
|
|
68
68
|
sets: z
|
|
69
69
|
.record(z.string().regex(URL_SCHEME), z.record(z.string().regex(FIELD_KEY), z.union([z.string(), z.number(), z.boolean()])))
|
|
70
70
|
.optional(),
|
|
71
|
+
/**
|
|
72
|
+
* The JDBC SQL Server form, `scheme://host[\instance][:port][;name=value…]` (plan 055): parameters
|
|
73
|
+
* follow `;`, a `{…}` value may hold one, and `\instance` is the parameter `instanceName`.
|
|
74
|
+
*/
|
|
75
|
+
semicolonParams: z.boolean().optional(),
|
|
71
76
|
});
|
|
72
77
|
/**
|
|
73
78
|
* Which fields hold the server's address (decision 070), so the host can open an SSH tunnel and
|
package/dist/plugin.d.ts
CHANGED
|
@@ -122,6 +122,12 @@ export interface ExecuteOptions {
|
|
|
122
122
|
* leads each row with a `node` column, and anything else is a `ServerNode.id`.
|
|
123
123
|
*/
|
|
124
124
|
readonly node?: string;
|
|
125
|
+
/**
|
|
126
|
+
* `StatementSpan.repeat` of the statement, at least 2, when the script asks for it to run more
|
|
127
|
+
* than once (SQL Server's `GO 3`, decision 100). A plugin that splits such scripts runs it that
|
|
128
|
+
* many times and answers with the last run; any other plugin never receives it.
|
|
129
|
+
*/
|
|
130
|
+
readonly repeat?: number;
|
|
125
131
|
}
|
|
126
132
|
/** `ExecuteOptions.node`: the plugin routes the command by its keys (decision 114). */
|
|
127
133
|
export declare const NODE_AUTO = "auto";
|
|
@@ -311,7 +317,8 @@ export interface StatementSpan {
|
|
|
311
317
|
readonly sql: string;
|
|
312
318
|
/**
|
|
313
319
|
* How many times the script asks for the statement to run: SQL Server's `GO 3` after it
|
|
314
|
-
* (decision 100). Absent means once. The host never repeats a statement;
|
|
320
|
+
* (decision 100). Absent means once. The host never repeats a statement; it passes the count
|
|
321
|
+
* as `ExecuteOptions.repeat` and the plugin runs it that many times.
|
|
315
322
|
*/
|
|
316
323
|
readonly repeat?: number;
|
|
317
324
|
}
|
package/dist/sql/complete.js
CHANGED
|
@@ -70,6 +70,8 @@ const BOOST = {
|
|
|
70
70
|
keyword: -10,
|
|
71
71
|
};
|
|
72
72
|
const UNQUOTED_WORD = "^[A-Za-z0-9_$]*$";
|
|
73
|
+
/** Where `@` and `#` start a name (`variables`), the typed `@@` stays part of what is filtered. */
|
|
74
|
+
const VARIABLE_WORD = "^[@#]*[A-Za-z0-9_$]*$";
|
|
73
75
|
const ALIAS_WORD_BREAK = /[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])/;
|
|
74
76
|
const FALLBACK_ALIAS = "t";
|
|
75
77
|
const QUOTES = new Set(['"', "`", "["]);
|
|
@@ -126,6 +128,7 @@ function relationKind(relation) {
|
|
|
126
128
|
/** The shared completion and navigation every built-in plugin spreads into its `SqlLanguage` (decision 028). */
|
|
127
129
|
export function sqlCompleter(options) {
|
|
128
130
|
const reserved = new Set(options.keywords.map((keyword) => keyword.toUpperCase()));
|
|
131
|
+
const unquotedWord = options.dialect.variables === true ? VARIABLE_WORD : UNQUOTED_WORD;
|
|
129
132
|
const { denotes, sameIdentifier, cteNamed, sourceNamed, serverOf, Lookup } = nameRules(options);
|
|
130
133
|
const specs = new Map();
|
|
131
134
|
for (const spec of options.functions) {
|
|
@@ -224,7 +227,9 @@ export function sqlCompleter(options) {
|
|
|
224
227
|
.filter((word) => word !== "")
|
|
225
228
|
.map((word) => word.charAt(0).toLowerCase())
|
|
226
229
|
.join("");
|
|
227
|
-
|
|
230
|
+
// Unquoted, an alias folds like any name; `c` is written as is and reads back as `C` under upper folding.
|
|
231
|
+
const folded = options.identifierFolding === "upper" ? initials.toUpperCase() : initials;
|
|
232
|
+
const base = initials === "" || !options.plainIdentifier.test(folded) ? FALLBACK_ALIAS : initials;
|
|
228
233
|
let alias = base;
|
|
229
234
|
for (let suffix = 1; taken.has(alias) || reserved.has(alias.toUpperCase()); suffix += 1)
|
|
230
235
|
alias = `${base}${String(suffix)}`;
|
|
@@ -258,7 +263,9 @@ export function sqlCompleter(options) {
|
|
|
258
263
|
continue;
|
|
259
264
|
const near = source.alias === null
|
|
260
265
|
? relationReference(found.path, found.relation.name, lookup.current)
|
|
261
|
-
:
|
|
266
|
+
: source.alias.quoted
|
|
267
|
+
? options.quoteIdentifier(source.alias.name)
|
|
268
|
+
: source.alias.name;
|
|
262
269
|
for (const key of found.relation.foreignKeys) {
|
|
263
270
|
const alias = aliasFor(key.target.relation, new Set(taken));
|
|
264
271
|
const on = key.columns
|
|
@@ -376,7 +383,7 @@ export function sqlCompleter(options) {
|
|
|
376
383
|
const lookup = new Lookup(catalog);
|
|
377
384
|
await lookup.follow(request.text, request.offset);
|
|
378
385
|
const items = await namespaceItems(slot, lookup);
|
|
379
|
-
return { from: slot.from, to: slot.to, items, validFor: slot.quote === null ?
|
|
386
|
+
return { from: slot.from, to: slot.to, items, validFor: slot.quote === null ? unquotedWord : null };
|
|
380
387
|
}
|
|
381
388
|
}
|
|
382
389
|
const { text, offset, shift } = wrap(request);
|
|
@@ -390,7 +397,7 @@ export function sqlCompleter(options) {
|
|
|
390
397
|
from: scope.prefixFrom - shift,
|
|
391
398
|
to: offset - shift,
|
|
392
399
|
items: typedQuote ? quotedLabels(items) : items,
|
|
393
|
-
validFor: typedQuote ? null :
|
|
400
|
+
validFor: typedQuote ? null : unquotedWord,
|
|
394
401
|
};
|
|
395
402
|
}
|
|
396
403
|
function sourceDeclaring(query, word) {
|
|
@@ -28,6 +28,7 @@ const COLUMN_CLAUSES = new Set([
|
|
|
28
28
|
/** Symbols right before a word that make it a parameter or variable: `:name`, `@var`, `x::type`. */
|
|
29
29
|
const PARAMETER_MARKS = new Set([":", "@", "?"]);
|
|
30
30
|
const PARAMETER_START = /^[$@:]/;
|
|
31
|
+
const PARAMETER_OR_TEMPORARY = /^[@#]/;
|
|
31
32
|
/** Rows a statement may name without declaring them: PostgreSQL's `EXCLUDED`, a trigger's `NEW` and `OLD`. */
|
|
32
33
|
const PSEUDO_RELATIONS = new Set(["EXCLUDED", "NEW", "OLD"]);
|
|
33
34
|
const RELATION_INTRODUCERS = new Set(["FROM", "JOIN", "UPDATE", "INTO", "STRAIGHT_JOIN"]);
|
|
@@ -146,6 +147,9 @@ async function unresolvedRelation(view, query, source) {
|
|
|
146
147
|
return [];
|
|
147
148
|
if (!source.name.quoted && NOT_RELATION_NAMES.has(source.name.name.toUpperCase()))
|
|
148
149
|
return [];
|
|
150
|
+
// SQL Server's `#temp` and `@table` live in no catalog the explorer reads.
|
|
151
|
+
if (!source.name.quoted && PARAMETER_OR_TEMPORARY.test(source.name.name))
|
|
152
|
+
return [];
|
|
149
153
|
const { lookup } = view;
|
|
150
154
|
let path = lookup.current;
|
|
151
155
|
if (source.qualifier.length > 0) {
|
package/dist/sql/namespace.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ export interface NamespaceSlot {
|
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
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, |`
|
|
26
|
+
* to: `USE |`, `ALTER SESSION SET CURRENT_SCHEMA = |` or the elements of `SET search_path TO a, |`
|
|
27
|
+
* and `SET SCHEMA |`. Null anywhere else,
|
|
27
28
|
* and after a name whose closing quote is typed.
|
|
28
29
|
*/
|
|
29
30
|
export declare function namespaceSlotAt(text: string, offset: number, dialect: SqlDialectOptions): NamespaceSlot | null;
|
package/dist/sql/namespace.js
CHANGED
|
@@ -82,6 +82,17 @@ function searchPathTarget(text, tokens) {
|
|
|
82
82
|
return null;
|
|
83
83
|
return { kind: "schema", candidates: names };
|
|
84
84
|
}
|
|
85
|
+
const ALTER_SESSION_PREFIX = ["ALTER", "SESSION", "SET", "CURRENT_SCHEMA", "="];
|
|
86
|
+
function alterSessionPrefix(text, tokens) {
|
|
87
|
+
return ALTER_SESSION_PREFIX.every((word, at) => wordAt(text, tokens, at) === word);
|
|
88
|
+
}
|
|
89
|
+
function alterSessionTarget(text, tokens) {
|
|
90
|
+
if (tokens.length !== ALTER_SESSION_PREFIX.length + 1 || !alterSessionPrefix(text, tokens))
|
|
91
|
+
return null;
|
|
92
|
+
const token = tokens[ALTER_SESSION_PREFIX.length];
|
|
93
|
+
const name = identifier(text, token);
|
|
94
|
+
return name === null || token?.kind === "string" ? null : { kind: "schema", candidates: [name] };
|
|
95
|
+
}
|
|
85
96
|
const NAME_TOKENS = new Set(["word", "quoted", "string"]);
|
|
86
97
|
/** The statement's tokens around `offset`, comments dropped; null when the caret is in a comment. */
|
|
87
98
|
function tokensAround(text, offset, dialect) {
|
|
@@ -136,7 +147,8 @@ function searchPathListStart(text, tokens) {
|
|
|
136
147
|
}
|
|
137
148
|
/**
|
|
138
149
|
* 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, |`
|
|
150
|
+
* to: `USE |`, `ALTER SESSION SET CURRENT_SCHEMA = |` or the elements of `SET search_path TO a, |`
|
|
151
|
+
* and `SET SCHEMA |`. Null anywhere else,
|
|
140
152
|
* and after a name whose closing quote is typed.
|
|
141
153
|
*/
|
|
142
154
|
export function namespaceSlotAt(text, offset, dialect) {
|
|
@@ -168,6 +180,13 @@ export function namespaceSlotAt(text, offset, dialect) {
|
|
|
168
180
|
continue;
|
|
169
181
|
return { kind: "database", from, to: offset, quote, listed: [] };
|
|
170
182
|
}
|
|
183
|
+
if (form === "alterSessionSchema") {
|
|
184
|
+
if (prefix?.kind === "string" || before.length !== ALTER_SESSION_PREFIX.length)
|
|
185
|
+
continue;
|
|
186
|
+
if (!alterSessionPrefix(text, before))
|
|
187
|
+
continue;
|
|
188
|
+
return { kind: "schema", from, to: offset, quote, listed: [] };
|
|
189
|
+
}
|
|
171
190
|
const start = searchPathListStart(text, before);
|
|
172
191
|
if (start === null)
|
|
173
192
|
continue;
|
|
@@ -190,15 +209,26 @@ export function namespaceTarget(sql, dialect) {
|
|
|
190
209
|
return null;
|
|
191
210
|
const tokens = significantTokens(sql, dialect).filter((token) => token.kind !== "delimiter");
|
|
192
211
|
for (const form of forms) {
|
|
193
|
-
const target = form
|
|
212
|
+
const target = TARGET_READERS[form](sql, tokens);
|
|
194
213
|
if (target !== null)
|
|
195
214
|
return target;
|
|
196
215
|
}
|
|
197
216
|
return null;
|
|
198
217
|
}
|
|
218
|
+
const TARGET_READERS = {
|
|
219
|
+
use: useTarget,
|
|
220
|
+
searchPath: searchPathTarget,
|
|
221
|
+
alterSessionSchema: alterSessionTarget,
|
|
222
|
+
};
|
|
199
223
|
const SWITCH_TARGETS = {
|
|
200
224
|
use: "topLevel",
|
|
201
225
|
searchPath: "schema",
|
|
226
|
+
alterSessionSchema: "schema",
|
|
227
|
+
};
|
|
228
|
+
const SWITCH_STATEMENTS = {
|
|
229
|
+
use: (quoted) => `USE ${quoted}`,
|
|
230
|
+
searchPath: (quoted) => `SET search_path TO ${quoted}`,
|
|
231
|
+
alterSessionSchema: (quoted) => `ALTER SESSION SET CURRENT_SCHEMA = ${quoted}`,
|
|
202
232
|
};
|
|
203
233
|
/**
|
|
204
234
|
* The console's namespace switch for a dialect moving its namespace by `form` (decision 060):
|
|
@@ -214,7 +244,7 @@ export function namespaceSwitcher(form, quoteIdentifier) {
|
|
|
214
244
|
if (name === undefined) {
|
|
215
245
|
throw new PluginError("statementFailed", "A namespace switch names no namespace; pick one from the list.");
|
|
216
246
|
}
|
|
217
|
-
return form
|
|
247
|
+
return SWITCH_STATEMENTS[form](quoteIdentifier(name));
|
|
218
248
|
},
|
|
219
249
|
};
|
|
220
250
|
}
|
package/dist/sql/tokens.d.ts
CHANGED
|
@@ -26,13 +26,16 @@ export interface SqlDialectOptions {
|
|
|
26
26
|
readonly qQuotes?: boolean;
|
|
27
27
|
/** `N'…'` national character strings. */
|
|
28
28
|
readonly nationalStrings?: boolean;
|
|
29
|
+
/** SQL Server `@local`, `@@global` and `#temp` names: `@` and `#` are part of a word. */
|
|
30
|
+
readonly variables?: boolean;
|
|
29
31
|
}
|
|
30
32
|
export type BatchSeparator = "go" | "slash";
|
|
31
33
|
/**
|
|
32
34
|
* `use` is MySQL's `USE db`; `searchPath` is PostgreSQL's `SET [SESSION | LOCAL] search_path`,
|
|
33
|
-
* `SET SCHEMA` and `RESET search_path` / `RESET ALL
|
|
35
|
+
* `SET SCHEMA` and `RESET search_path` / `RESET ALL`; `alterSessionSchema` is Oracle's
|
|
36
|
+
* `ALTER SESSION SET CURRENT_SCHEMA = name`.
|
|
34
37
|
*/
|
|
35
|
-
export type NamespaceStatementForm = "use" | "searchPath";
|
|
38
|
+
export type NamespaceStatementForm = "use" | "searchPath" | "alterSessionSchema";
|
|
36
39
|
export type TokenKind = "word" | "quoted" | "string" | "comment" | "symbol" | "delimiter";
|
|
37
40
|
export interface Token {
|
|
38
41
|
readonly kind: TokenKind;
|
package/dist/sql/tokens.js
CHANGED
|
@@ -16,6 +16,7 @@ const Q_QUOTE_PAIRS = new Map([
|
|
|
16
16
|
const STRING_QUOTE = "'";
|
|
17
17
|
const BRACKET_OPEN = "[";
|
|
18
18
|
const BRACKET_CLOSE = "]";
|
|
19
|
+
const VARIABLE_CHARS = new Set(["@", "#"]);
|
|
19
20
|
function isSpace(char) {
|
|
20
21
|
return char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f" || char === "\v";
|
|
21
22
|
}
|
|
@@ -70,13 +71,16 @@ export class Lexer {
|
|
|
70
71
|
else if (char === "$" && this.options.dollarQuotes === true && this.#dollarTag(at) !== null) {
|
|
71
72
|
token = this.#dollarQuoted(at, this.#dollarTag(at) ?? "");
|
|
72
73
|
}
|
|
73
|
-
else if (isWordChar(char))
|
|
74
|
+
else if (this.#isWordChar(char))
|
|
74
75
|
token = this.#word(at, delimiter);
|
|
75
76
|
else
|
|
76
77
|
token = { kind: "symbol", from: at, to: at + 1 };
|
|
77
78
|
this.position = token.to;
|
|
78
79
|
return token;
|
|
79
80
|
}
|
|
81
|
+
#isWordChar(char) {
|
|
82
|
+
return isWordChar(char) || (this.options.variables === true && VARIABLE_CHARS.has(char));
|
|
83
|
+
}
|
|
80
84
|
#lineComment(from) {
|
|
81
85
|
const end = this.text.indexOf(NEWLINE, from);
|
|
82
86
|
return { kind: "comment", from, to: end < 0 ? this.text.length : end };
|
|
@@ -160,7 +164,7 @@ export class Lexer {
|
|
|
160
164
|
const text = this.text;
|
|
161
165
|
const customDelimiter = delimiter !== DEFAULT_DELIMITER;
|
|
162
166
|
let at = from;
|
|
163
|
-
while (at < text.length && isWordChar(text.charAt(at))) {
|
|
167
|
+
while (at < text.length && this.#isWordChar(text.charAt(at))) {
|
|
164
168
|
if (customDelimiter && at > from && text.startsWith(delimiter, at))
|
|
165
169
|
break;
|
|
166
170
|
at += 1;
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -87,6 +87,16 @@ export interface ContractFixture {
|
|
|
87
87
|
readonly config: unknown;
|
|
88
88
|
readonly note: RegExp;
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* False where the dialect cannot interrupt another session's statement and keep it connected,
|
|
92
|
+
* as SQL Server's `KILL` cannot: every row's `cancel` is null and `cancelSession` rejects.
|
|
93
|
+
*/
|
|
94
|
+
readonly cancels?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* A session id no session has, for servers that hand a closed session's id to the next login
|
|
97
|
+
* (SQL Server): without it the test closes a session of its own and waits for its id to go.
|
|
98
|
+
*/
|
|
99
|
+
readonly missingId?: string;
|
|
90
100
|
};
|
|
91
101
|
/** Required when the plugin declares `routineCall`, which also needs `catalog`. */
|
|
92
102
|
readonly routines?: {
|
|
@@ -108,6 +118,8 @@ export interface ContractFixture {
|
|
|
108
118
|
* default. `LINE_ITEMS_TABLE` is quoted in every dialect and never folded.
|
|
109
119
|
*/
|
|
110
120
|
readonly foldName?: (name: string) => string;
|
|
121
|
+
/** The database stores an empty string as NULL, as Oracle does, so an empty text reads back null. */
|
|
122
|
+
readonly emptyTextIsNull?: boolean;
|
|
111
123
|
}
|
|
112
124
|
/** Dialect-specific text the suite runs; every member is optional (decision 100). */
|
|
113
125
|
export interface ContractSql {
|
|
@@ -128,6 +140,10 @@ export interface ContractSql {
|
|
|
128
140
|
};
|
|
129
141
|
/** Opens a transaction explicitly in auto-commit mode. */
|
|
130
142
|
readonly begin?: string;
|
|
143
|
+
/** A name only quoting makes legal, which the dialect accepts; Oracle refuses a `"` inside one. */
|
|
144
|
+
readonly awkwardIdentifier?: string;
|
|
145
|
+
/** What `ExplainedPlan.statement` starts with; `EXPLAIN` by default, `SET SHOWPLAN_XML ON` on SQL Server. */
|
|
146
|
+
readonly explainStatement?: RegExp;
|
|
131
147
|
}
|
|
132
148
|
/** A command and the result tab it opens: column names and rows as wire values. */
|
|
133
149
|
export interface CommandRows {
|
package/dist/testing/index.js
CHANGED
|
@@ -117,8 +117,11 @@ export function contractSql(fixture) {
|
|
|
117
117
|
temporaryTable: fixture.sql?.temporaryTable ??
|
|
118
118
|
((name, columns) => ({ create: `CREATE TEMPORARY TABLE ${name} (${columns})`, name })),
|
|
119
119
|
begin: fixture.sql?.begin ?? "BEGIN",
|
|
120
|
+
awkwardIdentifier: fixture.sql?.awkwardIdentifier ?? AWKWARD_IDENTIFIER,
|
|
121
|
+
explainStatement: fixture.sql?.explainStatement ?? DEFAULT_EXPLAIN_STATEMENT,
|
|
120
122
|
};
|
|
121
123
|
}
|
|
124
|
+
const DEFAULT_EXPLAIN_STATEMENT = /^EXPLAIN\b/i;
|
|
122
125
|
const ROWS_READ_BEFORE_RETURN = 3;
|
|
123
126
|
/** Longer than `BYTES_PREVIEW_LENGTH` and within every fixture's `payload` column (MySQL's is `VARBINARY(64)`). */
|
|
124
127
|
const LONG_BYTES_LENGTH = 48;
|
|
@@ -678,9 +681,9 @@ export function runPluginContract(plugin, fixture) {
|
|
|
678
681
|
});
|
|
679
682
|
it("givenAwkwardIdentifier_whenQuotedAndSelected_thenNameReadsBack", async () => {
|
|
680
683
|
await withSession(plugin, fixture, async (session) => {
|
|
681
|
-
const result = await session.execute(dialectSql.select(`1 AS ${language().quoteIdentifier(
|
|
684
|
+
const result = await session.execute(dialectSql.select(`1 AS ${language().quoteIdentifier(dialectSql.awkwardIdentifier)}`));
|
|
682
685
|
await collect(result.rows);
|
|
683
|
-
expect(result.columns.map((column) => column.name)).toEqual([
|
|
686
|
+
expect(result.columns.map((column) => column.name)).toEqual([dialectSql.awkwardIdentifier]);
|
|
684
687
|
});
|
|
685
688
|
});
|
|
686
689
|
it("givenTypeProbeRow_whenSqlInsertPerDialect_thenRoundTripsEqual", async () => {
|
|
@@ -861,7 +864,7 @@ export function runPluginContract(plugin, fixture) {
|
|
|
861
864
|
const texts = AWKWARD_TEXTS;
|
|
862
865
|
const rows = texts.map((text, index) => target.columns.map((column) => column.name === fold("id") ? String(index + 1) : column.name === fold("unicode_text") ? text : null));
|
|
863
866
|
await insertRowsOf(session)(target, rows);
|
|
864
|
-
expect(await run(session, `SELECT unicode_text FROM ${TYPE_PROBE_COPY_TABLE} ORDER BY id`)).toEqual(texts.map((text) => [text]));
|
|
867
|
+
expect(await run(session, `SELECT unicode_text FROM ${TYPE_PROBE_COPY_TABLE} ORDER BY id`)).toEqual(texts.map((text) => [text === "" && fixture.emptyTextIsNull === true ? null : text]));
|
|
865
868
|
});
|
|
866
869
|
});
|
|
867
870
|
it("givenCreateTable_whenEveryImportType_thenTableTakesSampleText", async () => {
|
|
@@ -904,7 +907,7 @@ export function runPluginContract(plugin, fixture) {
|
|
|
904
907
|
return session.introspector;
|
|
905
908
|
}
|
|
906
909
|
async function relationOf(session, name) {
|
|
907
|
-
return (await introspectorOf(session).describe(path())).relations.find((relation) => relation.name === name);
|
|
910
|
+
return (await introspectorOf(session).describe(path())).relations.find((relation) => relation.name === fold(name));
|
|
908
911
|
}
|
|
909
912
|
/** Runs a script as the engine does: prepare, the statements in one transaction where it says so, verify, finish. */
|
|
910
913
|
async function apply(session, script) {
|
|
@@ -948,14 +951,14 @@ export function runPluginContract(plugin, fixture) {
|
|
|
948
951
|
return editorOf().script(change, context);
|
|
949
952
|
}
|
|
950
953
|
function column(name, type, nullable = true) {
|
|
951
|
-
return { name, type, nullable, default: null, autoIncrement: false, comment: null, original: null };
|
|
954
|
+
return { name: fold(name), type, nullable, default: null, autoIncrement: false, comment: null, original: null };
|
|
952
955
|
}
|
|
953
956
|
function parentDesign() {
|
|
954
957
|
return {
|
|
955
|
-
name: OBJECT_PARENT_TABLE,
|
|
958
|
+
name: fold(OBJECT_PARENT_TABLE),
|
|
956
959
|
comment: null,
|
|
957
960
|
columns: [column("id", OBJECT_KEY_TYPE, false)],
|
|
958
|
-
primaryKey: ["id"],
|
|
961
|
+
primaryKey: [fold("id")],
|
|
959
962
|
primaryKeyName: null,
|
|
960
963
|
indexes: [],
|
|
961
964
|
foreignKeys: [],
|
|
@@ -964,7 +967,7 @@ export function runPluginContract(plugin, fixture) {
|
|
|
964
967
|
}
|
|
965
968
|
function childDesign() {
|
|
966
969
|
return {
|
|
967
|
-
name: OBJECT_CHILD_TABLE,
|
|
970
|
+
name: fold(OBJECT_CHILD_TABLE),
|
|
968
971
|
comment: null,
|
|
969
972
|
columns: [
|
|
970
973
|
column("id", OBJECT_KEY_TYPE, false),
|
|
@@ -972,14 +975,14 @@ export function runPluginContract(plugin, fixture) {
|
|
|
972
975
|
column("label", OBJECT_NARROW_TYPE),
|
|
973
976
|
column("obsolete", OBJECT_NARROW_TYPE),
|
|
974
977
|
],
|
|
975
|
-
primaryKey: ["id"],
|
|
978
|
+
primaryKey: [fold("id")],
|
|
976
979
|
primaryKeyName: null,
|
|
977
|
-
indexes: [{ name: OBJECT_INDEX, columns: ["label"], unique: false, original: null }],
|
|
980
|
+
indexes: [{ name: fold(OBJECT_INDEX), columns: [fold("label")], unique: false, original: null }],
|
|
978
981
|
foreignKeys: [
|
|
979
982
|
{
|
|
980
|
-
name: OBJECT_FOREIGN_KEY,
|
|
981
|
-
columns: ["parent_id"],
|
|
982
|
-
target: { path: path(), relation: OBJECT_PARENT_TABLE, columns: ["id"] },
|
|
983
|
+
name: fold(OBJECT_FOREIGN_KEY),
|
|
984
|
+
columns: [fold("parent_id")],
|
|
985
|
+
target: { path: path(), relation: fold(OBJECT_PARENT_TABLE), columns: [fold("id")] },
|
|
983
986
|
onDelete: "CASCADE",
|
|
984
987
|
onUpdate: null,
|
|
985
988
|
original: null,
|
|
@@ -1020,8 +1023,8 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1020
1023
|
...described,
|
|
1021
1024
|
columns: [
|
|
1022
1025
|
...described.columns
|
|
1023
|
-
.filter((entry) => entry.name !== "obsolete")
|
|
1024
|
-
.map((entry) => (entry.name === "label" ? { ...entry, type: OBJECT_WIDE_TYPE } : entry)),
|
|
1026
|
+
.filter((entry) => entry.name !== fold("obsolete"))
|
|
1027
|
+
.map((entry) => (entry.name === fold("label") ? { ...entry, type: OBJECT_WIDE_TYPE } : entry)),
|
|
1025
1028
|
column("note", OBJECT_NARROW_TYPE),
|
|
1026
1029
|
],
|
|
1027
1030
|
...extra,
|
|
@@ -1030,11 +1033,11 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1030
1033
|
it("givenObjectsDeclared_whenCreateTableWithKeyIndexAndForeignKey_thenDescribedAsDesigned", async () => {
|
|
1031
1034
|
await withTables(async (session) => {
|
|
1032
1035
|
const child = await relationOf(session, OBJECT_CHILD_TABLE);
|
|
1033
|
-
expect(child?.columns.map((entry) => entry.name)).toEqual(["id", "parent_id", "label", "obsolete"]);
|
|
1034
|
-
expect(child?.primaryKey?.columns).toEqual(["id"]);
|
|
1035
|
-
expect(child?.indexes.map((index) => index.name)).toContain(OBJECT_INDEX);
|
|
1036
|
+
expect(child?.columns.map((entry) => entry.name)).toEqual(["id", "parent_id", "label", "obsolete"].map(fold));
|
|
1037
|
+
expect(child?.primaryKey?.columns).toEqual([fold("id")]);
|
|
1038
|
+
expect(child?.indexes.map((index) => index.name)).toContain(fold(OBJECT_INDEX));
|
|
1036
1039
|
expect(child?.foreignKeys.map((key) => [key.target.relation, key.target.columns, key.onDelete])).toEqual([
|
|
1037
|
-
[OBJECT_PARENT_TABLE, ["id"], "CASCADE"],
|
|
1040
|
+
[fold(OBJECT_PARENT_TABLE), [fold("id")], "CASCADE"],
|
|
1038
1041
|
]);
|
|
1039
1042
|
});
|
|
1040
1043
|
});
|
|
@@ -1046,7 +1049,7 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1046
1049
|
const script = await scriptOf(session, {
|
|
1047
1050
|
kind: "alterTable",
|
|
1048
1051
|
path: path(),
|
|
1049
|
-
relation: OBJECT_CHILD_TABLE,
|
|
1052
|
+
relation: fold(OBJECT_CHILD_TABLE),
|
|
1050
1053
|
design: editorOf().design(relation),
|
|
1051
1054
|
});
|
|
1052
1055
|
expect(script.statements).toEqual([]);
|
|
@@ -1055,37 +1058,38 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1055
1058
|
it("givenTable_whenColumnAddedTypeChangedAndDropped_thenDescribedAfterAndRowsKept", async () => {
|
|
1056
1059
|
await withTables(async (session) => {
|
|
1057
1060
|
const design = await widenedChild(session);
|
|
1058
|
-
await apply(session, await scriptOf(session, { kind: "alterTable", path: path(), relation: OBJECT_CHILD_TABLE, design }));
|
|
1061
|
+
await apply(session, await scriptOf(session, { kind: "alterTable", path: path(), relation: fold(OBJECT_CHILD_TABLE), design }));
|
|
1059
1062
|
const child = await relationOf(session, OBJECT_CHILD_TABLE);
|
|
1060
|
-
expect(child?.columns.map((entry) => entry.name)).toEqual(["id", "parent_id", "label", "note"]);
|
|
1061
|
-
expect(child?.columns.find((entry) => entry.name === "label")?.type).toMatch(OBJECT_WIDE_LENGTH);
|
|
1063
|
+
expect(child?.columns.map((entry) => entry.name)).toEqual(["id", "parent_id", "label", "note"].map(fold));
|
|
1064
|
+
expect(child?.columns.find((entry) => entry.name === fold("label"))?.type).toMatch(OBJECT_WIDE_LENGTH);
|
|
1062
1065
|
expect(child?.foreignKeys).toHaveLength(1);
|
|
1063
|
-
expect(child?.indexes.map((index) => index.name)).toContain(OBJECT_INDEX);
|
|
1066
|
+
expect(child?.indexes.map((index) => index.name)).toContain(fold(OBJECT_INDEX));
|
|
1064
1067
|
expect(await run(session, `SELECT label FROM ${OBJECT_CHILD_TABLE}`)).toEqual([["kept"]]);
|
|
1065
1068
|
});
|
|
1066
1069
|
});
|
|
1067
1070
|
it.runIf(plugin.capabilities.includes("transactions"))("givenTransactionalScript_whenStatementFails_thenNothingApplied", async () => {
|
|
1068
1071
|
await withTables(async (session) => {
|
|
1069
|
-
//
|
|
1072
|
+
// A second index under the table's own index name clashes in every dialect; one named like a
|
|
1073
|
+
// table does not where index names are per table, as on SQL Server.
|
|
1070
1074
|
const design = await widenedChild(session, {});
|
|
1071
1075
|
const clashing = {
|
|
1072
1076
|
...design,
|
|
1073
1077
|
indexes: [
|
|
1074
1078
|
...design.indexes,
|
|
1075
|
-
{ name:
|
|
1079
|
+
{ name: fold(OBJECT_INDEX), columns: [fold("note")], unique: false, original: null },
|
|
1076
1080
|
],
|
|
1077
1081
|
};
|
|
1078
1082
|
const script = await scriptOf(session, {
|
|
1079
1083
|
kind: "alterTable",
|
|
1080
1084
|
path: path(),
|
|
1081
|
-
relation: OBJECT_CHILD_TABLE,
|
|
1085
|
+
relation: fold(OBJECT_CHILD_TABLE),
|
|
1082
1086
|
design: clashing,
|
|
1083
1087
|
});
|
|
1084
1088
|
if (!script.transactional)
|
|
1085
1089
|
return;
|
|
1086
1090
|
await expect(apply(session, script)).rejects.toThrow();
|
|
1087
1091
|
const child = await relationOf(session, OBJECT_CHILD_TABLE);
|
|
1088
|
-
expect(child?.columns.map((entry) => entry.name)).toEqual(["id", "parent_id", "label", "obsolete"]);
|
|
1092
|
+
expect(child?.columns.map((entry) => entry.name)).toEqual(["id", "parent_id", "label", "obsolete"].map(fold));
|
|
1089
1093
|
expect(await run(session, `SELECT label FROM ${OBJECT_CHILD_TABLE}`)).toEqual([["kept"]]);
|
|
1090
1094
|
});
|
|
1091
1095
|
});
|
|
@@ -1094,8 +1098,8 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1094
1098
|
await apply(session, await scriptOf(session, {
|
|
1095
1099
|
kind: "rename",
|
|
1096
1100
|
path: path(),
|
|
1097
|
-
object: { kind: "table", name: OBJECT_CHILD_TABLE },
|
|
1098
|
-
name: OBJECT_RENAMED_TABLE,
|
|
1101
|
+
object: { kind: "table", name: fold(OBJECT_CHILD_TABLE) },
|
|
1102
|
+
name: fold(OBJECT_RENAMED_TABLE),
|
|
1099
1103
|
}));
|
|
1100
1104
|
expect(await relationOf(session, OBJECT_CHILD_TABLE)).toBeUndefined();
|
|
1101
1105
|
expect((await relationOf(session, OBJECT_RENAMED_TABLE))?.columns).toHaveLength(4);
|
|
@@ -1103,7 +1107,12 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1103
1107
|
});
|
|
1104
1108
|
it("givenTableWithRows_whenTruncated_thenEmptyAndStillDescribed", async () => {
|
|
1105
1109
|
await withTables(async (session) => {
|
|
1106
|
-
await apply(session, await scriptOf(session, {
|
|
1110
|
+
await apply(session, await scriptOf(session, {
|
|
1111
|
+
kind: "truncate",
|
|
1112
|
+
path: path(),
|
|
1113
|
+
relation: fold(OBJECT_CHILD_TABLE),
|
|
1114
|
+
cascade: false,
|
|
1115
|
+
}));
|
|
1107
1116
|
expect(await run(session, `SELECT id FROM ${OBJECT_CHILD_TABLE}`)).toEqual([]);
|
|
1108
1117
|
expect(await relationOf(session, OBJECT_CHILD_TABLE)).toBeDefined();
|
|
1109
1118
|
});
|
|
@@ -1113,7 +1122,7 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1113
1122
|
await apply(session, await scriptOf(session, {
|
|
1114
1123
|
kind: "drop",
|
|
1115
1124
|
path: path(),
|
|
1116
|
-
object: { kind: "table", name: OBJECT_CHILD_TABLE },
|
|
1125
|
+
object: { kind: "table", name: fold(OBJECT_CHILD_TABLE) },
|
|
1117
1126
|
cascade: false,
|
|
1118
1127
|
}));
|
|
1119
1128
|
expect(await relationOf(session, OBJECT_CHILD_TABLE)).toBeUndefined();
|
|
@@ -1405,7 +1414,10 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1405
1414
|
await withCompletion(async (language, catalog) => {
|
|
1406
1415
|
const result = await language.complete(at("SELECT * FROM orders o JOIN |", "|"), catalog);
|
|
1407
1416
|
const first = [...result.items].sort((left, right) => right.boost - left.boost)[0];
|
|
1408
|
-
expect(first).toMatchObject({
|
|
1417
|
+
expect(first).toMatchObject({
|
|
1418
|
+
kind: "join",
|
|
1419
|
+
apply: `${fold("customers")} c ON c.${fold("id")} = o.${fold("customer_id")}`,
|
|
1420
|
+
});
|
|
1409
1421
|
});
|
|
1410
1422
|
});
|
|
1411
1423
|
it("givenNameNeedingQuotes_whenCompleting_thenApplyIsQuotedByDialect", async () => {
|
|
@@ -1498,7 +1510,7 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1498
1510
|
const read = session.introspector?.definition?.bind(session.introspector);
|
|
1499
1511
|
if (read === undefined)
|
|
1500
1512
|
throw new Error("A ddl session needs introspector.definition.");
|
|
1501
|
-
definition = await read(path, name);
|
|
1513
|
+
definition = await read(path, fold(name));
|
|
1502
1514
|
});
|
|
1503
1515
|
return definition;
|
|
1504
1516
|
}
|
|
@@ -1634,7 +1646,7 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1634
1646
|
await withSession(plugin, fixture, async (session) => {
|
|
1635
1647
|
const plan = await explained(session, dialectSql.explainJoin, false);
|
|
1636
1648
|
expect(plan.analyzed).toBe(false);
|
|
1637
|
-
expect(plan.statement).toMatch(
|
|
1649
|
+
expect(plan.statement).toMatch(dialectSql.explainStatement);
|
|
1638
1650
|
expect(plan.statement).toContain(dialectSql.explainJoin);
|
|
1639
1651
|
expect(plan.raw).toMatch(/\S/);
|
|
1640
1652
|
const nodes = planNodes(plan.roots);
|
|
@@ -1787,10 +1799,14 @@ export function runPluginContract(plugin, fixture) {
|
|
|
1787
1799
|
const { path } = namespaceSwitch();
|
|
1788
1800
|
const written = switcher();
|
|
1789
1801
|
await withSession(plugin, fixture, async (session) => {
|
|
1802
|
+
// A database switch reaches [database, default schema] on SQL Server, so the target may be a prefix.
|
|
1790
1803
|
const listed = (await session.introspector?.namespaces()) ?? [];
|
|
1791
|
-
const target = listed
|
|
1792
|
-
|
|
1793
|
-
|
|
1804
|
+
const target = listed
|
|
1805
|
+
.filter((namespace) => namespace.path.every((part, index) => part === path[index]))
|
|
1806
|
+
.filter((namespace) => switchableTo(written.targets, namespace))
|
|
1807
|
+
.sort((a, b) => b.path.length - a.path.length)[0];
|
|
1808
|
+
expect(target).toBeDefined();
|
|
1809
|
+
const sql = written.statement(target?.path ?? path);
|
|
1794
1810
|
expect(plugin.language?.classify(sql).setsNamespace).toBe(true);
|
|
1795
1811
|
await collect((await session.execute(sql)).rows);
|
|
1796
1812
|
expect(await current(session)).toEqual(path);
|
|
@@ -2240,7 +2256,16 @@ export function runPluginContract(plugin, fixture) {
|
|
|
2240
2256
|
});
|
|
2241
2257
|
});
|
|
2242
2258
|
});
|
|
2243
|
-
it("
|
|
2259
|
+
it.runIf(fixture.sessions?.cancels === false)("givenNoCancel_whenCancelSession_thenRejects", async () => {
|
|
2260
|
+
await withMonitor(async (monitor) => {
|
|
2261
|
+
await withSleeper(async (sleeper) => {
|
|
2262
|
+
const row = await present(monitor, idOf(sleeper), (listed) => listed.state === "active");
|
|
2263
|
+
expect(row.cancel).toBeNull();
|
|
2264
|
+
expect(await rejectionCode(monitor.cancelSession({ id: row.id, instance: row.instance }))).toBe("statementFailed");
|
|
2265
|
+
});
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
it.runIf(fixture.sessions?.cancels !== false)("givenActiveQuery_whenCancel_thenQueryCancelled", async () => {
|
|
2244
2269
|
await withMonitor(async (monitor) => {
|
|
2245
2270
|
await withSleeper(async (sleeper, running) => {
|
|
2246
2271
|
const row = await present(monitor, idOf(sleeper), (listed) => listed.state === "active");
|
|
@@ -2268,6 +2293,15 @@ export function runPluginContract(plugin, fixture) {
|
|
|
2268
2293
|
}
|
|
2269
2294
|
});
|
|
2270
2295
|
it("givenMissingSession_whenTerminate_thenRejects", async () => {
|
|
2296
|
+
const missingId = sessionsFixture().missingId;
|
|
2297
|
+
if (missingId !== undefined) {
|
|
2298
|
+
await withMonitor(async (monitor) => {
|
|
2299
|
+
const listed = await monitor.list();
|
|
2300
|
+
expect(listed.sessions.map((session) => session.id)).not.toContain(missingId);
|
|
2301
|
+
expect(await rejectionCode(monitor.terminateSession({ id: missingId, instance: null }))).toBe("statementFailed");
|
|
2302
|
+
});
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2271
2305
|
const victim = await connect(plugin, fixture.config);
|
|
2272
2306
|
const id = idOf(victim);
|
|
2273
2307
|
await victim.close();
|
package/dist/url.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface ConnectionUrlSpec {
|
|
|
25
25
|
readonly values?: Readonly<Record<string, string>> | undefined;
|
|
26
26
|
}[] | undefined;
|
|
27
27
|
readonly sets?: Readonly<Record<string, Readonly<Record<string, ConnectionValue>>>> | undefined;
|
|
28
|
+
readonly semicolonParams?: boolean | undefined;
|
|
28
29
|
}
|
|
29
30
|
interface UrlField {
|
|
30
31
|
readonly key: string;
|
package/dist/url.js
CHANGED
|
@@ -9,6 +9,9 @@ const SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
|
|
|
9
9
|
const WINDOWS_DRIVE_PATH = /^\/[A-Za-z]:[/\\]/;
|
|
10
10
|
const LEADING_SLASHES = /^\/+/;
|
|
11
11
|
const LOCAL_HOSTS = new Set(["", "localhost"]);
|
|
12
|
+
/** `host`, `[v6]`, then SQL Server's `\instance` and the port, as the JDBC driver reads them. */
|
|
13
|
+
const SEMICOLON_AUTHORITY = /^(\[[^\]]*\]|[^\\:]*)(?:\\([^:]*))?(?::([^:]*))?$/;
|
|
14
|
+
const INSTANCE_PARAM = "instanceName";
|
|
12
15
|
/** libpq's names for the parts of the URL, accepted as query parameters by every server form. */
|
|
13
16
|
const PART_PARAMS = {
|
|
14
17
|
host: "host",
|
|
@@ -104,6 +107,8 @@ function parseServer(form, url, text) {
|
|
|
104
107
|
if (scheme === undefined || !url.schemes.includes(scheme) || !text.slice(scheme.length + 1).startsWith("//")) {
|
|
105
108
|
throw new ConnectionUrlError(`Expected a URL starting with ${schemeList(url)}.`);
|
|
106
109
|
}
|
|
110
|
+
if (url.semicolonParams === true)
|
|
111
|
+
return parseSemicolon(form, url, scheme, text);
|
|
107
112
|
let parsed;
|
|
108
113
|
try {
|
|
109
114
|
parsed = new URL(text);
|
|
@@ -121,6 +126,56 @@ function parseServer(form, url, text) {
|
|
|
121
126
|
collected.query(parsed.search);
|
|
122
127
|
return collected.done();
|
|
123
128
|
}
|
|
129
|
+
/** Splits on `;` outside `{…}`, which the JDBC driver uses to put a `;` in a value. */
|
|
130
|
+
function semicolonPairs(text) {
|
|
131
|
+
const pairs = [];
|
|
132
|
+
let current = "";
|
|
133
|
+
let braced = false;
|
|
134
|
+
for (const char of text) {
|
|
135
|
+
if (char === "{")
|
|
136
|
+
braced = true;
|
|
137
|
+
else if (char === "}")
|
|
138
|
+
braced = false;
|
|
139
|
+
if (char === ";" && !braced) {
|
|
140
|
+
pairs.push(current);
|
|
141
|
+
current = "";
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
current += char;
|
|
145
|
+
}
|
|
146
|
+
pairs.push(current);
|
|
147
|
+
return pairs.filter((pair) => pair.trim() !== "");
|
|
148
|
+
}
|
|
149
|
+
function unbraced(value) {
|
|
150
|
+
const trimmed = value.trim();
|
|
151
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}") ? trimmed.slice(1, -1) : trimmed;
|
|
152
|
+
}
|
|
153
|
+
function parseSemicolon(form, url, scheme, text) {
|
|
154
|
+
const [authority = "", ...pairs] = semicolonPairs(text.slice(scheme.length + "://".length));
|
|
155
|
+
const match = SEMICOLON_AUTHORITY.exec(authority.trim());
|
|
156
|
+
if (match === null) {
|
|
157
|
+
throw new ConnectionUrlError(`"${authority}" is not host, host\\instance or host:port.`);
|
|
158
|
+
}
|
|
159
|
+
const [, host = "", instance, port] = match;
|
|
160
|
+
if (port !== undefined && !/^\d+$/.test(port)) {
|
|
161
|
+
throw new ConnectionUrlError(`The port "${port}" is not a number.`);
|
|
162
|
+
}
|
|
163
|
+
const collected = new Collected(form, url);
|
|
164
|
+
Object.assign(collected.values, url.sets?.[scheme]);
|
|
165
|
+
collected.part("host", host.replace(/^\[(.*)\]$/, "$1"));
|
|
166
|
+
if (port !== undefined)
|
|
167
|
+
collected.part("port", port);
|
|
168
|
+
if (instance !== undefined && instance !== "")
|
|
169
|
+
collected.param(INSTANCE_PARAM, instance);
|
|
170
|
+
for (const pair of pairs) {
|
|
171
|
+
const equals = pair.indexOf("=");
|
|
172
|
+
if (equals < 0)
|
|
173
|
+
collected.ignored.push(pair.trim());
|
|
174
|
+
else
|
|
175
|
+
collected.param(pair.slice(0, equals).trim(), unbraced(pair.slice(equals + 1)));
|
|
176
|
+
}
|
|
177
|
+
return collected.done();
|
|
178
|
+
}
|
|
124
179
|
function parseFile(form, url, text) {
|
|
125
180
|
const collected = new Collected(form, url);
|
|
126
181
|
const scheme = SCHEME.exec(text)?.[1]?.toLowerCase();
|
|
@@ -175,6 +230,37 @@ function isConnectionValue(value) {
|
|
|
175
230
|
function written(value) {
|
|
176
231
|
return String(value);
|
|
177
232
|
}
|
|
233
|
+
function bracedValue(value) {
|
|
234
|
+
return /[;{}=]/.test(value) || value !== value.trim() ? `{${value}}` : value;
|
|
235
|
+
}
|
|
236
|
+
/** Every parameter after a `;`, one per field, so a part a parameter also names is written once. */
|
|
237
|
+
function semicolonUrl(form, url, scheme, config) {
|
|
238
|
+
const text = (key) => {
|
|
239
|
+
if (key === undefined || fieldOf(form, key)?.secret === true)
|
|
240
|
+
return "";
|
|
241
|
+
const value = config[key];
|
|
242
|
+
return isConnectionValue(value) ? written(value) : "";
|
|
243
|
+
};
|
|
244
|
+
const host = text(url.parts.host);
|
|
245
|
+
const port = text(url.parts.port);
|
|
246
|
+
const pairs = [];
|
|
247
|
+
const keys = new Set([url.parts.host, url.parts.port]);
|
|
248
|
+
for (const param of url.params ?? []) {
|
|
249
|
+
if (keys.has(param.key))
|
|
250
|
+
continue;
|
|
251
|
+
keys.add(param.key);
|
|
252
|
+
const value = config[param.key];
|
|
253
|
+
const field = fieldOf(form, param.key);
|
|
254
|
+
if (field?.secret === true || !isConnectionValue(value) || value === "" || value === field?.default)
|
|
255
|
+
continue;
|
|
256
|
+
const shown = param.values === undefined
|
|
257
|
+
? written(value)
|
|
258
|
+
: Object.entries(param.values).find(([, mapped]) => mapped === value)?.[0];
|
|
259
|
+
if (shown !== undefined)
|
|
260
|
+
pairs.push(`;${param.name}=${bracedValue(shown)}`);
|
|
261
|
+
}
|
|
262
|
+
return `${scheme}://${host.includes(":") ? `[${host}]` : host}${port === "" ? "" : `:${port}`}${pairs.join("")}`;
|
|
263
|
+
}
|
|
178
264
|
function encodedPath(path) {
|
|
179
265
|
return encodeURI(path).replaceAll("?", "%3F").replaceAll("#", "%23");
|
|
180
266
|
}
|
|
@@ -193,6 +279,8 @@ export function formatConnectionUrl(form, config) {
|
|
|
193
279
|
url.schemes.find((candidate) => url.sets?.[candidate] === undefined) ??
|
|
194
280
|
url.schemes[0] ??
|
|
195
281
|
"";
|
|
282
|
+
if (url.semicolonParams === true)
|
|
283
|
+
return semicolonUrl(form, url, scheme, config);
|
|
196
284
|
const valueOf = (part) => {
|
|
197
285
|
const key = url.parts[part];
|
|
198
286
|
if (key === undefined || fieldOf(form, key)?.secret === true)
|
package/dist/version.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The plugin contract's version, by semver (decision 106): a host loads a plugin built for the same
|
|
3
3
|
* major and an older or equal minor. Typed `string` so a bump does not change the API report.
|
|
4
4
|
*/
|
|
5
|
-
export const API_VERSION = "1.
|
|
5
|
+
export const API_VERSION = "1.5.0";
|
|
6
6
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
7
7
|
/** `major.minor.patch` and nothing else; pre-release and build tags are not accepted. */
|
|
8
8
|
export function parseVersion(text) {
|