@deckbuilderkit/editor 0.0.0-stage → 0.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 (56) hide show
  1. package/package.json +53 -4
  2. package/src/ArchiveView.tsx +111 -0
  3. package/src/CodeEditor.tsx +265 -0
  4. package/src/CodeView.tsx +67 -0
  5. package/src/DiffView.tsx +139 -0
  6. package/src/EnvView.tsx +42 -0
  7. package/src/FilePreview.tsx +108 -0
  8. package/src/FontView.tsx +48 -0
  9. package/src/HarView.tsx +70 -0
  10. package/src/HexView.tsx +28 -0
  11. package/src/HtmlPreview.tsx +23 -0
  12. package/src/MarkdownEditor.tsx +90 -0
  13. package/src/MarkdownToc.tsx +135 -0
  14. package/src/MarkdownView.tsx +84 -0
  15. package/src/MediaView.tsx +17 -0
  16. package/src/NotebookView.tsx +74 -0
  17. package/src/PdfView.tsx +289 -0
  18. package/src/RequestEditor.tsx +419 -0
  19. package/src/ResponseView.tsx +89 -0
  20. package/src/SqlGraph.tsx +322 -0
  21. package/src/SqlSteps.tsx +487 -0
  22. package/src/SqliteView.tsx +68 -0
  23. package/src/SvgView.tsx +231 -0
  24. package/src/TableView.tsx +85 -0
  25. package/src/VarSuggest.tsx +125 -0
  26. package/src/curl-file.ts +70 -0
  27. package/src/editorOptions.ts +83 -0
  28. package/src/editorOptionsContext.ts +4 -0
  29. package/src/fileKinds.ts +69 -0
  30. package/src/http-client.ts +9 -0
  31. package/src/http-import.ts +594 -0
  32. package/src/http.ts +251 -0
  33. package/src/import-warning.ts +8 -0
  34. package/src/index.ts +56 -0
  35. package/src/languages.ts +107 -0
  36. package/src/markdown.ts +123 -0
  37. package/src/markdownLive.ts +289 -0
  38. package/src/options.ts +3 -0
  39. package/src/pdf-url.d.ts +4 -0
  40. package/src/sql/format.ts +637 -0
  41. package/src/sql/formatOptions.ts +71 -0
  42. package/src/sql/lineage.ts +365 -0
  43. package/src/sql/lint.ts +951 -0
  44. package/src/sql/markerHighlight.ts +50 -0
  45. package/src/sql/markers.ts +138 -0
  46. package/src/sql/outline.ts +662 -0
  47. package/src/sql/statements.ts +83 -0
  48. package/src/sql/tokens.ts +156 -0
  49. package/src/sql/xmlQuery.ts +249 -0
  50. package/src/sqlProblems.ts +40 -0
  51. package/src/table.ts +89 -0
  52. package/src/text-save.ts +28 -0
  53. package/src/theme.ts +86 -0
  54. package/src/toc.ts +11 -0
  55. package/src/variables.ts +123 -0
  56. package/README.md +0 -3
@@ -0,0 +1,662 @@
1
+ import { sqlMarkers } from './markers';
2
+ import { isPlsqlStart, splitStatements } from './statements.ts';
3
+ import { tokenize, type Token } from './tokens.ts';
4
+
5
+ export type SqlStatementKind = 'select' | 'insert' | 'update' | 'delete' | 'merge' | 'create' | 'plsql' | 'other';
6
+ export type SqlJoinType = 'inner' | 'left' | 'right' | 'full' | 'cross' | 'comma' | 'apply';
7
+ export type SqlSetOp = 'union' | 'union all' | 'intersect' | 'minus' | 'except';
8
+
9
+ export interface SqlRead {
10
+ name: string;
11
+ alias?: string;
12
+ cte: boolean;
13
+ from: number;
14
+ to: number;
15
+ }
16
+
17
+ export interface SqlJoin {
18
+ type: SqlJoinType;
19
+ target: string;
20
+ alias?: string;
21
+ on?: string;
22
+ }
23
+
24
+ export interface SqlStep {
25
+ /** CTE name, or '' for the main query. */
26
+ name: string;
27
+ kind: 'cte' | 'main';
28
+ from: number;
29
+ to: number;
30
+ nameFrom?: number;
31
+ nameTo?: number;
32
+ reads: SqlRead[];
33
+ joins: SqlJoin[];
34
+ filters: string[];
35
+ groupBy: string[];
36
+ aggregates: string[];
37
+ having?: string;
38
+ distinct: boolean;
39
+ windows: string[];
40
+ dedupe: boolean;
41
+ setOps: SqlSetOp[];
42
+ orderBy: string[];
43
+ limit?: string;
44
+ subqueries: number;
45
+ /** Names of later steps ('main' for the main query) that read this CTE. */
46
+ feeds: string[];
47
+ /** Sorted parameter numbers guarded by `/*%JS=...%*\/` markers inside this step. */
48
+ optional: number[];
49
+ }
50
+
51
+ export interface SqlStatementOutline {
52
+ from: number;
53
+ to: number;
54
+ kind: SqlStatementKind;
55
+ target?: string;
56
+ steps: SqlStep[];
57
+ }
58
+
59
+ export interface SqlOutline {
60
+ statements: SqlStatementOutline[];
61
+ /** Distinct base tables read anywhere, as first written. */
62
+ tables: string[];
63
+ }
64
+
65
+ const TRIVIA = new Set(['whitespace', 'newline', 'lineComment', 'blockComment', 'slash']);
66
+ const MAX = 120;
67
+ const AGGREGATES = new Set([
68
+ 'sum', 'count', 'avg', 'min', 'max', 'listagg', 'stddev', 'variance', 'median', 'string_agg', 'array_agg',
69
+ 'collect', 'xmlagg', 'json_arrayagg', 'json_objectagg', 'group_concat', 'stats_mode', 'approx_count_distinct',
70
+ ]);
71
+ const RANKING = new Set(['row_number', 'rank', 'dense_rank']);
72
+ const SET_OPS = new Set(['union', 'intersect', 'minus', 'except']);
73
+ const JOIN_WORDS = new Set(['inner', 'left', 'right', 'full', 'cross', 'natural', 'outer', 'join', 'apply']);
74
+ const OTHER_BOUNDARY = new Set([
75
+ 'set', 'values', 'returning', 'into', 'for', 'model', 'pivot', 'unpivot', 'insert', 'update', 'delete', 'when',
76
+ 'then', 'merge',
77
+ ]);
78
+ export const RESERVED: ReadonlySet<string> = new Set([
79
+ 'select', 'from', 'where', 'group', 'order', 'having', 'union', 'intersect', 'minus', 'except', 'fetch', 'offset',
80
+ 'limit', 'connect', 'start', 'join', 'inner', 'left', 'right', 'full', 'cross', 'natural', 'outer', 'apply', 'on',
81
+ 'using', 'as', 'with', 'lateral', 'partition', 'pivot', 'unpivot', 'sample', 'model', 'for', 'when', 'then', 'set',
82
+ 'into', 'values', 'returning', 'and', 'or', 'not', 'case', 'end', 'is', 'in', 'by', 'null', 'over', 'window',
83
+ 'distinct', 'unique', 'all', 'cycle', 'search', 'insert', 'update', 'delete', 'merge',
84
+ ]);
85
+ const QUERY_START = new Set(['select', 'with']);
86
+
87
+ export interface Ctx {
88
+ ts: Token[];
89
+ match: Int32Array;
90
+ ctes: Set<string>;
91
+ }
92
+
93
+ interface Extra {
94
+ rank: boolean;
95
+ aliases: Set<string>;
96
+ }
97
+
98
+ type SecKind = 'where' | 'mergeOn' | 'group' | 'order' | 'having' | 'on' | 'limit';
99
+ interface Sec {
100
+ kind: SecKind;
101
+ start: number;
102
+ splits: number[];
103
+ }
104
+
105
+ const isWordTok = (t: Token | undefined): boolean => t !== undefined && (t.kind === 'keyword' || t.kind === 'identifier');
106
+ export const word = (ctx: Ctx, i: number): string => {
107
+ const t = ctx.ts[i];
108
+ return t !== undefined && isWordTok(t) ? t.text.toLowerCase() : '';
109
+ };
110
+ export const isPunct = (ctx: Ctx, i: number, c: string): boolean => {
111
+ const t = ctx.ts[i];
112
+ return t !== undefined && t.kind === 'punctuation' && t.text === c;
113
+ };
114
+ export const nameKey = (name: string): string => name.replace(/"/g, '').toLowerCase();
115
+ const cap = (s: string): string => (s.length > MAX ? s.slice(0, MAX - 1).trimEnd() + '…' : s);
116
+
117
+ function textOf(ctx: Ctx, a: number, b: number): string {
118
+ let out = '';
119
+ for (let k = a; k < b && k < ctx.ts.length; k++) {
120
+ const t = ctx.ts[k]!;
121
+ if (k > a && t.from > ctx.ts[k - 1]!.to) out += ' ';
122
+ out += t.text.replace(/\s+/g, ' ');
123
+ if (out.length > MAX) break;
124
+ }
125
+ return cap(out.trim());
126
+ }
127
+
128
+ function closeOf(ctx: Ctx, i: number, b: number): number {
129
+ const m = ctx.match[i] ?? -1;
130
+ return m >= 0 && m < b ? m : b;
131
+ }
132
+
133
+ export function isNameTok(ctx: Ctx, i: number, b: number): boolean {
134
+ if (i >= b) return false;
135
+ const t = ctx.ts[i]!;
136
+ if (t.kind === 'quotedIdentifier') return true;
137
+ return isWordTok(t) && !RESERVED.has(t.text.toLowerCase());
138
+ }
139
+
140
+ const isPartTok = (ctx: Ctx, i: number, b: number): boolean =>
141
+ i < b && (isWordTok(ctx.ts[i]) || ctx.ts[i]!.kind === 'quotedIdentifier');
142
+
143
+ export function parseName(ctx: Ctx, i: number, b: number): { name: string; from: number; to: number; next: number } | null {
144
+ if (!isNameTok(ctx, i, b)) return null;
145
+ let j = i;
146
+ while (isPunct(ctx, j + 1, '.') && isPartTok(ctx, j + 2, b)) j += 2;
147
+ if (isPunct(ctx, j + 1, '@') && isPartTok(ctx, j + 2, b)) {
148
+ j += 2;
149
+ while (isPunct(ctx, j + 1, '.') && isPartTok(ctx, j + 2, b)) j += 2;
150
+ }
151
+ let name = '';
152
+ for (let k = i; k <= j; k++) name += ctx.ts[k]!.text;
153
+ return { name, from: ctx.ts[i]!.from, to: ctx.ts[j]!.to, next: j + 1 };
154
+ }
155
+
156
+ export function parseAlias(ctx: Ctx, k: number, b: number): { alias?: string; next: number } {
157
+ if (word(ctx, k) === 'as') {
158
+ return isNameTok(ctx, k + 1, b) ? { alias: ctx.ts[k + 1]!.text, next: k + 2 } : { next: k + 1 };
159
+ }
160
+ return isNameTok(ctx, k, b) ? { alias: ctx.ts[k]!.text, next: k + 1 } : { next: k };
161
+ }
162
+
163
+ function newStep(name: string, kind: 'cte' | 'main', from: number, to: number): SqlStep {
164
+ return {
165
+ name, kind, from, to, reads: [], joins: [], filters: [], groupBy: [], aggregates: [], distinct: false,
166
+ windows: [], dedupe: false, setOps: [], orderBy: [], subqueries: 0, feeds: [], optional: [],
167
+ };
168
+ }
169
+
170
+ function analyze(ctx: Ctx, a: number, b: number, step: SqlStep, extra: Extra, startClause = 'select'): void {
171
+ let clause = startClause;
172
+ let sec: Sec | null = null;
173
+ let d = 0;
174
+ let caseDepth = 0;
175
+ let between = false;
176
+ let expectItem = false;
177
+ let joinPending: SqlJoinType | null = null;
178
+ let joinWords: string[] = [];
179
+ let lastJoin: SqlJoin | null = null;
180
+
181
+ const pieces = (s: Sec, end: number): string[] => {
182
+ const out: string[] = [];
183
+ let from = s.start;
184
+ for (const sp of [...s.splits, end]) {
185
+ if (sp > from) {
186
+ const p = textOf(ctx, from, sp);
187
+ if (p) out.push(p);
188
+ }
189
+ from = sp + 1;
190
+ }
191
+ return out;
192
+ };
193
+
194
+ const close = (end: number): void => {
195
+ if (!sec) return;
196
+ const s = sec;
197
+ sec = null;
198
+ between = false;
199
+ if (s.kind === 'where' || s.kind === 'mergeOn') {
200
+ for (const p of pieces(s, end)) {
201
+ step.filters.push(p);
202
+ if (step.limit === undefined && /\brownum\b/i.test(p)) step.limit = p;
203
+ }
204
+ } else if (s.kind === 'group') step.groupBy.push(...pieces(s, end));
205
+ else if (s.kind === 'order') step.orderBy.push(...pieces(s, end));
206
+ else {
207
+ const t = textOf(ctx, s.start, end);
208
+ if (!t) return;
209
+ if (s.kind === 'having') step.having = t;
210
+ else if (s.kind === 'limit') step.limit = t;
211
+ else if (lastJoin) lastJoin.on = t;
212
+ }
213
+ };
214
+
215
+ const open = (kind: SecKind, at: number, start: number): void => {
216
+ close(at);
217
+ sec = { kind, start, splits: [] };
218
+ };
219
+
220
+ const addItem = (name: string, alias: string | undefined, from: number, to: number, base: boolean): void => {
221
+ if (base) {
222
+ const cte = !/[.@]/.test(name.replace(/"[^"]*"/g, 'x')) && ctx.ctes.has(nameKey(name));
223
+ const read: SqlRead = { name, cte, from, to };
224
+ if (alias) read.alias = alias;
225
+ step.reads.push(read);
226
+ }
227
+ if (joinPending) {
228
+ const j: SqlJoin = { type: joinPending, target: name };
229
+ if (alias) j.alias = alias;
230
+ step.joins.push(j);
231
+ lastJoin = j;
232
+ } else lastJoin = null;
233
+ joinPending = null;
234
+ joinWords = [];
235
+ expectItem = false;
236
+ };
237
+
238
+ const inner = (from: number, to: number): void => {
239
+ const sub = newStep('', 'main', 0, 0);
240
+ const subExtra: Extra = { rank: false, aliases: new Set() };
241
+ analyze(ctx, from, to, sub, subExtra);
242
+ step.reads.push(...sub.reads);
243
+ step.windows.push(...sub.windows);
244
+ step.subqueries += sub.subqueries;
245
+ if (subExtra.rank) extra.rank = true;
246
+ for (const x of subExtra.aliases) extra.aliases.add(x);
247
+ };
248
+
249
+ for (let i = a; i < b; i++) {
250
+ const t = ctx.ts[i]!;
251
+ if (t.kind === 'punctuation' && t.text === '(') {
252
+ const c = closeOf(ctx, i, b);
253
+ if (QUERY_START.has(word(ctx, i + 1))) {
254
+ step.subqueries++;
255
+ inner(i + 1, c);
256
+ if (expectItem) {
257
+ const al = parseAlias(ctx, c + 1, b);
258
+ addItem('(subquery)', al.alias, 0, 0, false);
259
+ i = al.next - 1;
260
+ } else i = c;
261
+ continue;
262
+ }
263
+ d++;
264
+ continue;
265
+ }
266
+ if (t.kind === 'punctuation' && t.text === ')') {
267
+ if (d > 0) d--;
268
+ continue;
269
+ }
270
+ const lw = word(ctx, i);
271
+ if (expectItem) {
272
+ if (lw === 'lateral' || lw === 'only') continue;
273
+ const n = parseName(ctx, i, b);
274
+ if (n) {
275
+ if (isPunct(ctx, n.next, '(')) {
276
+ const c = closeOf(ctx, n.next, b);
277
+ const al = parseAlias(ctx, c + 1, b);
278
+ addItem(textOf(ctx, i, c + 1), al.alias, 0, 0, false);
279
+ i = al.next - 1;
280
+ continue;
281
+ }
282
+ const al = parseAlias(ctx, n.next, b);
283
+ addItem(n.name, al.alias, n.from, n.to, true);
284
+ i = al.next - 1;
285
+ continue;
286
+ }
287
+ expectItem = false;
288
+ }
289
+ if (lw && isPunct(ctx, i + 1, '(')) {
290
+ const c = closeOf(ctx, i + 1, b);
291
+ if (word(ctx, c + 1) === 'over') {
292
+ let end = c + 2;
293
+ if (isPunct(ctx, c + 2, '(')) end = closeOf(ctx, c + 2, b) + 1;
294
+ else if (c + 2 < b) end = c + 3;
295
+ end = Math.min(end, b);
296
+ step.windows.push(textOf(ctx, i, end));
297
+ if (RANKING.has(lw)) {
298
+ extra.rank = true;
299
+ const al = parseAlias(ctx, end, b);
300
+ if (al.alias) extra.aliases.add(nameKey(al.alias));
301
+ }
302
+ i = end - 1;
303
+ continue;
304
+ }
305
+ if (AGGREGATES.has(lw)) {
306
+ const agg = textOf(ctx, i, Math.min(c + 1, b));
307
+ if (!step.aggregates.includes(agg)) step.aggregates.push(agg);
308
+ i = c;
309
+ continue;
310
+ }
311
+ }
312
+ if (d > 0) continue;
313
+ if (lw === 'case') {
314
+ caseDepth++;
315
+ continue;
316
+ }
317
+ if (caseDepth > 0) {
318
+ if (lw === 'end') caseDepth--;
319
+ continue;
320
+ }
321
+ if (t.kind === 'punctuation' && t.text === ',') {
322
+ if (sec !== null && ((sec as Sec).kind === 'group' || (sec as Sec).kind === 'order')) (sec as Sec).splits.push(i);
323
+ else if (clause === 'from') {
324
+ close(i);
325
+ joinPending = 'comma';
326
+ expectItem = true;
327
+ }
328
+ continue;
329
+ }
330
+ if (!lw) continue;
331
+ if (lw === 'and') {
332
+ const s = sec as Sec | null;
333
+ if (s && (s.kind === 'where' || s.kind === 'mergeOn')) {
334
+ if (between) between = false;
335
+ else s.splits.push(i);
336
+ }
337
+ continue;
338
+ }
339
+ if (lw === 'between') {
340
+ between = true;
341
+ continue;
342
+ }
343
+ if (lw === 'select') {
344
+ close(i);
345
+ clause = 'select';
346
+ const nx = word(ctx, i + 1);
347
+ if (nx === 'distinct' || nx === 'unique') {
348
+ step.distinct = true;
349
+ i++;
350
+ }
351
+ continue;
352
+ }
353
+ if (lw === 'from') {
354
+ close(i);
355
+ clause = 'from';
356
+ expectItem = true;
357
+ joinPending = null;
358
+ lastJoin = null;
359
+ continue;
360
+ }
361
+ if (lw === 'where') {
362
+ open('where', i, i + 1);
363
+ clause = 'where';
364
+ continue;
365
+ }
366
+ if (lw === 'group' || lw === 'order') {
367
+ let k = i + 1;
368
+ if (word(ctx, k) === 'siblings') k++;
369
+ if (word(ctx, k) === 'by') {
370
+ open(lw, i, k + 1);
371
+ clause = lw;
372
+ i = k;
373
+ continue;
374
+ }
375
+ }
376
+ if (lw === 'having') {
377
+ open('having', i, i + 1);
378
+ clause = 'having';
379
+ continue;
380
+ }
381
+ if (SET_OPS.has(lw)) {
382
+ close(i);
383
+ const nx = word(ctx, i + 1);
384
+ let op = lw as SqlSetOp;
385
+ if (lw === 'union' && nx === 'all') {
386
+ op = 'union all';
387
+ i++;
388
+ } else if (nx === 'all' || nx === 'distinct') i++;
389
+ step.setOps.push(op);
390
+ clause = 'other';
391
+ continue;
392
+ }
393
+ if (lw === 'fetch' || lw === 'limit' || lw === 'offset') {
394
+ if ((sec as Sec | null)?.kind !== 'limit') open('limit', i, i);
395
+ clause = 'limit';
396
+ continue;
397
+ }
398
+ if ((lw === 'connect' && word(ctx, i + 1) === 'by') || (lw === 'start' && word(ctx, i + 1) === 'with')) {
399
+ close(i);
400
+ clause = 'other';
401
+ i++;
402
+ continue;
403
+ }
404
+ if (clause === 'from' && JOIN_WORDS.has(lw) && !isPunct(ctx, i + 1, '(')) {
405
+ close(i);
406
+ if (lw === 'join' || lw === 'apply') {
407
+ joinPending =
408
+ lw === 'apply' ? 'apply'
409
+ : joinWords.includes('left') ? 'left'
410
+ : joinWords.includes('right') ? 'right'
411
+ : joinWords.includes('full') ? 'full'
412
+ : joinWords.includes('cross') ? 'cross'
413
+ : 'inner';
414
+ joinWords = [];
415
+ expectItem = true;
416
+ } else joinWords.push(lw);
417
+ continue;
418
+ }
419
+ if (lw === 'on' && clause === 'from') {
420
+ open(lastJoin ? 'on' : 'mergeOn', i, i + 1);
421
+ continue;
422
+ }
423
+ if (lw === 'using') {
424
+ if (clause === 'from' && lastJoin) open('on', i, i);
425
+ else {
426
+ close(i);
427
+ clause = 'from';
428
+ expectItem = true;
429
+ joinPending = null;
430
+ lastJoin = null;
431
+ }
432
+ continue;
433
+ }
434
+ if (OTHER_BOUNDARY.has(lw)) {
435
+ close(i);
436
+ clause = 'other';
437
+ }
438
+ }
439
+ close(b);
440
+ }
441
+
442
+ function findQuery(ctx: Ctx, j: number, b: number): number {
443
+ let d = 0;
444
+ for (let i = j; i < b; i++) {
445
+ if (isPunct(ctx, i, '(')) {
446
+ if (d === 0 && QUERY_START.has(word(ctx, i + 1))) return i + 1;
447
+ d++;
448
+ } else if (isPunct(ctx, i, ')')) {
449
+ if (d > 0) d--;
450
+ } else if (d === 0 && QUERY_START.has(word(ctx, i))) return i;
451
+ }
452
+ return -1;
453
+ }
454
+
455
+ const FIRST_ROW = [
456
+ /^(?:[^\s.]+\.)?([^\s.=<>]+)\s*(?:=|<=)\s*1$/,
457
+ /^(?:[^\s.]+\.)?([^\s.=<>]+)\s*<\s*2$/,
458
+ /^1\s*(?:=|>=)\s*(?:[^\s.]+\.)?([^\s.=<>]+)$/,
459
+ ];
460
+
461
+ function isFirstRowFilter(f: string, aliases: Set<string>): boolean {
462
+ for (const re of FIRST_ROW) {
463
+ const m = re.exec(f);
464
+ if (m) return aliases.size === 0 || aliases.has(nameKey(m[1]!));
465
+ }
466
+ return false;
467
+ }
468
+
469
+ function outlineStatement(ctx: Ctx, from: number, to: number, plsql: boolean): SqlStatementOutline {
470
+ const out: SqlStatementOutline = { from, to, kind: 'other', steps: [] };
471
+ const n = ctx.ts.length;
472
+ if (plsql) {
473
+ out.kind = 'plsql';
474
+ return out;
475
+ }
476
+ if (n === 0) return out;
477
+ const steps: SqlStep[] = [];
478
+ const extras = new Map<SqlStep, Extra>();
479
+
480
+ const main = (a: number, fromIdx: number, clause: string): void => {
481
+ if (a < 0 || a >= n) return;
482
+ const step = newStep('', 'main', ctx.ts[fromIdx]!.from, ctx.ts[n - 1]!.to);
483
+ const extra: Extra = { rank: false, aliases: new Set() };
484
+ analyze(ctx, a, n, step, extra, clause);
485
+ steps.push(step);
486
+ extras.set(step, extra);
487
+ };
488
+
489
+ const withList = (i: number): number => {
490
+ let j = i + 1;
491
+ if (word(ctx, j) === 'recursive') j++;
492
+ while (j < n) {
493
+ if (!isPartTok(ctx, j, n) || RESERVED.has(word(ctx, j)) || word(ctx, j) === 'function' || word(ctx, j) === 'procedure') break;
494
+ const nameIdx = j;
495
+ j++;
496
+ if (isPunct(ctx, j, '(')) j = closeOf(ctx, j, n) + 1;
497
+ if (word(ctx, j) !== 'as') break;
498
+ j++;
499
+ if (word(ctx, j) === 'not') j++;
500
+ if (word(ctx, j) === 'materialized') j++;
501
+ if (!isPunct(ctx, j, '(')) break;
502
+ const c = closeOf(ctx, j, n);
503
+ const nameTok = ctx.ts[nameIdx]!;
504
+ ctx.ctes.add(nameKey(nameTok.text));
505
+ const step = newStep(nameTok.text, 'cte', nameTok.from, (ctx.ts[c] ?? ctx.ts[n - 1]!).to);
506
+ step.nameFrom = nameTok.from;
507
+ step.nameTo = nameTok.to;
508
+ const extra: Extra = { rank: false, aliases: new Set() };
509
+ analyze(ctx, j + 1, c, step, extra);
510
+ steps.push(step);
511
+ extras.set(step, extra);
512
+ j = c + 1;
513
+ while (j < n && !isPunct(ctx, j, ',') && !isPunct(ctx, j, '(') && !QUERY_START.has(word(ctx, j)) &&
514
+ !['insert', 'update', 'delete', 'merge'].includes(word(ctx, j))) j++;
515
+ if (isPunct(ctx, j, ',')) {
516
+ j++;
517
+ continue;
518
+ }
519
+ break;
520
+ }
521
+ return j;
522
+ };
523
+
524
+ const query = (q: number): void => {
525
+ if (q < 0) return;
526
+ const start = word(ctx, q) === 'with' ? withList(q) : q;
527
+ main(start, q, 'select');
528
+ };
529
+
530
+ let i = 0;
531
+ if (word(ctx, 0) === 'with') i = withList(0);
532
+ const w = word(ctx, i);
533
+ if (w === 'select' || isPunct(ctx, i, '(')) {
534
+ out.kind = 'select';
535
+ main(i, i, 'select');
536
+ } else if (w === 'insert') {
537
+ out.kind = 'insert';
538
+ let j = i + 1;
539
+ if (word(ctx, j) === 'into') j++;
540
+ const t = word(ctx, j) === 'all' || word(ctx, j) === 'first' ? null : parseName(ctx, j, n);
541
+ if (t) out.target = t.name;
542
+ query(findQuery(ctx, t ? t.next : j, n));
543
+ } else if (w === 'update' || w === 'delete' || w === 'merge') {
544
+ out.kind = w;
545
+ let j = i + 1;
546
+ if ((w === 'delete' && word(ctx, j) === 'from') || (w === 'merge' && word(ctx, j) === 'into')) j++;
547
+ const t = parseName(ctx, j, n);
548
+ if (t) out.target = t.name;
549
+ main(t ? t.next : j, i, 'other');
550
+ } else if (w === 'create') {
551
+ out.kind = 'create';
552
+ for (let j = i + 1; j < Math.min(n, i + 10); j++) {
553
+ const k = word(ctx, j);
554
+ if (k === 'table' || k === 'view' || k === 'index' || k === 'sequence' || k === 'synonym') {
555
+ const t = parseName(ctx, j + 1, n);
556
+ if (t) out.target = t.name;
557
+ if ((k === 'table' || k === 'view') && t) query(findQuery(ctx, t.next, n));
558
+ break;
559
+ }
560
+ }
561
+ }
562
+
563
+ for (const s of steps) {
564
+ if (s.kind !== 'cte') continue;
565
+ const key = nameKey(s.name);
566
+ for (const o of steps) {
567
+ if (o === s || !o.reads.some((r) => r.cte && nameKey(r.name) === key)) continue;
568
+ const label = o.kind === 'main' ? 'main' : o.name;
569
+ if (!s.feeds.includes(label)) s.feeds.push(label);
570
+ }
571
+ }
572
+ for (const s of steps) {
573
+ const ex = extras.get(s)!;
574
+ if (!ex.rank) continue;
575
+ const cands = [...s.filters];
576
+ if (s.kind === 'cte') {
577
+ const key = nameKey(s.name);
578
+ for (const o of steps) if (o !== s && o.reads.some((r) => r.cte && nameKey(r.name) === key)) cands.push(...o.filters);
579
+ }
580
+ s.dedupe = cands.some((f) => isFirstRowFilter(f, ex.aliases));
581
+ }
582
+ out.steps = steps;
583
+ return out;
584
+ }
585
+
586
+ export function matchParens(ts: Token[]): Int32Array {
587
+ const match = new Int32Array(ts.length).fill(-1);
588
+ const stack: number[] = [];
589
+ for (let i = 0; i < ts.length; i++) {
590
+ const t = ts[i]!;
591
+ if (t.kind !== 'punctuation') continue;
592
+ if (t.text === '(') stack.push(i);
593
+ else if (t.text === ')' && stack.length) {
594
+ const o = stack.pop()!;
595
+ match[o] = i;
596
+ match[i] = o;
597
+ }
598
+ }
599
+ return match;
600
+ }
601
+
602
+ function assignOptional(steps: SqlStep[], markers: ReturnType<typeof sqlMarkers>): void {
603
+ for (const m of markers) {
604
+ let own: SqlStep | undefined;
605
+ for (const st of steps) {
606
+ if (m.from < st.from || m.from >= st.to) continue;
607
+ if (!own || st.to - st.from < own.to - own.from) own = st;
608
+ }
609
+ if (!own) continue;
610
+ for (const n of m.params) if (!own.optional.includes(n)) own.optional.push(n);
611
+ }
612
+ for (const st of steps) st.optional.sort((a, b) => a - b);
613
+ }
614
+
615
+ /** Explains SQL text as statements made of ordered steps (CTEs, then the main query). Never throws. */
616
+ export function sqlOutline(text: string): SqlOutline {
617
+ const statements: SqlStatementOutline[] = [];
618
+ const tables: string[] = [];
619
+ let all: Token[];
620
+ let parts: ReturnType<typeof splitStatements>;
621
+ try {
622
+ all = tokenize(text);
623
+ parts = splitStatements(text);
624
+ } catch {
625
+ return { statements, tables };
626
+ }
627
+ const seen = new Set<string>();
628
+ const markers = sqlMarkers(text);
629
+ let k = 0;
630
+ for (const s of parts) {
631
+ while (k < all.length && all[k]!.from < s.from) k++;
632
+ const first = k;
633
+ const ts: Token[] = [];
634
+ while (k < all.length && all[k]!.to <= s.to) {
635
+ if (!TRIVIA.has(all[k]!.kind)) ts.push(all[k]!);
636
+ k++;
637
+ }
638
+ let outline: SqlStatementOutline;
639
+ try {
640
+ const ctx: Ctx = { ts, match: matchParens(ts), ctes: new Set() };
641
+ outline = outlineStatement(ctx, s.from, s.to, isPlsqlStart(all, first));
642
+ } catch {
643
+ outline = { from: s.from, to: s.to, kind: 'other', steps: [] };
644
+ }
645
+ statements.push(outline);
646
+ try {
647
+ assignOptional(outline.steps, markers);
648
+ } catch {
649
+ // markers are optional extras
650
+ }
651
+ for (const step of outline.steps) {
652
+ for (const r of step.reads) {
653
+ if (r.cte) continue;
654
+ const key = nameKey(r.name);
655
+ if (seen.has(key)) continue;
656
+ seen.add(key);
657
+ tables.push(r.name);
658
+ }
659
+ }
660
+ }
661
+ return { statements, tables };
662
+ }