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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/bin/querylens.js +208 -0
- package/dist/src/engine/bufferTrace.js +67 -0
- package/dist/src/engine/datasets.js +139 -0
- package/dist/src/engine/exec/delete.js +95 -0
- package/dist/src/engine/exec/evaluate.js +174 -0
- package/dist/src/engine/exec/index.js +4 -0
- package/dist/src/engine/exec/insert.js +75 -0
- package/dist/src/engine/exec/operators.js +1290 -0
- package/dist/src/engine/exec/run.js +35 -0
- package/dist/src/engine/exec/sort.js +171 -0
- package/dist/src/engine/exec/unique.js +79 -0
- package/dist/src/engine/exec/update.js +124 -0
- package/dist/src/engine/exec/writeScan.js +88 -0
- package/dist/src/engine/explain.js +114 -0
- package/dist/src/engine/index/btree.js +481 -0
- package/dist/src/engine/index/build.js +99 -0
- package/dist/src/engine/index/bulk.js +107 -0
- package/dist/src/engine/index/display.js +38 -0
- package/dist/src/engine/index/index.js +9 -0
- package/dist/src/engine/index/lookup.js +213 -0
- package/dist/src/engine/index/rangeLookup.js +158 -0
- package/dist/src/engine/index/spec.js +47 -0
- package/dist/src/engine/index/unique.js +31 -0
- package/dist/src/engine/index/validate.js +105 -0
- package/dist/src/engine/index.js +16 -0
- package/dist/src/engine/locks/index.js +1 -0
- package/dist/src/engine/locks/lockManager.js +46 -0
- package/dist/src/engine/parser/ast.js +77 -0
- package/dist/src/engine/parser/display.js +404 -0
- package/dist/src/engine/parser/index.js +4 -0
- package/dist/src/engine/parser/parser.js +1108 -0
- package/dist/src/engine/parser/print.js +74 -0
- package/dist/src/engine/parser/tokenizer.js +146 -0
- package/dist/src/engine/planner/buildPlan.js +208 -0
- package/dist/src/engine/planner/cost.js +582 -0
- package/dist/src/engine/planner/emit.js +267 -0
- package/dist/src/engine/planner/emitDelete.js +57 -0
- package/dist/src/engine/planner/emitUpdate.js +51 -0
- package/dist/src/engine/planner/index.js +8 -0
- package/dist/src/engine/planner/joinOrder.js +252 -0
- package/dist/src/engine/planner/optimize.js +906 -0
- package/dist/src/engine/planner/plan.js +445 -0
- package/dist/src/engine/predict.js +120 -0
- package/dist/src/engine/runQuery.js +393 -0
- package/dist/src/engine/seed.js +165 -0
- package/dist/src/engine/stats.js +118 -0
- package/dist/src/engine/storage/bufferPool.js +194 -0
- package/dist/src/engine/storage/index.js +3 -0
- package/dist/src/engine/storage/page.js +46 -0
- package/dist/src/engine/storage/policy.js +360 -0
- package/dist/src/engine/subquery.js +88 -0
- package/dist/src/engine/trace.js +17 -0
- package/dist/src/engine/types.js +39 -0
- package/dist/src/engine/value.js +80 -0
- package/dist/src/engine/viewState.js +187 -0
- package/package.json +40 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events are deltas. The view state at step *n* is derived by folding them.
|
|
3
|
+
*
|
|
4
|
+
* We deliberately do NOT snapshot engine state per step: snapshots bloat
|
|
5
|
+
* memory, make events non-diffable, and forfeit the ability to animate the
|
|
6
|
+
* *transition* — an eviction must be a frame visibly leaving, not grid-N
|
|
7
|
+
* swapped for grid-N+1. See plan.md §7.8.
|
|
8
|
+
*/
|
|
9
|
+
export function initialViewState(options) {
|
|
10
|
+
return {
|
|
11
|
+
step: 0,
|
|
12
|
+
stage: null,
|
|
13
|
+
narration: '',
|
|
14
|
+
lastEvent: null,
|
|
15
|
+
ast: null,
|
|
16
|
+
plan: null,
|
|
17
|
+
planBefore: null,
|
|
18
|
+
appliedRules: [],
|
|
19
|
+
frames: Array.from({ length: options.bufferFrames }, (_, frameId) => ({
|
|
20
|
+
frameId,
|
|
21
|
+
pageId: null,
|
|
22
|
+
usageCount: 0,
|
|
23
|
+
lastTouchedAt: -1,
|
|
24
|
+
})),
|
|
25
|
+
clockHand: 0,
|
|
26
|
+
bufferStats: { hits: 0, misses: 0, evictions: 0, writes: 0 },
|
|
27
|
+
indexTree: null,
|
|
28
|
+
indexPath: [],
|
|
29
|
+
indexOutcome: null,
|
|
30
|
+
locks: [],
|
|
31
|
+
locksTaken: 0,
|
|
32
|
+
operators: [],
|
|
33
|
+
rows: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const USAGE_COUNT_CAP = 5;
|
|
37
|
+
function touchFrame(frames, frameId, patch) {
|
|
38
|
+
return frames.map((f) => (f.frameId === frameId ? { ...f, ...patch } : f));
|
|
39
|
+
}
|
|
40
|
+
function frameById(frames, frameId) {
|
|
41
|
+
return frames.find((f) => f.frameId === frameId);
|
|
42
|
+
}
|
|
43
|
+
/** Pure reducer. `state` is never mutated. */
|
|
44
|
+
export function applyEvent(state, event) {
|
|
45
|
+
const next = {
|
|
46
|
+
...state,
|
|
47
|
+
step: state.step + 1,
|
|
48
|
+
stage: event.stage,
|
|
49
|
+
narration: event.label,
|
|
50
|
+
lastEvent: event,
|
|
51
|
+
};
|
|
52
|
+
switch (event.stage) {
|
|
53
|
+
case 'parse':
|
|
54
|
+
next.ast = event.ast;
|
|
55
|
+
return next;
|
|
56
|
+
case 'plan':
|
|
57
|
+
next.plan = event.planTree;
|
|
58
|
+
return next;
|
|
59
|
+
case 'optimize':
|
|
60
|
+
next.planBefore = event.before;
|
|
61
|
+
next.plan = event.after;
|
|
62
|
+
next.appliedRules = [...state.appliedRules, event.rule];
|
|
63
|
+
return next;
|
|
64
|
+
case 'execute': {
|
|
65
|
+
const existing = state.operators.findIndex((o) => o.op === event.op);
|
|
66
|
+
next.operators =
|
|
67
|
+
existing === -1
|
|
68
|
+
? [...state.operators, { op: event.op, rowsProduced: event.rowsProduced }]
|
|
69
|
+
: state.operators.map((o, i) => i === existing ? { ...o, rowsProduced: event.rowsProduced } : o);
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
case 'buffer': {
|
|
73
|
+
switch (event.action) {
|
|
74
|
+
case 'hit': {
|
|
75
|
+
const frame = frameById(state.frames, event.frameId);
|
|
76
|
+
next.frames = touchFrame(state.frames, event.frameId, {
|
|
77
|
+
usageCount: Math.min((frame?.usageCount ?? 0) + 1, USAGE_COUNT_CAP),
|
|
78
|
+
lastTouchedAt: event.id,
|
|
79
|
+
});
|
|
80
|
+
next.bufferStats = { ...state.bufferStats, hits: state.bufferStats.hits + 1 };
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
case 'miss':
|
|
84
|
+
next.bufferStats = {
|
|
85
|
+
...state.bufferStats,
|
|
86
|
+
misses: state.bufferStats.misses + 1,
|
|
87
|
+
};
|
|
88
|
+
return next;
|
|
89
|
+
case 'load':
|
|
90
|
+
next.frames = touchFrame(state.frames, event.frameId, {
|
|
91
|
+
pageId: event.pageId,
|
|
92
|
+
usageCount: 1,
|
|
93
|
+
lastTouchedAt: event.id,
|
|
94
|
+
});
|
|
95
|
+
return next;
|
|
96
|
+
case 'write':
|
|
97
|
+
next.frames = touchFrame(state.frames, event.frameId, {
|
|
98
|
+
pageId: event.pageId,
|
|
99
|
+
usageCount: 1,
|
|
100
|
+
lastTouchedAt: event.id,
|
|
101
|
+
});
|
|
102
|
+
next.bufferStats = { ...state.bufferStats, writes: state.bufferStats.writes + 1 };
|
|
103
|
+
return next;
|
|
104
|
+
case 'evict':
|
|
105
|
+
next.frames = touchFrame(state.frames, event.frameId, {
|
|
106
|
+
pageId: null,
|
|
107
|
+
usageCount: 0,
|
|
108
|
+
lastTouchedAt: event.id,
|
|
109
|
+
});
|
|
110
|
+
next.bufferStats = {
|
|
111
|
+
...state.bufferStats,
|
|
112
|
+
evictions: state.bufferStats.evictions + 1,
|
|
113
|
+
// A dirty victim costs a write on the way out — the pool's own
|
|
114
|
+
// `stats.writes` counts it too (bufferPool.ts), so the fold must
|
|
115
|
+
// match or the panel and the engine would silently disagree.
|
|
116
|
+
writes: state.bufferStats.writes + (event.writtenBack ? 1 : 0),
|
|
117
|
+
};
|
|
118
|
+
if (event.clockHand !== undefined)
|
|
119
|
+
next.clockHand = event.clockHand;
|
|
120
|
+
return next;
|
|
121
|
+
case 'sweep':
|
|
122
|
+
next.frames = touchFrame(state.frames, event.frameId, {
|
|
123
|
+
usageCount: event.usageCountAfter,
|
|
124
|
+
});
|
|
125
|
+
next.clockHand = event.clockHand;
|
|
126
|
+
return next;
|
|
127
|
+
}
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
case 'index':
|
|
131
|
+
switch (event.action) {
|
|
132
|
+
case 'open':
|
|
133
|
+
next.indexTree = event.tree;
|
|
134
|
+
next.indexPath = [];
|
|
135
|
+
next.indexOutcome = null;
|
|
136
|
+
return next;
|
|
137
|
+
case 'traverse':
|
|
138
|
+
next.indexPath = [...state.indexPath, event.nodeId];
|
|
139
|
+
next.indexOutcome = 'searching';
|
|
140
|
+
return next;
|
|
141
|
+
case 'found':
|
|
142
|
+
next.indexOutcome = 'found';
|
|
143
|
+
return next;
|
|
144
|
+
case 'not-found':
|
|
145
|
+
next.indexOutcome = 'not-found';
|
|
146
|
+
return next;
|
|
147
|
+
// A range scan's horizontal hop. Appends the leaf it lands on, the
|
|
148
|
+
// same way `traverse` appends a vertically-descended node — outcome
|
|
149
|
+
// stays 'searching' throughout, since a range has no single
|
|
150
|
+
// found/not-found answer the way a point lookup does.
|
|
151
|
+
case 'chain-next':
|
|
152
|
+
next.indexPath = [...state.indexPath, event.toNodeId];
|
|
153
|
+
next.indexOutcome = 'searching';
|
|
154
|
+
return next;
|
|
155
|
+
}
|
|
156
|
+
return next;
|
|
157
|
+
case 'lock':
|
|
158
|
+
next.locks =
|
|
159
|
+
event.action === 'acquire'
|
|
160
|
+
? [...state.locks, { pageId: event.pageId, mode: event.mode }]
|
|
161
|
+
: state.locks.filter((l) => !(l.pageId === event.pageId && l.mode === event.mode));
|
|
162
|
+
next.locksTaken = state.locksTaken + (event.action === 'acquire' ? 1 : 0);
|
|
163
|
+
return next;
|
|
164
|
+
case 'result':
|
|
165
|
+
next.rows = event.rows;
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function createTraceProjector(events, options) {
|
|
170
|
+
const cache = [initialViewState(options)];
|
|
171
|
+
return {
|
|
172
|
+
length: events.length,
|
|
173
|
+
at(step) {
|
|
174
|
+
const target = Math.max(0, Math.min(step, events.length));
|
|
175
|
+
while (cache.length <= target) {
|
|
176
|
+
const prev = cache[cache.length - 1];
|
|
177
|
+
const event = events[cache.length - 1];
|
|
178
|
+
cache.push(applyEvent(prev, event));
|
|
179
|
+
}
|
|
180
|
+
return cache[target];
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** Convenience for tests and one-off reads. */
|
|
185
|
+
export function stateAt(events, step, options) {
|
|
186
|
+
return createTraceProjector(events, options).at(step);
|
|
187
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "querylens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Trace a SQL query through QueryLens's toy database engine on the terminal: every parser, planner, buffer-pool and B+Tree step, one line each.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"querylens": "dist/bin/querylens.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.13.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"database-internals",
|
|
20
|
+
"sql",
|
|
21
|
+
"b-tree",
|
|
22
|
+
"buffer-pool",
|
|
23
|
+
"query-planner",
|
|
24
|
+
"cli",
|
|
25
|
+
"education"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://query-lens.1234naveenboddepalli.workers.dev",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/Naveen-Boddepalli/query-lens.git",
|
|
31
|
+
"directory": "packages/cli"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/Naveen-Boddepalli/query-lens/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
}
|
|
40
|
+
}
|