@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,806 @@
1
+ import { EXPLAIN_VERBS } from "./words.js";
2
+ import { firstTok, isWord, lastTok, spells, Unsupported, } from "./layoutNodes.js";
3
+ import { Writer } from "./layoutWriter.js";
4
+ /** Past this width an aliased expression neither pads to the alias column nor pushes it right. */
5
+ export const ALIAS_ALIGN_LIMIT = 80;
6
+ /** A `CASE` up to this width stays on one line. */
7
+ export const CASE_INLINE_LIMIT = 60;
8
+ /** A parenthesised condition group ending past this column opens one condition per line (DataGrip's margin). */
9
+ export const RIGHT_MARGIN = 120;
10
+ const COMMA = ",";
11
+ const CAST = "::";
12
+ const EQUALS = "=";
13
+ /** Operators written with one space on each side; any other run of operator characters keeps its source spacing. */
14
+ const SPACED_OPERATORS = new Set(["=", "<", ">", "<=", ">=", "<>", "!=", "<=>", ":="]);
15
+ const SET_OPERATORS = new Set(["UNION", "INTERSECT", "EXCEPT", "MINUS"]);
16
+ const SET_QUANTIFIERS = new Set(["ALL", "DISTINCT"]);
17
+ const SELECT_MODIFIERS = new Set([
18
+ "ALL",
19
+ "DISTINCT",
20
+ "DISTINCTROW",
21
+ "HIGH_PRIORITY",
22
+ "STRAIGHT_JOIN",
23
+ "SQL_SMALL_RESULT",
24
+ "SQL_BIG_RESULT",
25
+ "SQL_BUFFER_RESULT",
26
+ "SQL_NO_CACHE",
27
+ "SQL_CACHE",
28
+ "SQL_CALC_FOUND_ROWS",
29
+ ]);
30
+ const JOIN_WORDS = new Set(["JOIN", "STRAIGHT_JOIN"]);
31
+ const JOIN_MODIFIERS = new Set(["NATURAL", "INNER", "LEFT", "RIGHT", "FULL", "OUTER", "CROSS"]);
32
+ const CONDITION_OPERATORS = new Set(["AND", "OR"]);
33
+ const QUERY_VERBS = new Set(["SELECT", "WITH", "INSERT", "REPLACE", "UPDATE", "DELETE"]);
34
+ const EXPLAIN_WORDS = new Set([
35
+ "EXPLAIN",
36
+ "DESCRIBE",
37
+ "DESC",
38
+ "ANALYZE",
39
+ "ANALYSE",
40
+ "VERBOSE",
41
+ "FORMAT",
42
+ "EXTENDED",
43
+ "PARTITIONS",
44
+ "QUERY",
45
+ "PLAN",
46
+ ]);
47
+ const INDEX_WORDS = new Set(["INDEX", "KEY"]);
48
+ const DELETE_MODIFIERS = new Set(["LOW_PRIORITY", "QUICK", "IGNORE"]);
49
+ function wordAt(nodes, index) {
50
+ const node = nodes[index];
51
+ return node?.type === "tok" && node.kind === "word" ? node.upper : "";
52
+ }
53
+ function keywordAt(nodes, index) {
54
+ const node = nodes[index];
55
+ return node?.type === "tok" && node.keyword ? node.upper : "";
56
+ }
57
+ function selectClauses(nodes, index, current) {
58
+ const word = keywordAt(nodes, index);
59
+ switch (word) {
60
+ case "SELECT":
61
+ case "INTO":
62
+ case "FROM":
63
+ case "WHERE":
64
+ case "HAVING":
65
+ case "WINDOW":
66
+ case "LIMIT":
67
+ case "FETCH":
68
+ return 1;
69
+ case "GROUP":
70
+ case "ORDER":
71
+ return wordAt(nodes, index + 1) === "BY" ? 2 : 0;
72
+ case "OFFSET":
73
+ return current === "LIMIT" ? 0 : 1;
74
+ case "FOR":
75
+ return INDEX_WORDS.has(wordAt(nodes, index - 1)) || current === "FOR" ? 0 : 1;
76
+ case "LOCK":
77
+ return wordAt(nodes, index + 1) === "IN" ? 1 : 0;
78
+ default:
79
+ return 0;
80
+ }
81
+ }
82
+ function updateClauses(nodes, index) {
83
+ const word = keywordAt(nodes, index);
84
+ if (word === "UPDATE" || word === "SET" || word === "FROM" || word === "WHERE" || word === "LIMIT")
85
+ return 1;
86
+ if (word === "RETURNING")
87
+ return 1;
88
+ return word === "ORDER" && wordAt(nodes, index + 1) === "BY" ? 2 : 0;
89
+ }
90
+ function deleteClauses(nodes, index) {
91
+ const word = keywordAt(nodes, index);
92
+ if (word === "DELETE" || word === "FROM" || word === "USING" || word === "WHERE" || word === "LIMIT")
93
+ return 1;
94
+ if (word === "RETURNING")
95
+ return 1;
96
+ return word === "ORDER" && wordAt(nodes, index + 1) === "BY" ? 2 : 0;
97
+ }
98
+ function splitClauses(nodes, matcher) {
99
+ const head = [];
100
+ const clauses = [];
101
+ for (let index = 0; index < nodes.length;) {
102
+ const count = matcher(nodes, index, clauses.at(-1)?.name ?? "");
103
+ if (count > 0) {
104
+ const keywords = nodes.slice(index, index + count);
105
+ for (const keyword of keywords)
106
+ keyword.keyword = true;
107
+ clauses.push({ name: keywords.map((keyword) => keyword.upper).join(" "), keywords, body: [] });
108
+ index += count;
109
+ continue;
110
+ }
111
+ const node = nodes[index];
112
+ if (node !== undefined)
113
+ (clauses.at(-1)?.body ?? head).push(node);
114
+ index += 1;
115
+ }
116
+ return { head, clauses };
117
+ }
118
+ function splitTop(nodes, separator) {
119
+ const parts = [[]];
120
+ const commas = [];
121
+ for (const node of nodes) {
122
+ if (node.type === "tok" && node.text === separator) {
123
+ commas.push(node);
124
+ parts.push([]);
125
+ }
126
+ else {
127
+ parts[parts.length - 1]?.push(node);
128
+ }
129
+ }
130
+ return { parts, commas };
131
+ }
132
+ /** Top-level `AND` / `OR`, but not the `AND` of `BETWEEN a AND b`. */
133
+ function splitConditions(nodes) {
134
+ const conditions = [{ operator: null, nodes: [] }];
135
+ let between = false;
136
+ for (const node of nodes) {
137
+ if (isWord(node, "BETWEEN"))
138
+ between = true;
139
+ if (node.type === "tok" && node.keyword && CONDITION_OPERATORS.has(node.upper)) {
140
+ if (between && node.upper === "AND") {
141
+ between = false;
142
+ }
143
+ else {
144
+ conditions.push({ operator: node, nodes: [] });
145
+ continue;
146
+ }
147
+ }
148
+ conditions[conditions.length - 1]?.nodes.push(node);
149
+ }
150
+ return conditions;
151
+ }
152
+ function isQueryStart(nodes) {
153
+ const first = nodes[0];
154
+ if (first === undefined)
155
+ return false;
156
+ if (first.type === "group")
157
+ return isQueryStart(first.children);
158
+ return first.type === "tok" && (isWord(first, "SELECT") || isWord(first, "WITH"));
159
+ }
160
+ function isSubquery(group) {
161
+ return isQueryStart(group.children);
162
+ }
163
+ function isSpacedOperator(tok) {
164
+ return tok.kind === "symbol" && SPACED_OPERATORS.has(tok.text);
165
+ }
166
+ function casedKeyword(text, options) {
167
+ if (options.keywordCase === "upper")
168
+ return text.toUpperCase();
169
+ if (options.keywordCase === "lower")
170
+ return text.toLowerCase();
171
+ return text;
172
+ }
173
+ /** The aligned layout of one statement (decision 061). Throws `Unsupported` for what it does not read. */
174
+ export class AlignedPrinter {
175
+ options;
176
+ constructor(options) {
177
+ this.options = options;
178
+ }
179
+ text(tok) {
180
+ return tok.keyword ? casedKeyword(tok.text, this.options) : tok.text;
181
+ }
182
+ print(nodes, tail) {
183
+ const writer = new Writer(0);
184
+ const prefixEnd = this.#explainPrefixEnd(nodes);
185
+ if (prefixEnd > 0) {
186
+ this.inline(writer, nodes.slice(0, prefixEnd), false, { preserve: true });
187
+ writer.newline(0);
188
+ }
189
+ this.statement(writer, nodes.slice(prefixEnd), 0);
190
+ for (const comment of tail)
191
+ writer.comment(comment, 0);
192
+ return { text: writer.out, endsInLineComment: writer.endsInLineComment };
193
+ }
194
+ #explainPrefixEnd(nodes) {
195
+ if (!EXPLAIN_VERBS.has(wordAt(nodes, 0)))
196
+ return 0;
197
+ const start = nodes.findIndex((node) => (node.type === "tok" && node.kind === "word" && QUERY_VERBS.has(node.upper)) ||
198
+ (node.type === "group" && isSubquery(node)));
199
+ if (start <= 0)
200
+ throw new Unsupported("an EXPLAIN of no query");
201
+ for (const node of nodes.slice(0, start))
202
+ this.#markExplainWords(node);
203
+ return start;
204
+ }
205
+ #markExplainWords(node) {
206
+ if (node.type === "tok") {
207
+ if (node.kind === "word" && EXPLAIN_WORDS.has(node.upper))
208
+ node.keyword = true;
209
+ }
210
+ else if (node.type === "group") {
211
+ for (const child of node.children)
212
+ this.#markExplainWords(child);
213
+ }
214
+ }
215
+ statement(writer, nodes, base) {
216
+ const first = nodes[0];
217
+ if (first === undefined)
218
+ throw new Unsupported("an empty statement");
219
+ if (first.type === "group") {
220
+ this.setQuery(writer, nodes, base);
221
+ return;
222
+ }
223
+ switch (first.type === "tok" && first.kind === "word" ? first.upper : "") {
224
+ case "WITH":
225
+ this.withQuery(writer, nodes, base);
226
+ return;
227
+ case "SELECT":
228
+ this.setQuery(writer, nodes, base);
229
+ return;
230
+ case "INSERT":
231
+ case "REPLACE":
232
+ this.insert(writer, nodes, base);
233
+ return;
234
+ case "UPDATE":
235
+ this.update(writer, nodes, base);
236
+ return;
237
+ case "DELETE":
238
+ this.delete(writer, nodes, base);
239
+ return;
240
+ default:
241
+ throw new Unsupported("a statement the aligned layout does not read");
242
+ }
243
+ }
244
+ withQuery(writer, nodes, base) {
245
+ let index = 0;
246
+ const keywords = [];
247
+ while (index < nodes.length && (isWord(nodes[index], "WITH") || isWord(nodes[index], "RECURSIVE"))) {
248
+ keywords.push(nodes[index]);
249
+ index += 1;
250
+ }
251
+ keywords.forEach((keyword, at) => {
252
+ writer.tok(keyword, this.text(keyword), at > 0);
253
+ });
254
+ const column = writer.column + 1;
255
+ const saved = writer.indent;
256
+ writer.indent = column;
257
+ let first = true;
258
+ for (;;) {
259
+ const bodyAt = nodes.findIndex((node, at) => at > index && node.type === "group" && (isWord(nodes[at - 1], "AS") || isWord(nodes[at - 1], "MATERIALIZED")));
260
+ const body = nodes[bodyAt];
261
+ if (body?.type !== "group")
262
+ throw new Unsupported("a WITH without its query");
263
+ const asAt = nodes.slice(index, bodyAt).findIndex((node) => isWord(node, "AS")) + index;
264
+ if (asAt < index)
265
+ throw new Unsupported("a common table expression without AS");
266
+ this.inline(writer, nodes.slice(index, asAt), first, { spaceBeforeGroup: true });
267
+ this.inline(writer, nodes.slice(asAt, bodyAt), true);
268
+ writer.tok(body.open, this.text(body.open), true);
269
+ this.#nested(writer, body.children);
270
+ writer.tok(body.close, this.text(body.close), false);
271
+ first = false;
272
+ index = bodyAt + 1;
273
+ const next = nodes[index];
274
+ if (next?.type === "tok" && next.text === COMMA) {
275
+ writer.tok(next, next.text, false);
276
+ writer.newline(column);
277
+ index += 1;
278
+ continue;
279
+ }
280
+ break;
281
+ }
282
+ writer.indent = saved;
283
+ if (index >= nodes.length)
284
+ throw new Unsupported("a WITH without its main statement");
285
+ writer.newline(base);
286
+ this.statement(writer, nodes.slice(index), base);
287
+ }
288
+ /** A statement inside parentheses, laid out at the column after `(`. */
289
+ #nested(writer, nodes) {
290
+ const saved = writer.indent;
291
+ const column = writer.column;
292
+ writer.indent = column;
293
+ this.statement(writer, nodes, column);
294
+ writer.indent = saved;
295
+ }
296
+ setQuery(writer, nodes, base) {
297
+ const parts = [[]];
298
+ const operators = [];
299
+ for (let index = 0; index < nodes.length; index += 1) {
300
+ const node = nodes[index];
301
+ if (node === undefined)
302
+ continue;
303
+ if (node.type === "tok" && node.keyword && SET_OPERATORS.has(node.upper)) {
304
+ const operator = [node];
305
+ const quantifier = nodes[index + 1];
306
+ if (quantifier?.type === "tok" && quantifier.keyword && SET_QUANTIFIERS.has(quantifier.upper)) {
307
+ operator.push(quantifier);
308
+ index += 1;
309
+ }
310
+ operators.push(operator);
311
+ parts.push([]);
312
+ continue;
313
+ }
314
+ parts[parts.length - 1]?.push(node);
315
+ }
316
+ parts.forEach((part, at) => {
317
+ const operator = operators[at - 1];
318
+ if (operator !== undefined) {
319
+ writer.newline(base);
320
+ operator.forEach((tok, position) => {
321
+ writer.tok(tok, this.text(tok), position > 0);
322
+ });
323
+ writer.newline(base);
324
+ }
325
+ this.select(writer, part, base);
326
+ });
327
+ }
328
+ select(writer, nodes, base) {
329
+ const first = nodes[0];
330
+ if (first === undefined)
331
+ throw new Unsupported("an empty query");
332
+ let rest = nodes;
333
+ let started = false;
334
+ if (first.type === "group") {
335
+ if (!isQueryStart(first.children))
336
+ throw new Unsupported("a parenthesised query of no SELECT");
337
+ writer.tok(first.open, first.open.text, false);
338
+ this.#nested(writer, first.children);
339
+ writer.tok(first.close, first.close.text, false);
340
+ rest = nodes.slice(1);
341
+ started = true;
342
+ }
343
+ else if (!isWord(first, "SELECT")) {
344
+ throw new Unsupported("a query that does not start with SELECT");
345
+ }
346
+ const { head, clauses } = splitClauses(rest, selectClauses);
347
+ if (head.length > 0)
348
+ throw new Unsupported("tokens before the first clause");
349
+ clauses.forEach((clause, at) => {
350
+ if (at > 0 || started)
351
+ writer.newline(base);
352
+ writer.indent = base;
353
+ this.clause(writer, clause);
354
+ });
355
+ }
356
+ clause(writer, clause) {
357
+ switch (clause.name) {
358
+ case "SELECT":
359
+ this.selectClause(writer, clause);
360
+ return;
361
+ case "FROM":
362
+ case "UPDATE":
363
+ this.#keywords(writer, clause.keywords);
364
+ this.fromItems(writer, clause.body, writer.column + 1);
365
+ return;
366
+ case "WHERE":
367
+ case "HAVING":
368
+ this.#keywords(writer, clause.keywords);
369
+ this.conditions(writer, clause.body, writer.column - this.#width(clause.keywords), writer.column);
370
+ return;
371
+ case "SET":
372
+ this.#keywords(writer, clause.keywords);
373
+ this.assignments(writer, clause.body, writer.column + 1);
374
+ return;
375
+ default:
376
+ this.#keywords(writer, clause.keywords);
377
+ this.#inlineBody(writer, clause.body);
378
+ }
379
+ }
380
+ #keywords(writer, keywords, leadSpace = false) {
381
+ keywords.forEach((keyword, at) => {
382
+ writer.tok(keyword, this.text(keyword), at > 0 || leadSpace);
383
+ });
384
+ }
385
+ /** The width of `keywords` written on one line. */
386
+ #width(keywords) {
387
+ return keywords.map((keyword) => this.text(keyword)).join(" ").length;
388
+ }
389
+ #inlineBody(writer, body) {
390
+ const saved = writer.indent;
391
+ writer.indent = writer.column + 1;
392
+ this.inline(writer, body, true);
393
+ writer.indent = saved;
394
+ }
395
+ selectClause(writer, clause) {
396
+ const keywords = [...clause.keywords];
397
+ let index = 0;
398
+ for (; index < clause.body.length; index += 1) {
399
+ const node = clause.body[index];
400
+ if (node?.type !== "tok" || !node.keyword || !SELECT_MODIFIERS.has(node.upper))
401
+ break;
402
+ keywords.push(node);
403
+ if (node.upper === "DISTINCT" && isWord(clause.body[index + 1], "ON")) {
404
+ keywords.push(clause.body[index + 1]);
405
+ index += 1;
406
+ }
407
+ }
408
+ this.#keywords(writer, keywords);
409
+ const distinctOn = clause.body[index];
410
+ if (isWord(clause.body[index - 1], "ON") && distinctOn?.type === "group") {
411
+ this.node(writer, distinctOn, true);
412
+ index += 1;
413
+ }
414
+ this.selectList(writer, clause.body.slice(index), writer.column + 1);
415
+ }
416
+ /** One item per line under the first; aliases after `AS` line up (rule 5). */
417
+ selectList(writer, nodes, column) {
418
+ const { parts, commas } = splitTop(nodes, COMMA);
419
+ const items = parts.map((part) => {
420
+ const as = part[part.length - 2];
421
+ const name = part[part.length - 1];
422
+ const aliased = part.length >= 3 && isWord(as, "AS") && name?.type === "tok" && name.kind !== "symbol";
423
+ const expression = aliased ? part.slice(0, -2) : part;
424
+ const width = aliased ? this.#singleLineWidth(column, expression) : null;
425
+ return { part, expression, alias: aliased ? part.slice(-2) : [], width };
426
+ });
427
+ const target = this.#alignment(items.map((item) => item.width));
428
+ const saved = writer.indent;
429
+ writer.indent = column;
430
+ items.forEach((item, at) => {
431
+ if (at > 0)
432
+ writer.newline(column);
433
+ this.inline(writer, item.expression, at === 0);
434
+ if (item.alias.length > 0) {
435
+ if (target !== null && item.width !== null)
436
+ writer.pad(target - item.width);
437
+ this.inline(writer, item.alias, true);
438
+ }
439
+ const comma = commas[at];
440
+ if (comma !== undefined)
441
+ writer.tok(comma, comma.text, false);
442
+ });
443
+ writer.indent = saved;
444
+ }
445
+ /** The width every aligned item pads to, or null when nothing aligns. */
446
+ #alignment(widths) {
447
+ if (!this.options.alignAliases)
448
+ return null;
449
+ const aligned = widths.filter((width) => width !== null);
450
+ return aligned.length === 0 ? null : Math.max(...aligned);
451
+ }
452
+ /** The width of `nodes` written inline at `column`, or null when that spans lines or passes the limit. */
453
+ #singleLineWidth(column, nodes) {
454
+ const measured = this.measure(column, (scratch) => {
455
+ this.inline(scratch, nodes, false);
456
+ });
457
+ return measured.multiline || measured.width > ALIAS_ALIGN_LIMIT ? null : measured.width;
458
+ }
459
+ measure(column, render) {
460
+ const scratch = new Writer(column);
461
+ render(scratch);
462
+ return {
463
+ width: scratch.out.length,
464
+ multiline: scratch.out.includes("\n") || scratch.endsInLineComment,
465
+ };
466
+ }
467
+ /** `SET a = 1,` with each further assignment under the first and the `=` lined up like aliases. */
468
+ assignments(writer, nodes, column) {
469
+ const { parts, commas } = splitTop(nodes, COMMA);
470
+ const items = parts.map((part) => {
471
+ const equals = part.findIndex((node) => node.type === "tok" && node.text === EQUALS);
472
+ const target = equals > 0 ? part.slice(0, equals) : part;
473
+ return {
474
+ target,
475
+ rest: equals > 0 ? part.slice(equals) : [],
476
+ width: equals > 0 ? this.#singleLineWidth(column, target) : null,
477
+ };
478
+ });
479
+ const target = this.#alignment(items.map((item) => item.width));
480
+ const saved = writer.indent;
481
+ writer.indent = column;
482
+ items.forEach((item, at) => {
483
+ if (at > 0)
484
+ writer.newline(column);
485
+ this.inline(writer, item.target, at === 0);
486
+ if (item.rest.length > 0) {
487
+ if (target !== null && item.width !== null)
488
+ writer.pad(target - item.width);
489
+ this.inline(writer, item.rest, true);
490
+ }
491
+ const comma = commas[at];
492
+ if (comma !== undefined)
493
+ writer.tok(comma, comma.text, false);
494
+ });
495
+ writer.indent = saved;
496
+ }
497
+ /** Tables one per line under the first; each join on its own line at the same column (rules 7-8). */
498
+ fromItems(writer, nodes, column) {
499
+ const { parts, commas } = splitTop(nodes, COMMA);
500
+ const saved = writer.indent;
501
+ writer.indent = column;
502
+ parts.forEach((part, at) => {
503
+ if (at > 0)
504
+ writer.newline(column);
505
+ this.#fromItem(writer, part, column, at === 0);
506
+ const comma = commas[at];
507
+ if (comma !== undefined)
508
+ writer.tok(comma, comma.text, false);
509
+ });
510
+ writer.indent = saved;
511
+ }
512
+ #fromItem(writer, nodes, column, leadSpace) {
513
+ const starts = [];
514
+ nodes.forEach((node, at) => {
515
+ if (node.type !== "tok" || !node.keyword || !JOIN_WORDS.has(node.upper))
516
+ return;
517
+ let start = at;
518
+ while (start > 0 && JOIN_MODIFIERS.has(keywordAt(nodes, start - 1)))
519
+ start -= 1;
520
+ starts.push(start);
521
+ });
522
+ const bounds = [0, ...starts, nodes.length];
523
+ for (let segment = 0; segment + 1 < bounds.length; segment += 1) {
524
+ const part = nodes.slice(bounds[segment], bounds[segment + 1]);
525
+ if (part.length === 0)
526
+ continue;
527
+ if (segment === 0) {
528
+ this.inline(writer, part, leadSpace);
529
+ continue;
530
+ }
531
+ writer.newline(column);
532
+ writer.indent = column;
533
+ const on = part.findIndex((node) => isWord(node, "ON"));
534
+ if (on < 0) {
535
+ this.inline(writer, part, false);
536
+ continue;
537
+ }
538
+ this.inline(writer, part.slice(0, on + 1), false);
539
+ const keyword = part[on];
540
+ this.conditions(writer, part.slice(on + 1), writer.column - this.text(keyword).length, writer.column);
541
+ }
542
+ }
543
+ /**
544
+ * The first condition on the keyword's line, each further `AND` / `OR` on its own line ending
545
+ * where the keyword ends, or starting under it when the keyword is shorter (rule 9).
546
+ */
547
+ conditions(writer, nodes, keywordStart, keywordEnd) {
548
+ const saved = writer.indent;
549
+ splitConditions(nodes).forEach((condition) => {
550
+ let anchor = keywordStart;
551
+ if (condition.operator !== null) {
552
+ const text = this.text(condition.operator);
553
+ anchor = Math.max(keywordStart, keywordEnd - text.length);
554
+ writer.newline(anchor);
555
+ writer.tok(condition.operator, text, false);
556
+ }
557
+ writer.indent = writer.column + 1;
558
+ this.condition(writer, condition.nodes, anchor, true);
559
+ });
560
+ writer.indent = saved;
561
+ }
562
+ /** A parenthesised group of conditions too wide for its line opens one condition per line (rule 10). */
563
+ condition(writer, nodes, anchor, leadSpace) {
564
+ const group = nodes[nodes.length - 1];
565
+ const prefix = nodes.slice(0, -1);
566
+ const expandable = group?.type === "group" &&
567
+ !isSubquery(group) &&
568
+ prefix.every((node) => isWord(node, "NOT")) &&
569
+ splitConditions(group.children).length > 1;
570
+ if (!expandable) {
571
+ this.inline(writer, nodes, leadSpace);
572
+ return;
573
+ }
574
+ const start = writer.column + (leadSpace ? 1 : 0);
575
+ const measured = this.measure(start, (scratch) => {
576
+ this.inline(scratch, nodes, false);
577
+ });
578
+ if (!measured.multiline && start + measured.width <= RIGHT_MARGIN) {
579
+ this.inline(writer, nodes, leadSpace);
580
+ return;
581
+ }
582
+ this.inline(writer, prefix, leadSpace);
583
+ writer.tok(group.open, group.open.text, prefix.length > 0 || leadSpace);
584
+ const inner = anchor + this.options.indentWidth;
585
+ const saved = writer.indent;
586
+ const parts = splitConditions(group.children);
587
+ parts.forEach((part, at) => {
588
+ writer.newline(inner);
589
+ writer.indent = inner;
590
+ this.condition(writer, part.nodes, inner, false);
591
+ const next = parts[at + 1]?.operator ?? null;
592
+ if (next !== null)
593
+ writer.tok(next, this.text(next), true);
594
+ });
595
+ writer.tok(group.close, group.close.text, false);
596
+ writer.indent = saved;
597
+ }
598
+ insert(writer, nodes, base) {
599
+ let index = 0;
600
+ const headEnd = nodes.findIndex((node, at) => at > 0 &&
601
+ ((node.type === "tok" &&
602
+ node.kind === "word" &&
603
+ (node.upper === "VALUES" ||
604
+ node.upper === "VALUE" ||
605
+ isWord(node, "SET") ||
606
+ isWord(node, "SELECT") ||
607
+ isWord(node, "WITH") ||
608
+ (isWord(node, "DEFAULT") && spells(nodes[at + 1], "VALUES")))) ||
609
+ (node.type === "group" && isSubquery(node))));
610
+ const end = headEnd < 0 ? nodes.length : headEnd;
611
+ const verb = nodes[0];
612
+ verb.keyword = true;
613
+ this.inline(writer, nodes.slice(0, end), false, { spaceBeforeGroup: true });
614
+ index = end;
615
+ if (index >= nodes.length)
616
+ return;
617
+ const tailAt = nodes.findIndex((node, at) => at > index &&
618
+ (isWord(node, "RETURNING") ||
619
+ (isWord(node, "ON") && (spells(nodes[at + 1], "DUPLICATE") || spells(nodes[at + 1], "CONFLICT")))));
620
+ const bodyEnd = tailAt < 0 ? nodes.length : tailAt;
621
+ const body = nodes.slice(index, bodyEnd);
622
+ writer.newline(base);
623
+ writer.indent = base;
624
+ const first = body[0];
625
+ if (first?.type === "tok" && (first.upper === "VALUES" || first.upper === "VALUE")) {
626
+ first.keyword = true;
627
+ writer.tok(first, this.text(first), false);
628
+ this.#rows(writer, body.slice(1), writer.column + 1);
629
+ }
630
+ else if (isWord(first, "SET")) {
631
+ writer.tok(first, this.text(first), false);
632
+ this.assignments(writer, body.slice(1), writer.column + 1);
633
+ }
634
+ else if (isWord(first, "DEFAULT")) {
635
+ for (const node of body)
636
+ if (node.type === "tok")
637
+ node.keyword = true;
638
+ this.inline(writer, body, false);
639
+ }
640
+ else {
641
+ this.statement(writer, body, base);
642
+ }
643
+ if (tailAt >= 0)
644
+ this.#insertTail(writer, nodes.slice(tailAt), base);
645
+ }
646
+ #insertTail(writer, nodes, base) {
647
+ const returning = nodes.findIndex((node, at) => at > 0 && isWord(node, "RETURNING"));
648
+ const clauses = returning < 0 ? [nodes] : [nodes.slice(0, returning), nodes.slice(returning)];
649
+ for (const clause of clauses) {
650
+ writer.newline(base);
651
+ writer.indent = base;
652
+ if (isWord(clause[0], "ON") && spells(clause[1], "DUPLICATE")) {
653
+ const keywords = clause.slice(0, 4);
654
+ if (!keywords.every((node) => node.type === "tok") || !spells(clause[3], "UPDATE")) {
655
+ throw new Unsupported("an ON DUPLICATE KEY without UPDATE");
656
+ }
657
+ for (const keyword of keywords)
658
+ keyword.keyword = true;
659
+ this.#keywords(writer, keywords);
660
+ this.assignments(writer, clause.slice(4), writer.column + 1);
661
+ }
662
+ else {
663
+ this.inline(writer, clause, false);
664
+ }
665
+ }
666
+ }
667
+ #rows(writer, nodes, column) {
668
+ const { parts, commas } = splitTop(nodes, COMMA);
669
+ const saved = writer.indent;
670
+ writer.indent = column;
671
+ parts.forEach((part, at) => {
672
+ if (at > 0)
673
+ writer.newline(column);
674
+ this.inline(writer, part, at === 0);
675
+ const comma = commas[at];
676
+ if (comma !== undefined)
677
+ writer.tok(comma, comma.text, false);
678
+ });
679
+ writer.indent = saved;
680
+ }
681
+ update(writer, nodes, base) {
682
+ const { head, clauses } = splitClauses(nodes, updateClauses);
683
+ if (head.length > 0)
684
+ throw new Unsupported("tokens before UPDATE");
685
+ clauses.forEach((clause, at) => {
686
+ if (at > 0)
687
+ writer.newline(base);
688
+ writer.indent = base;
689
+ this.clause(writer, clause);
690
+ });
691
+ }
692
+ delete(writer, nodes, base) {
693
+ const { head, clauses } = splitClauses(nodes, deleteClauses);
694
+ if (head.length > 0)
695
+ throw new Unsupported("tokens before DELETE");
696
+ const [verb, from] = clauses;
697
+ const sameLine = from?.name === "FROM" &&
698
+ (verb?.body.every((node) => node.type === "tok" && node.kind === "word" && DELETE_MODIFIERS.has(node.upper)) ??
699
+ false);
700
+ clauses.forEach((clause, at) => {
701
+ if (at === 1 && sameLine) {
702
+ this.#keywords(writer, clause.keywords, true);
703
+ this.fromItems(writer, clause.body, writer.column + 1);
704
+ return;
705
+ }
706
+ if (at > 0)
707
+ writer.newline(base);
708
+ writer.indent = base;
709
+ this.clause(writer, clause);
710
+ });
711
+ }
712
+ inline(writer, nodes, leadSpace, options = {}) {
713
+ let previous = null;
714
+ for (const node of nodes) {
715
+ const space = previous === null ? leadSpace : this.#spaceBetween(previous, node, options);
716
+ this.node(writer, node, space, options);
717
+ previous = node;
718
+ }
719
+ }
720
+ node(writer, node, space, options = {}) {
721
+ if (node.type === "tok")
722
+ writer.tok(node, this.text(node), space);
723
+ else if (node.type === "group")
724
+ this.#group(writer, node, space, options);
725
+ else
726
+ this.#case(writer, node, space);
727
+ }
728
+ #group(writer, group, space, options) {
729
+ writer.tok(group.open, group.open.text, space);
730
+ if (isSubquery(group) && options.preserve !== true) {
731
+ this.#nested(writer, group.children);
732
+ }
733
+ else {
734
+ this.inline(writer, group.children, false, { preserve: options.preserve ?? false });
735
+ }
736
+ writer.tok(group.close, group.close.text, false);
737
+ }
738
+ /** Inline when it fits `CASE_INLINE_LIMIT`; else `WHEN` / `ELSE` one per line under `CASE`, `END` at its column (rule 14). */
739
+ #case(writer, node, space) {
740
+ const start = writer.column + (space ? 1 : 0);
741
+ const measured = this.measure(start, (scratch) => {
742
+ this.#caseInline(scratch, node, false);
743
+ });
744
+ if (!measured.multiline && measured.width <= CASE_INLINE_LIMIT) {
745
+ this.#caseInline(writer, node, space);
746
+ return;
747
+ }
748
+ const headText = this.text(node.head);
749
+ writer.tok(node.head, headText, space);
750
+ const column = writer.column - headText.length;
751
+ const branches = [[]];
752
+ for (const child of node.children) {
753
+ if (isWord(child, "WHEN") || isWord(child, "ELSE"))
754
+ branches.push([]);
755
+ branches[branches.length - 1]?.push(child);
756
+ }
757
+ const [operand = [], ...rest] = branches;
758
+ this.inline(writer, operand, true);
759
+ const saved = writer.indent;
760
+ const inner = column + this.options.indentWidth;
761
+ for (const branch of rest) {
762
+ writer.newline(inner);
763
+ writer.indent = inner;
764
+ this.inline(writer, branch, false);
765
+ }
766
+ writer.newline(column);
767
+ writer.indent = saved;
768
+ writer.tok(node.end, this.text(node.end), false);
769
+ }
770
+ #caseInline(writer, node, space) {
771
+ writer.tok(node.head, this.text(node.head), space);
772
+ this.inline(writer, node.children, true);
773
+ writer.tok(node.end, this.text(node.end), true);
774
+ }
775
+ #spaceBetween(previous, current, options) {
776
+ const before = lastTok(previous);
777
+ const after = firstTok(current);
778
+ if (options.preserve === true)
779
+ return after.spaced;
780
+ if (after.text === COMMA)
781
+ return false;
782
+ if (before.text === COMMA)
783
+ return true;
784
+ if (before.text === CAST || after.text === CAST)
785
+ return false;
786
+ if (current.type === "group") {
787
+ if (before.kind === "word") {
788
+ if (before.keyword)
789
+ return true;
790
+ return options.spaceBeforeGroup === true && previous.type === "tok";
791
+ }
792
+ if (before.kind === "quoted" && options.spaceBeforeGroup === true)
793
+ return true;
794
+ if (isSpacedOperator(before))
795
+ return true;
796
+ return after.spaced;
797
+ }
798
+ if (isSpacedOperator(before) || isSpacedOperator(after))
799
+ return true;
800
+ if (before.kind === "word" && after.kind === "word")
801
+ return true;
802
+ if (previous.type !== "tok" && after.kind === "word")
803
+ return true;
804
+ return after.spaced;
805
+ }
806
+ }