querylens 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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/bin/querylens.js +208 -0
  4. package/dist/src/engine/bufferTrace.js +67 -0
  5. package/dist/src/engine/datasets.js +139 -0
  6. package/dist/src/engine/exec/delete.js +95 -0
  7. package/dist/src/engine/exec/evaluate.js +174 -0
  8. package/dist/src/engine/exec/index.js +4 -0
  9. package/dist/src/engine/exec/insert.js +75 -0
  10. package/dist/src/engine/exec/operators.js +1290 -0
  11. package/dist/src/engine/exec/run.js +35 -0
  12. package/dist/src/engine/exec/sort.js +171 -0
  13. package/dist/src/engine/exec/unique.js +79 -0
  14. package/dist/src/engine/exec/update.js +124 -0
  15. package/dist/src/engine/exec/writeScan.js +88 -0
  16. package/dist/src/engine/explain.js +114 -0
  17. package/dist/src/engine/index/btree.js +481 -0
  18. package/dist/src/engine/index/build.js +99 -0
  19. package/dist/src/engine/index/bulk.js +107 -0
  20. package/dist/src/engine/index/display.js +38 -0
  21. package/dist/src/engine/index/index.js +9 -0
  22. package/dist/src/engine/index/lookup.js +213 -0
  23. package/dist/src/engine/index/rangeLookup.js +158 -0
  24. package/dist/src/engine/index/spec.js +47 -0
  25. package/dist/src/engine/index/unique.js +31 -0
  26. package/dist/src/engine/index/validate.js +105 -0
  27. package/dist/src/engine/index.js +16 -0
  28. package/dist/src/engine/locks/index.js +1 -0
  29. package/dist/src/engine/locks/lockManager.js +46 -0
  30. package/dist/src/engine/parser/ast.js +77 -0
  31. package/dist/src/engine/parser/display.js +404 -0
  32. package/dist/src/engine/parser/index.js +4 -0
  33. package/dist/src/engine/parser/parser.js +1108 -0
  34. package/dist/src/engine/parser/print.js +74 -0
  35. package/dist/src/engine/parser/tokenizer.js +146 -0
  36. package/dist/src/engine/planner/buildPlan.js +208 -0
  37. package/dist/src/engine/planner/cost.js +582 -0
  38. package/dist/src/engine/planner/emit.js +267 -0
  39. package/dist/src/engine/planner/emitDelete.js +57 -0
  40. package/dist/src/engine/planner/emitUpdate.js +51 -0
  41. package/dist/src/engine/planner/index.js +8 -0
  42. package/dist/src/engine/planner/joinOrder.js +252 -0
  43. package/dist/src/engine/planner/optimize.js +906 -0
  44. package/dist/src/engine/planner/plan.js +445 -0
  45. package/dist/src/engine/predict.js +120 -0
  46. package/dist/src/engine/runQuery.js +393 -0
  47. package/dist/src/engine/seed.js +165 -0
  48. package/dist/src/engine/stats.js +118 -0
  49. package/dist/src/engine/storage/bufferPool.js +194 -0
  50. package/dist/src/engine/storage/index.js +3 -0
  51. package/dist/src/engine/storage/page.js +46 -0
  52. package/dist/src/engine/storage/policy.js +360 -0
  53. package/dist/src/engine/subquery.js +88 -0
  54. package/dist/src/engine/trace.js +17 -0
  55. package/dist/src/engine/types.js +39 -0
  56. package/dist/src/engine/value.js +80 -0
  57. package/dist/src/engine/viewState.js +187 -0
  58. package/package.json +40 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Page-level locking, visual only (plan.md §7.7).
3
+ *
4
+ * What this models honestly: which pages a query holds a read lock on, and for
5
+ * how long. What it does NOT model: contention. v0 runs exactly one query, so
6
+ * no lock ever waits on another. The panel says so rather than implying that a
7
+ * lock icon means something is being protected from a rival.
8
+ *
9
+ * Real engines also distinguish a short *latch* on the buffer frame from a
10
+ * transaction-length *lock* on the data. We collapse the two into one
11
+ * page-level read lock held for the duration of the read.
12
+ */
13
+ function key(pageId, mode) {
14
+ return `${String(pageId)}:${mode}`;
15
+ }
16
+ export function createLockManager(emit) {
17
+ const holding = new Map();
18
+ const stats = { acquired: 0, released: 0, maxHeldAtOnce: 0 };
19
+ return {
20
+ acquire(pageId, mode, why) {
21
+ const id = key(pageId, mode);
22
+ // Re-entrant on the same page and mode: one query cannot conflict with
23
+ // itself, so this is a no-op rather than a second lock.
24
+ if (holding.has(id))
25
+ return;
26
+ holding.set(id, { pageId, mode });
27
+ stats.acquired += 1;
28
+ stats.maxHeldAtOnce = Math.max(stats.maxHeldAtOnce, holding.size);
29
+ emit(why, { stage: 'lock', action: 'acquire', pageId, mode });
30
+ },
31
+ release(pageId, mode, why) {
32
+ const id = key(pageId, mode);
33
+ if (!holding.delete(id))
34
+ return;
35
+ stats.released += 1;
36
+ emit(why ?? `Release the ${mode} lock on page ${String(pageId)}.`, {
37
+ stage: 'lock',
38
+ action: 'release',
39
+ pageId,
40
+ mode,
41
+ });
42
+ },
43
+ held: () => [...holding.values()],
44
+ stats,
45
+ };
46
+ }
@@ -0,0 +1,77 @@
1
+ import { aggregateKeyOf, exprToSql } from "./print.js";
2
+ export { aggregateKeyOf } from "./print.js";
3
+ /** A qualified reference's row key — `authors.id` — or the bare name when unqualified. */
4
+ export function columnKey(ref) {
5
+ return ref.table ? `${ref.table}.${ref.name}` : ref.name;
6
+ }
7
+ /** The row key an aggregate reads, or `null` for `COUNT(*)`. */
8
+ export function selectItemArgColumn(item) {
9
+ return item.arg.kind === 'star' ? null : columnKey(item.arg);
10
+ }
11
+ /** Canonical display key for a SELECT item — its own name, or e.g. `COUNT(*)`, `SUM(age)`. */
12
+ export function selectItemKey(item) {
13
+ if (item.kind === 'column')
14
+ return columnKey(item);
15
+ if (item.kind === 'expression')
16
+ return item.alias ?? exprToSql(item.expr);
17
+ return aggregateKeyOf(item.fn, item.arg);
18
+ }
19
+ /** The direct sub-expressions of a node, in source order. */
20
+ export function childrenOf(expr) {
21
+ switch (expr.kind) {
22
+ case 'column':
23
+ case 'literal':
24
+ case 'aggregate':
25
+ return [];
26
+ case 'compare':
27
+ case 'and':
28
+ case 'or':
29
+ case 'arith':
30
+ return [expr.left, expr.right];
31
+ case 'not':
32
+ case 'isNull':
33
+ case 'neg':
34
+ return [expr.operand];
35
+ case 'in':
36
+ return [expr.operand, ...expr.items];
37
+ case 'like':
38
+ return [expr.operand, expr.pattern];
39
+ }
40
+ }
41
+ /** `expr` with `f` applied to each direct sub-expression — the rebuild half of `childrenOf`. */
42
+ export function mapChildren(expr, f) {
43
+ switch (expr.kind) {
44
+ case 'column':
45
+ case 'literal':
46
+ case 'aggregate':
47
+ return expr;
48
+ case 'compare':
49
+ case 'and':
50
+ case 'or':
51
+ case 'arith':
52
+ return { ...expr, left: f(expr.left), right: f(expr.right) };
53
+ case 'not':
54
+ case 'isNull':
55
+ case 'neg':
56
+ return { ...expr, operand: f(expr.operand) };
57
+ case 'in':
58
+ return { ...expr, operand: f(expr.operand), items: expr.items.map(f) };
59
+ case 'like':
60
+ return { ...expr, operand: f(expr.operand), pattern: f(expr.pattern) };
61
+ }
62
+ }
63
+ /** Depth-first walk, parents before children. */
64
+ export function walkExpr(expr, visit) {
65
+ visit(expr);
66
+ for (const child of childrenOf(expr))
67
+ walkExpr(child, visit);
68
+ }
69
+ /** Every column name referenced anywhere in a predicate. */
70
+ export function columnsIn(expr) {
71
+ const names = [];
72
+ walkExpr(expr, (e) => {
73
+ if (e.kind === 'column')
74
+ names.push(e.name);
75
+ });
76
+ return names;
77
+ }
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Turns the typed AST into the display tree the UI renders, and emits the
3
+ * parse stage as a progressive reveal: SELECT list, then FROM, then WHERE.
4
+ *
5
+ * Node ids are stable across those steps so the tree animates as it grows
6
+ * instead of remounting on every event.
7
+ */
8
+ import { selectItemKey } from "./ast.js";
9
+ import { aggregateKeyOf, exprToSql } from "./print.js";
10
+ function selectItemNode(item, id) {
11
+ if (item.kind === 'column')
12
+ return { id, kind: 'Column', label: item.name, span: item.span };
13
+ if (item.kind === 'expression') {
14
+ return {
15
+ id,
16
+ kind: 'Expression',
17
+ label: selectItemKey(item),
18
+ ...(item.alias === undefined ? {} : { detail: exprToSql(item.expr) }),
19
+ span: item.span,
20
+ children: [exprNode(item.expr, `${id}.e`)],
21
+ };
22
+ }
23
+ return { id, kind: 'Aggregate', label: selectItemKey(item), span: item.span };
24
+ }
25
+ function selectListNode(select, distinct = false) {
26
+ // `SELECT DISTINCT` (plan.md §25.4 B2) is a property of the whole list, so it is shown on the list's own node.
27
+ const prefix = distinct ? 'DISTINCT · ' : '';
28
+ if (select.kind === 'star') {
29
+ return {
30
+ id: 'select',
31
+ kind: 'SelectList',
32
+ label: distinct ? 'DISTINCT columns' : 'columns',
33
+ detail: `${prefix}every column`,
34
+ span: select.span,
35
+ children: [{ id: 'select.star', kind: 'Star', label: '*', span: select.span }],
36
+ };
37
+ }
38
+ return {
39
+ id: 'select',
40
+ kind: 'SelectList',
41
+ label: distinct ? 'DISTINCT columns' : 'columns',
42
+ detail: `${prefix}${select.items.length} named`,
43
+ span: select.span,
44
+ children: select.items.map((item, i) => selectItemNode(item, `select.col${i}`)),
45
+ };
46
+ }
47
+ function groupByNode(groupBy) {
48
+ return {
49
+ id: 'groupBy',
50
+ kind: 'GroupBy',
51
+ label: 'GROUP BY',
52
+ detail: groupBy.columns.map((c) => c.name).join(', '),
53
+ span: groupBy.span,
54
+ children: groupBy.columns.map((c, i) => ({
55
+ id: `groupBy.col${i}`,
56
+ kind: 'Column',
57
+ label: c.name,
58
+ span: c.span,
59
+ })),
60
+ };
61
+ }
62
+ function fromNode(from) {
63
+ return {
64
+ id: 'from',
65
+ kind: 'From',
66
+ label: 'FROM',
67
+ detail: from.name,
68
+ span: from.span,
69
+ children: [{ id: 'from.table', kind: 'Table', label: from.name, span: from.span }],
70
+ };
71
+ }
72
+ function exprNode(expr, id) {
73
+ switch (expr.kind) {
74
+ case 'column':
75
+ return { id, kind: 'Column', label: expr.name, span: expr.span };
76
+ case 'literal':
77
+ return { id, kind: 'Literal', label: expr.raw, span: expr.span };
78
+ case 'compare':
79
+ return {
80
+ id,
81
+ kind: 'Compare',
82
+ label: expr.op,
83
+ span: expr.span,
84
+ children: [exprNode(expr.left, `${id}.l`), exprNode(expr.right, `${id}.r`)],
85
+ };
86
+ case 'and':
87
+ return {
88
+ id,
89
+ kind: 'And',
90
+ label: 'AND',
91
+ span: expr.span,
92
+ children: [exprNode(expr.left, `${id}.l`), exprNode(expr.right, `${id}.r`)],
93
+ };
94
+ case 'or':
95
+ return {
96
+ id,
97
+ kind: 'Or',
98
+ label: 'OR',
99
+ span: expr.span,
100
+ children: [exprNode(expr.left, `${id}.l`), exprNode(expr.right, `${id}.r`)],
101
+ };
102
+ case 'not':
103
+ return { id, kind: 'Not', label: 'NOT', span: expr.span, children: [exprNode(expr.operand, `${id}.o`)] };
104
+ case 'isNull':
105
+ return {
106
+ id,
107
+ kind: 'IsNull',
108
+ label: expr.negated ? 'IS NOT NULL' : 'IS NULL',
109
+ span: expr.span,
110
+ children: [exprNode(expr.operand, `${id}.o`)],
111
+ };
112
+ case 'in':
113
+ return expr.subquery
114
+ ? {
115
+ id,
116
+ kind: 'In',
117
+ label: expr.negated ? 'NOT IN' : 'IN',
118
+ detail: `subquery — SELECT ${expr.subquery.column.name} FROM ${expr.subquery.from.name}`,
119
+ span: expr.span,
120
+ children: [exprNode(expr.operand, `${id}.o`)],
121
+ }
122
+ : {
123
+ id,
124
+ kind: 'In',
125
+ label: expr.negated ? 'NOT IN' : 'IN',
126
+ detail: `${expr.items.length} value${expr.items.length === 1 ? '' : 's'}`,
127
+ span: expr.span,
128
+ children: [
129
+ exprNode(expr.operand, `${id}.o`),
130
+ ...expr.items.map((item, i) => exprNode(item, `${id}.i${i}`)),
131
+ ],
132
+ };
133
+ case 'aggregate':
134
+ return { id, kind: 'Aggregate', label: aggregateKeyOf(expr.fn, expr.arg), span: expr.span };
135
+ case 'arith':
136
+ return {
137
+ id,
138
+ kind: 'Arith',
139
+ label: expr.op,
140
+ span: expr.span,
141
+ children: [exprNode(expr.left, `${id}.l`), exprNode(expr.right, `${id}.r`)],
142
+ };
143
+ case 'neg':
144
+ return { id, kind: 'Arith', label: '−', detail: 'negate', span: expr.span, children: [exprNode(expr.operand, `${id}.o`)] };
145
+ case 'like':
146
+ return {
147
+ id,
148
+ kind: 'Like',
149
+ label: expr.negated ? 'NOT LIKE' : 'LIKE',
150
+ span: expr.span,
151
+ children: [exprNode(expr.operand, `${id}.o`), exprNode(expr.pattern, `${id}.p`)],
152
+ };
153
+ }
154
+ }
155
+ function whereNode(where) {
156
+ return {
157
+ id: 'where',
158
+ kind: 'Where',
159
+ label: 'WHERE',
160
+ span: where.span,
161
+ children: [exprNode(where, 'where.expr')],
162
+ };
163
+ }
164
+ function havingNode(having) {
165
+ return {
166
+ id: 'having',
167
+ kind: 'Having',
168
+ label: 'HAVING',
169
+ span: having.span,
170
+ children: [exprNode(having, 'having.expr')],
171
+ };
172
+ }
173
+ function orderByNode(orderBy) {
174
+ return {
175
+ id: 'orderBy',
176
+ kind: 'OrderBy',
177
+ label: 'ORDER BY',
178
+ detail: `${orderBy.column} ${orderBy.direction === 'asc' ? 'ASC' : 'DESC'}`,
179
+ span: orderBy.span,
180
+ children: [{ id: 'orderBy.col', kind: 'Column', label: orderBy.column, span: orderBy.span }],
181
+ };
182
+ }
183
+ function limitNode(limit) {
184
+ return {
185
+ id: 'limit',
186
+ kind: 'Limit',
187
+ label: 'LIMIT',
188
+ detail: limit.offset === undefined ? String(limit.value) : `${String(limit.value)} OFFSET ${String(limit.offset)}`,
189
+ span: limit.span,
190
+ };
191
+ }
192
+ function intoNode(into) {
193
+ return {
194
+ id: 'into',
195
+ kind: 'Into',
196
+ label: 'INTO',
197
+ detail: into.name,
198
+ span: into.span,
199
+ children: [{ id: 'into.table', kind: 'Table', label: into.name, span: into.span }],
200
+ };
201
+ }
202
+ function insertColumnsNode(columns) {
203
+ return {
204
+ id: 'columns',
205
+ kind: 'ColumnList',
206
+ label: 'columns',
207
+ detail: `${columns.names.length} named`,
208
+ span: columns.span,
209
+ children: columns.names.map((c, i) => ({
210
+ id: `columns.col${i}`,
211
+ kind: 'Column',
212
+ label: c.name,
213
+ span: c.span,
214
+ })),
215
+ };
216
+ }
217
+ function valuesNode(values) {
218
+ return {
219
+ id: 'values',
220
+ kind: 'Values',
221
+ label: 'VALUES',
222
+ detail: `${values.items.length} value${values.items.length === 1 ? '' : 's'}`,
223
+ span: values.span,
224
+ children: values.items.map((v, i) => exprNode(v, `values.v${i}`)),
225
+ };
226
+ }
227
+ function updateTableNode(table) {
228
+ return { id: 'table', kind: 'Table', label: table.name, span: table.span };
229
+ }
230
+ function assignmentNode(a, id) {
231
+ return {
232
+ id,
233
+ kind: 'Assignment',
234
+ label: a.column,
235
+ detail: `= ${exprToSql(a.value)}`,
236
+ span: { from: a.columnSpan.from, to: a.value.span.to },
237
+ // A literal is fully described by the detail; a computed value gets its own subtree.
238
+ ...(a.value.kind === 'literal' ? {} : { children: [exprNode(a.value, `${id}.v`)] }),
239
+ };
240
+ }
241
+ function setNode(set) {
242
+ return {
243
+ id: 'set',
244
+ kind: 'Set',
245
+ label: 'SET',
246
+ detail: `${set.assignments.length} assignment${set.assignments.length === 1 ? '' : 's'}`,
247
+ span: set.span,
248
+ children: set.assignments.map((a, i) => assignmentNode(a, `set.a${i}`)),
249
+ };
250
+ }
251
+ const ROOT_KIND = {
252
+ SELECT: 'SelectStatement',
253
+ DELETE: 'DeleteStatement',
254
+ INSERT: 'InsertStatement',
255
+ UPDATE: 'UpdateStatement',
256
+ ANALYZE: 'AnalyzeStatement',
257
+ };
258
+ function root(children, span, label = 'SELECT') {
259
+ return { id: 'select-stmt', kind: ROOT_KIND[label], label, span, children };
260
+ }
261
+ /** The complete display tree for a parsed statement. */
262
+ export function toDisplayTree(ast) {
263
+ const children = [selectListNode(ast.select, ast.distinct), fromNode(ast.from)];
264
+ if (ast.where)
265
+ children.push(whereNode(ast.where));
266
+ if (ast.groupBy)
267
+ children.push(groupByNode(ast.groupBy));
268
+ if (ast.having)
269
+ children.push(havingNode(ast.having));
270
+ if (ast.orderBy)
271
+ children.push(orderByNode(ast.orderBy));
272
+ if (ast.limit)
273
+ children.push(limitNode(ast.limit));
274
+ return root(children, ast.span);
275
+ }
276
+ function countNodes(node) {
277
+ return 1 + (node.children ?? []).reduce((n, c) => n + countNodes(c), 0);
278
+ }
279
+ /** Emit the parse stage one clause at a time, so it can be stepped through. */
280
+ export function emitParseEvents(emit, ast) {
281
+ const children = [];
282
+ const selectList = selectListNode(ast.select, ast.distinct);
283
+ children.push(selectList);
284
+ emit(ast.select.kind === 'star'
285
+ ? `Read the SELECT list: ${ast.distinct ? 'DISTINCT ' : ''}\`*\`, meaning every column of the table${ast.distinct ? ' — and each whole row is returned only once' : ''}.`
286
+ : `Read the SELECT list: ${ast.distinct ? 'DISTINCT ' : ''}${ast.select.items.map((item) => `\`${selectItemKey(item)}\``).join(', ')}${ast.distinct ? ' — each distinct combination is returned only once' : ''}.`, { stage: 'parse', ast: root([...children], ast.span), span: ast.select.span });
287
+ children.push(fromNode(ast.from));
288
+ emit(`Read the FROM clause: the query reads the \`${ast.from.name}\` table.`, {
289
+ stage: 'parse',
290
+ ast: root([...children], ast.span),
291
+ span: ast.from.span,
292
+ });
293
+ if (ast.where) {
294
+ children.push(whereNode(ast.where));
295
+ emit('Read the WHERE clause and build the predicate tree. Nothing has been executed yet — this is still only structure.', { stage: 'parse', ast: root([...children], ast.span), span: ast.where.span });
296
+ }
297
+ if (ast.groupBy) {
298
+ children.push(groupByNode(ast.groupBy));
299
+ emit(`Read the GROUP BY clause: rows are grouped by ${ast.groupBy.columns
300
+ .map((c) => `\`${c.name}\``)
301
+ .join(', ')}. Nothing has been computed yet — this is still only structure.`, { stage: 'parse', ast: root([...children], ast.span), span: ast.groupBy.span });
302
+ }
303
+ if (ast.having) {
304
+ children.push(havingNode(ast.having));
305
+ emit('Read the HAVING clause: a predicate over the *groups* — unlike WHERE it may test aggregates, because it runs after they are computed. Nothing has been computed yet — this is still only structure.', { stage: 'parse', ast: root([...children], ast.span), span: ast.having.span });
306
+ }
307
+ if (ast.orderBy) {
308
+ children.push(orderByNode(ast.orderBy));
309
+ emit(`Read the ORDER BY clause: sort by \`${ast.orderBy.column}\`, ${ast.orderBy.direction === 'asc' ? 'ascending' : 'descending'}.`, { stage: 'parse', ast: root([...children], ast.span), span: ast.orderBy.span });
310
+ }
311
+ if (ast.limit) {
312
+ children.push(limitNode(ast.limit));
313
+ emit(ast.limit.offset === undefined
314
+ ? `Read the LIMIT clause: cap the result at ${ast.limit.value}.`
315
+ : `Read the LIMIT clause: skip the first ${ast.limit.offset} rows, then cap the result at ${ast.limit.value}.`, { stage: 'parse', ast: root([...children], ast.span), span: ast.limit.span });
316
+ }
317
+ const tree = root([...children], ast.span);
318
+ emit(`Parsing is done: ${countNodes(tree)} AST nodes and no data touched. The planner turns this shape into a plan next.`, { stage: 'parse', ast: tree, span: ast.span });
319
+ }
320
+ /** The complete display tree for a parsed `DELETE`. */
321
+ export function deleteDisplayTree(ast) {
322
+ const children = [fromNode(ast.from)];
323
+ if (ast.where)
324
+ children.push(whereNode(ast.where));
325
+ return root(children, ast.span, 'DELETE');
326
+ }
327
+ /** Emit the parse stage for a `DELETE`, one clause at a time — mirrors `emitParseEvents`. */
328
+ export function emitDeleteParseEvents(emit, ast) {
329
+ const children = [fromNode(ast.from)];
330
+ emit(`Read the FROM clause: DELETE removes rows from the \`${ast.from.name}\` table.`, {
331
+ stage: 'parse',
332
+ ast: root([...children], ast.span, 'DELETE'),
333
+ span: ast.from.span,
334
+ });
335
+ if (ast.where) {
336
+ children.push(whereNode(ast.where));
337
+ emit('Read the WHERE clause and build the predicate tree. Nothing has been removed yet — this is still only structure.', { stage: 'parse', ast: root([...children], ast.span, 'DELETE'), span: ast.where.span });
338
+ }
339
+ const tree = root([...children], ast.span, 'DELETE');
340
+ emit(`Parsing is done: ${countNodes(tree)} AST nodes and no data touched. The planner turns this shape into a plan next.`, { stage: 'parse', ast: tree, span: ast.span });
341
+ }
342
+ /** The complete display tree for a parsed `INSERT`. */
343
+ export function insertDisplayTree(ast) {
344
+ const children = [intoNode(ast.into)];
345
+ if (ast.columns)
346
+ children.push(insertColumnsNode(ast.columns));
347
+ children.push(valuesNode(ast.values));
348
+ return root(children, ast.span, 'INSERT');
349
+ }
350
+ /** Emit the parse stage for an `INSERT`, one clause at a time — mirrors `emitParseEvents`. */
351
+ export function emitInsertParseEvents(emit, ast) {
352
+ const children = [intoNode(ast.into)];
353
+ emit(`Read the INTO clause: INSERT adds a row to the \`${ast.into.name}\` table.`, {
354
+ stage: 'parse',
355
+ ast: root([...children], ast.span, 'INSERT'),
356
+ span: ast.into.span,
357
+ });
358
+ if (ast.columns) {
359
+ children.push(insertColumnsNode(ast.columns));
360
+ emit(`Read the column list: ${ast.columns.names.map((c) => `\`${c.name}\``).join(', ')}.`, { stage: 'parse', ast: root([...children], ast.span, 'INSERT'), span: ast.columns.span });
361
+ }
362
+ children.push(valuesNode(ast.values));
363
+ emit(`Read the VALUES list: ${ast.values.items.length} value${ast.values.items.length === 1 ? '' : 's'}. Nothing has been written yet — this is still only structure.`, { stage: 'parse', ast: root([...children], ast.span, 'INSERT'), span: ast.values.span });
364
+ const tree = root([...children], ast.span, 'INSERT');
365
+ emit(`Parsing is done: ${countNodes(tree)} AST nodes and no data touched. There is nothing to plan or optimize — an INSERT with only literal values goes straight to execution.`, { stage: 'parse', ast: tree, span: ast.span });
366
+ }
367
+ /** The complete display tree for a parsed `UPDATE`. */
368
+ export function updateDisplayTree(ast) {
369
+ const children = [updateTableNode(ast.table), setNode(ast.set)];
370
+ if (ast.where)
371
+ children.push(whereNode(ast.where));
372
+ return root(children, ast.span, 'UPDATE');
373
+ }
374
+ /** Emit the parse stage for an `UPDATE`, one clause at a time — mirrors `emitParseEvents`. */
375
+ export function emitUpdateParseEvents(emit, ast) {
376
+ const children = [updateTableNode(ast.table)];
377
+ emit(`Read the table name: UPDATE modifies rows in \`${ast.table.name}\`.`, {
378
+ stage: 'parse',
379
+ ast: root([...children], ast.span, 'UPDATE'),
380
+ span: ast.table.span,
381
+ });
382
+ children.push(setNode(ast.set));
383
+ emit(`Read the SET clause: ${ast.set.assignments.map((a) => `\`${a.column}\` = ${exprToSql(a.value)}`).join(', ')}. Nothing has been written yet — this is still only structure.`, { stage: 'parse', ast: root([...children], ast.span, 'UPDATE'), span: ast.set.span });
384
+ if (ast.where) {
385
+ children.push(whereNode(ast.where));
386
+ emit('Read the WHERE clause and build the predicate tree. Nothing has been modified yet — this is still only structure.', { stage: 'parse', ast: root([...children], ast.span, 'UPDATE'), span: ast.where.span });
387
+ }
388
+ const tree = root([...children], ast.span, 'UPDATE');
389
+ emit(`Parsing is done: ${countNodes(tree)} AST nodes and no data touched. The planner turns the WHERE clause into a plan next — the SET clause has nothing to plan, it only takes effect once a row is found.`, { stage: 'parse', ast: tree, span: ast.span });
390
+ }
391
+ /** The complete display tree for a parsed `ANALYZE`. */
392
+ export function analyzeDisplayTree(ast) {
393
+ return root([updateTableNode(ast.table)], ast.span, 'ANALYZE');
394
+ }
395
+ /** Emit the parse stage for an `ANALYZE` — one clause, so one step: mirrors `emitParseEvents`'s shape all the same. */
396
+ export function emitAnalyzeParseEvents(emit, ast) {
397
+ const tree = root([updateTableNode(ast.table)], ast.span, 'ANALYZE');
398
+ emit(`Read the table name: ANALYZE scans \`${ast.table.name}\` to build its statistics.`, {
399
+ stage: 'parse',
400
+ ast: tree,
401
+ span: ast.table.span,
402
+ });
403
+ emit(`Parsing is done: ${countNodes(tree)} AST node${countNodes(tree) === 1 ? '' : 's'} and no data touched. There is nothing to plan or optimize — ANALYZE goes straight to a real scan.`, { stage: 'parse', ast: tree, span: ast.span });
404
+ }
@@ -0,0 +1,4 @@
1
+ export { AGGREGATE_FNS, parse, parseStatement } from "./parser.js";
2
+ export { tokenize, KEYWORDS, TokenizeError } from "./tokenizer.js";
3
+ export { analyzeDisplayTree, deleteDisplayTree, emitAnalyzeParseEvents, emitDeleteParseEvents, emitInsertParseEvents, emitParseEvents, emitUpdateParseEvents, insertDisplayTree, toDisplayTree, updateDisplayTree, } from "./display.js";
4
+ export { aggregateKeyOf, childrenOf, columnKey, columnsIn, mapChildren, selectItemArgColumn, selectItemKey, walkExpr } from "./ast.js";