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,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer replacement, behind one interface, with two implementations.
|
|
3
|
+
*
|
|
4
|
+
* This exists because **textbook LRU is not what real databases do**, and
|
|
5
|
+
* plan.md §2 makes honesty about ground truth a top-line goal. Postgres avoids
|
|
6
|
+
* an LRU list precisely because maintaining one needs a lock on every page
|
|
7
|
+
* access; it uses clock sweep instead. InnoDB uses LRU but inserts at a
|
|
8
|
+
* midpoint so a big scan cannot flush the hot set. Teaching "eviction = LRU"
|
|
9
|
+
* would plant exactly the misconception §12 warns about.
|
|
10
|
+
*/
|
|
11
|
+
/** Postgres caps `usage_count` at 5. Same here. */
|
|
12
|
+
export const USAGE_COUNT_CAP = 5;
|
|
13
|
+
function frame(state, frameId) {
|
|
14
|
+
return state.frames.find((f) => f.frameId === frameId);
|
|
15
|
+
}
|
|
16
|
+
/* -------------------------------------------------------------------------- */
|
|
17
|
+
const lru = {
|
|
18
|
+
name: 'lru',
|
|
19
|
+
onAccess(state, frameId) {
|
|
20
|
+
const f = frame(state, frameId);
|
|
21
|
+
if (!f)
|
|
22
|
+
return;
|
|
23
|
+
state.now += 1;
|
|
24
|
+
f.lastUsedAt = state.now;
|
|
25
|
+
f.usageCount = Math.min(f.usageCount + 1, USAGE_COUNT_CAP);
|
|
26
|
+
},
|
|
27
|
+
selectVictim(state) {
|
|
28
|
+
let victim;
|
|
29
|
+
for (const f of state.frames) {
|
|
30
|
+
if (f.pinCount > 0)
|
|
31
|
+
continue;
|
|
32
|
+
if (!victim || f.lastUsedAt < victim.lastUsedAt)
|
|
33
|
+
victim = f;
|
|
34
|
+
}
|
|
35
|
+
return victim ? { frameId: victim.frameId } : null;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Postgres's clock sweep. The hand walks the frames in a circle; each pass
|
|
40
|
+
* decrements `usage_count`, and the first frame found with
|
|
41
|
+
* `refcount == 0 && usage_count == 0` is the victim. Frequently used pages
|
|
42
|
+
* survive several passes without any list to maintain — which is the whole
|
|
43
|
+
* point, since maintaining one would need a lock on every access.
|
|
44
|
+
*/
|
|
45
|
+
const clock = {
|
|
46
|
+
name: 'clock',
|
|
47
|
+
onAccess(state, frameId) {
|
|
48
|
+
const f = frame(state, frameId);
|
|
49
|
+
if (!f)
|
|
50
|
+
return;
|
|
51
|
+
state.now += 1;
|
|
52
|
+
f.lastUsedAt = state.now;
|
|
53
|
+
f.usageCount = Math.min(f.usageCount + 1, USAGE_COUNT_CAP);
|
|
54
|
+
},
|
|
55
|
+
selectVictim(state, emit) {
|
|
56
|
+
const n = state.frames.length;
|
|
57
|
+
if (n === 0)
|
|
58
|
+
return null;
|
|
59
|
+
if (state.frames.every((f) => f.pinCount > 0))
|
|
60
|
+
return null;
|
|
61
|
+
// Bounded by construction: every full pass strictly decreases the total
|
|
62
|
+
// usage count of unpinned frames, and at least one is unpinned.
|
|
63
|
+
const maxSteps = n * (USAGE_COUNT_CAP + 1) + n;
|
|
64
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
65
|
+
const f = state.frames[state.hand];
|
|
66
|
+
const at = state.hand;
|
|
67
|
+
state.hand = (state.hand + 1) % n;
|
|
68
|
+
if (f.pinCount > 0)
|
|
69
|
+
continue;
|
|
70
|
+
if (f.usageCount === 0)
|
|
71
|
+
return { frameId: f.frameId, clockHand: state.hand };
|
|
72
|
+
f.usageCount -= 1;
|
|
73
|
+
emit(`Clock sweep: frame ${at} was used recently, so it gets a second chance — usage count drops to ${f.usageCount} and the hand moves on.`, { stage: 'buffer', action: 'sweep', frameId: at, usageCountAfter: f.usageCount, clockHand: state.hand });
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
/**
|
|
80
|
+
* FIFO: evict whichever page has been resident longest, no matter how heavily
|
|
81
|
+
* it has been used since. The textbook baseline clock sweep improves on — and
|
|
82
|
+
* the one that shows Bélády's anomaly, where *adding* frames can add misses.
|
|
83
|
+
*/
|
|
84
|
+
const fifo = {
|
|
85
|
+
name: 'fifo',
|
|
86
|
+
onAccess() {
|
|
87
|
+
// A page's age under FIFO is when it was loaded, not when it was touched.
|
|
88
|
+
},
|
|
89
|
+
selectVictim(state) {
|
|
90
|
+
let victim;
|
|
91
|
+
for (const f of state.frames) {
|
|
92
|
+
if (f.pinCount > 0)
|
|
93
|
+
continue;
|
|
94
|
+
if (!victim || f.loadedAt < victim.loadedAt)
|
|
95
|
+
victim = f;
|
|
96
|
+
}
|
|
97
|
+
return victim ? { frameId: victim.frameId } : null;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Second-chance: FIFO with a reference bit. Walk the queue oldest-first; a
|
|
102
|
+
* frame whose bit is set is forgiven once — the bit clears and it goes to the
|
|
103
|
+
* back — so the first frame reached with a clear bit is the victim. This is
|
|
104
|
+
* clock sweep's ancestor: the same idea before the circular hand and the
|
|
105
|
+
* multi-bit counter. The forgiveness passes emit `sweep` events, the same kind
|
|
106
|
+
* clock uses, so the panel animates them identically.
|
|
107
|
+
*/
|
|
108
|
+
const secondChance = {
|
|
109
|
+
name: 'second-chance',
|
|
110
|
+
onAccess(state, frameId) {
|
|
111
|
+
const f = frame(state, frameId);
|
|
112
|
+
if (!f)
|
|
113
|
+
return;
|
|
114
|
+
state.now += 1;
|
|
115
|
+
f.lastUsedAt = state.now;
|
|
116
|
+
f.usageCount = Math.min(f.usageCount + 1, USAGE_COUNT_CAP);
|
|
117
|
+
},
|
|
118
|
+
selectVictim(state, emit) {
|
|
119
|
+
if (state.frames.every((f) => f.pinCount > 0))
|
|
120
|
+
return null;
|
|
121
|
+
// Every forgiveness clears one bit and cannot recur for that frame in the
|
|
122
|
+
// same call, so a clear bit is reached within one pass over the unpinned
|
|
123
|
+
// frames. The bound doubles that for headroom, matching `clock` above.
|
|
124
|
+
const unpinned = state.frames.filter((f) => f.pinCount === 0).length;
|
|
125
|
+
const maxSteps = unpinned * 2 + 1;
|
|
126
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
127
|
+
let oldest;
|
|
128
|
+
for (const f of state.frames) {
|
|
129
|
+
if (f.pinCount > 0)
|
|
130
|
+
continue;
|
|
131
|
+
if (!oldest || f.loadedAt < oldest.loadedAt)
|
|
132
|
+
oldest = f;
|
|
133
|
+
}
|
|
134
|
+
if (!oldest)
|
|
135
|
+
return null;
|
|
136
|
+
if (oldest.usageCount === 0)
|
|
137
|
+
return { frameId: oldest.frameId };
|
|
138
|
+
oldest.usageCount = 0;
|
|
139
|
+
state.now += 1;
|
|
140
|
+
oldest.loadedAt = state.now; // to the back of the queue
|
|
141
|
+
emit(`Second chance: frame ${oldest.frameId} was used since it loaded, so it survives this pass — its reference bit clears and it moves to the back of the queue.`, {
|
|
142
|
+
stage: 'buffer',
|
|
143
|
+
action: 'sweep',
|
|
144
|
+
frameId: oldest.frameId,
|
|
145
|
+
usageCountAfter: 0,
|
|
146
|
+
clockHand: state.hand,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Bélády's MIN / OPT: evict the resident page whose next use is farthest in
|
|
154
|
+
* the future (a page never used again wins outright). It cannot be implemented
|
|
155
|
+
* for real — nothing knows the future — but it is the optimal bound every
|
|
156
|
+
* other policy is measured against, and seeing it makes "how good is LRU here?"
|
|
157
|
+
* concrete. With no `lookahead` on the state it degrades to FIFO.
|
|
158
|
+
*/
|
|
159
|
+
function nextUseDistance(future, cursor, pageId) {
|
|
160
|
+
if (!future)
|
|
161
|
+
return Number.POSITIVE_INFINITY;
|
|
162
|
+
for (let k = cursor + 1; k < future.length; k++) {
|
|
163
|
+
if (future[k] === pageId)
|
|
164
|
+
return k;
|
|
165
|
+
}
|
|
166
|
+
return Number.POSITIVE_INFINITY;
|
|
167
|
+
}
|
|
168
|
+
const optimal = {
|
|
169
|
+
name: 'optimal',
|
|
170
|
+
onAccess() {
|
|
171
|
+
// MIN needs no per-access bookkeeping — the whole decision is the future.
|
|
172
|
+
},
|
|
173
|
+
selectVictim(state) {
|
|
174
|
+
let victim;
|
|
175
|
+
let victimNextUse = -1;
|
|
176
|
+
for (const f of state.frames) {
|
|
177
|
+
if (f.pinCount > 0 || f.pageId === null)
|
|
178
|
+
continue;
|
|
179
|
+
const nextUse = nextUseDistance(state.lookahead, state.lookaheadCursor, f.pageId);
|
|
180
|
+
// Farthest next use wins; on a tie (typically two pages never used
|
|
181
|
+
// again) evict the one loaded earliest, which makes the no-lookahead
|
|
182
|
+
// fallback exactly FIFO.
|
|
183
|
+
if (nextUse > victimNextUse ||
|
|
184
|
+
(nextUse === victimNextUse && victim !== undefined && f.loadedAt < victim.loadedAt)) {
|
|
185
|
+
victim = f;
|
|
186
|
+
victimNextUse = nextUse;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return victim ? { frameId: victim.frameId } : null;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* InnoDB's midpoint-insertion LRU. The buffer pool's LRU list is split into a
|
|
194
|
+
* *young* (hot) sublist and an *old* sublist; a page read from disk lands at
|
|
195
|
+
* the head of the *old* sublist, and only reaches the young sublist if it is
|
|
196
|
+
* touched again. A sequential scan touches each page once, so its pages ride
|
|
197
|
+
* through the old sublist and evict one another without ever displacing the
|
|
198
|
+
* hot set — the flaw in plain LRU that this exists to fix (plan.md §7.5).
|
|
199
|
+
*
|
|
200
|
+
* Modelled without InnoDB's `innodb_old_blocks_time` delay: `usageCount` is a
|
|
201
|
+
* 1-bit "which sublist" flag — 0 = old, 1 = young — and one re-access promotes.
|
|
202
|
+
* Eviction takes the least-recently-used frame in the old sublist, falling
|
|
203
|
+
* back to the overall LRU frame only when every unpinned frame is young.
|
|
204
|
+
*/
|
|
205
|
+
const midpoint = {
|
|
206
|
+
onLoad(state, frameId) {
|
|
207
|
+
const f = frame(state, frameId);
|
|
208
|
+
if (!f)
|
|
209
|
+
return;
|
|
210
|
+
state.now += 1;
|
|
211
|
+
f.lastUsedAt = state.now; // recency, but it stays in the old sublist
|
|
212
|
+
},
|
|
213
|
+
name: 'midpoint',
|
|
214
|
+
onAccess(state, frameId) {
|
|
215
|
+
const f = frame(state, frameId);
|
|
216
|
+
if (!f)
|
|
217
|
+
return;
|
|
218
|
+
state.now += 1;
|
|
219
|
+
f.lastUsedAt = state.now;
|
|
220
|
+
f.usageCount = 1; // a re-access promotes it to the young sublist
|
|
221
|
+
},
|
|
222
|
+
selectVictim(state) {
|
|
223
|
+
let old;
|
|
224
|
+
let overall;
|
|
225
|
+
for (const f of state.frames) {
|
|
226
|
+
if (f.pinCount > 0)
|
|
227
|
+
continue;
|
|
228
|
+
if (!overall || f.lastUsedAt < overall.lastUsedAt)
|
|
229
|
+
overall = f;
|
|
230
|
+
if (f.usageCount === 0 && (!old || f.lastUsedAt < old.lastUsedAt))
|
|
231
|
+
old = f;
|
|
232
|
+
}
|
|
233
|
+
const victim = old ?? overall;
|
|
234
|
+
return victim ? { frameId: victim.frameId } : null;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* LRU-K with K = 2 (O'Neil, O'Neil and Weikum, 1993). Plain LRU evicts whatever was touched least recently, so a
|
|
239
|
+
* single sequential scan can push out the pages that were genuinely hot. LRU-K evicts by the time of a page's K-th
|
|
240
|
+
* most recent reference instead. A page referenced only once has no K-th reference at all, so it is the first to go,
|
|
241
|
+
* and a page that has been touched twice survives a one-pass scan. That is the scan resistance this policy exists for.
|
|
242
|
+
*/
|
|
243
|
+
const LRU_K = 2;
|
|
244
|
+
const lruK = {
|
|
245
|
+
name: 'lru-k',
|
|
246
|
+
onAccess(state, frameId) {
|
|
247
|
+
const f = frame(state, frameId);
|
|
248
|
+
if (!f)
|
|
249
|
+
return;
|
|
250
|
+
state.now += 1;
|
|
251
|
+
f.lastUsedAt = state.now;
|
|
252
|
+
f.usageCount = Math.min(f.usageCount + 1, USAGE_COUNT_CAP);
|
|
253
|
+
if (f.pageId === null)
|
|
254
|
+
return;
|
|
255
|
+
const history = (state.history ??= {});
|
|
256
|
+
const times = [...(history[f.pageId] ?? []), state.now].slice(-LRU_K);
|
|
257
|
+
history[f.pageId] = times;
|
|
258
|
+
},
|
|
259
|
+
selectVictim(state) {
|
|
260
|
+
let victim;
|
|
261
|
+
let victimKth = Number.POSITIVE_INFINITY;
|
|
262
|
+
for (const f of state.frames) {
|
|
263
|
+
if (f.pinCount > 0)
|
|
264
|
+
continue;
|
|
265
|
+
const times = f.pageId === null ? [] : (state.history?.[f.pageId] ?? []);
|
|
266
|
+
// A page with fewer than K references has no K-th reference: treat it as older than anything, so it goes first.
|
|
267
|
+
const kth = times.length >= LRU_K ? times[0] : Number.NEGATIVE_INFINITY;
|
|
268
|
+
if (!victim ||
|
|
269
|
+
kth < victimKth ||
|
|
270
|
+
(kth === victimKth && f.lastUsedAt < victim.lastUsedAt)) {
|
|
271
|
+
victim = f;
|
|
272
|
+
victimKth = kth;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return victim ? { frameId: victim.frameId } : null;
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* 2Q (Johnson and Shasha, 1994). A page read once waits in a small FIFO, A1in. Only a page read again after it has
|
|
280
|
+
* left that FIFO, its id remembered in the ghost list, is promoted to the LRU of hot pages, Am. A one-pass scan
|
|
281
|
+
* therefore fills A1in and evicts only itself. A1in holds a quarter of the frames, the ghost list half as many ids.
|
|
282
|
+
*/
|
|
283
|
+
const twoQ = {
|
|
284
|
+
name: 'two-q',
|
|
285
|
+
onAccess(state, frameId) {
|
|
286
|
+
const f = frame(state, frameId);
|
|
287
|
+
if (!f)
|
|
288
|
+
return;
|
|
289
|
+
// A re-reference while the page is still in A1in does not promote it: the paper's correlated-reference rule.
|
|
290
|
+
if (state.queues?.[frameId] === 'a1in')
|
|
291
|
+
return;
|
|
292
|
+
state.now += 1;
|
|
293
|
+
f.lastUsedAt = state.now;
|
|
294
|
+
f.usageCount = Math.min(f.usageCount + 1, USAGE_COUNT_CAP);
|
|
295
|
+
},
|
|
296
|
+
onLoad(state, frameId) {
|
|
297
|
+
const f = frame(state, frameId);
|
|
298
|
+
if (!f)
|
|
299
|
+
return;
|
|
300
|
+
state.now += 1;
|
|
301
|
+
f.lastUsedAt = state.now;
|
|
302
|
+
f.usageCount = Math.min(f.usageCount + 1, USAGE_COUNT_CAP);
|
|
303
|
+
const ghost = state.ghost ?? [];
|
|
304
|
+
const remembered = f.pageId !== null && ghost.includes(f.pageId);
|
|
305
|
+
if (remembered)
|
|
306
|
+
state.ghost = ghost.filter((p) => p !== f.pageId);
|
|
307
|
+
(state.queues ??= {})[frameId] = remembered ? 'am' : 'a1in';
|
|
308
|
+
},
|
|
309
|
+
selectVictim(state) {
|
|
310
|
+
const queues = state.queues ?? {};
|
|
311
|
+
const candidates = state.frames.filter((f) => f.pinCount === 0);
|
|
312
|
+
const oldestFirst = (a, b) => a.lastUsedAt - b.lastUsedAt;
|
|
313
|
+
const a1in = candidates.filter((f) => queues[f.frameId] === 'a1in').sort(oldestFirst);
|
|
314
|
+
const am = candidates.filter((f) => queues[f.frameId] !== 'a1in').sort(oldestFirst);
|
|
315
|
+
const kin = Math.max(1, Math.floor(state.frames.length / 4));
|
|
316
|
+
const victim = a1in.length > kin || am.length === 0 ? a1in[0] : am[0];
|
|
317
|
+
if (!victim)
|
|
318
|
+
return null;
|
|
319
|
+
if (queues[victim.frameId] === 'a1in' && victim.pageId !== null) {
|
|
320
|
+
const kout = Math.max(1, Math.floor(state.frames.length / 2));
|
|
321
|
+
state.ghost = [...(state.ghost ?? []), victim.pageId].slice(-kout);
|
|
322
|
+
}
|
|
323
|
+
return { frameId: victim.frameId };
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
const POLICIES = {
|
|
327
|
+
lru,
|
|
328
|
+
clock,
|
|
329
|
+
fifo,
|
|
330
|
+
'second-chance': secondChance,
|
|
331
|
+
optimal,
|
|
332
|
+
midpoint,
|
|
333
|
+
'lru-k': lruK,
|
|
334
|
+
'two-q': twoQ,
|
|
335
|
+
};
|
|
336
|
+
export function policyFor(name) {
|
|
337
|
+
return POLICIES[name];
|
|
338
|
+
}
|
|
339
|
+
/** Display order for policy pickers. */
|
|
340
|
+
export const REPLACEMENT_POLICY_NAMES = [
|
|
341
|
+
'lru',
|
|
342
|
+
'clock',
|
|
343
|
+
'fifo',
|
|
344
|
+
'second-chance',
|
|
345
|
+
'optimal',
|
|
346
|
+
'midpoint',
|
|
347
|
+
'lru-k',
|
|
348
|
+
'two-q',
|
|
349
|
+
];
|
|
350
|
+
/** Short label for each policy, for `<select>`s and column headers. */
|
|
351
|
+
export const REPLACEMENT_POLICY_LABELS = {
|
|
352
|
+
lru: 'LRU',
|
|
353
|
+
clock: 'Clock sweep',
|
|
354
|
+
fifo: 'FIFO',
|
|
355
|
+
'second-chance': 'Second-chance',
|
|
356
|
+
optimal: 'Optimal (Bélády)',
|
|
357
|
+
midpoint: 'Midpoint LRU (InnoDB)',
|
|
358
|
+
'lru-k': 'LRU-2 (scan-resistant)',
|
|
359
|
+
'two-q': '2Q (scan-resistant)',
|
|
360
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves an `IN (subquery)` predicate (plan.md §25.4 C3 slice c, closing C3) exactly once, before the query
|
|
3
|
+
* that names it is planned — the one place `runQuery.ts`'s SELECT, DELETE and UPDATE paths all share.
|
|
4
|
+
*
|
|
5
|
+
* `mapChildren` (`parser/ast.ts`) already rebuilds every other `Expr` kind from its children, but it does not
|
|
6
|
+
* know how to turn a `subquery` into `items` — that needs a real sub-run of the engine, not a pure tree
|
|
7
|
+
* transform — so this walks the same shape `mapChildren` would (`and`/`or`/`not`, the only kinds that can nest
|
|
8
|
+
* another predicate) by hand, stopping to resolve any `in` node that carries a `subquery`.
|
|
9
|
+
*/
|
|
10
|
+
/** Every distinct value `column` holds across `rows`, in first-seen order — `null` counted like any other value. */
|
|
11
|
+
function distinctColumnValues(rows, column) {
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const values = [];
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
const value = row[column] ?? null;
|
|
16
|
+
const key = value === null ? '\u0000null' : `${typeof value}:${String(value)}`;
|
|
17
|
+
if (seen.has(key))
|
|
18
|
+
continue;
|
|
19
|
+
seen.add(key);
|
|
20
|
+
values.push(value);
|
|
21
|
+
}
|
|
22
|
+
return values;
|
|
23
|
+
}
|
|
24
|
+
/** A resolved value, as the literal `Expr` an ordinary `IN (1, 2, 3)` would already have held. */
|
|
25
|
+
function literalFor(value, span) {
|
|
26
|
+
return {
|
|
27
|
+
kind: 'literal',
|
|
28
|
+
value,
|
|
29
|
+
raw: value === null ? 'NULL' : typeof value === 'string' ? `'${value}'` : String(value),
|
|
30
|
+
span,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Runs one `IN`'s subquery for real — its own complete, independent sub-run of the engine (its own buffer pool,
|
|
35
|
+
* its own disk reads), exactly the way `explain.ts`'s `explainResult` already runs an inner query: padding the
|
|
36
|
+
* text with leading spaces rather than slicing them away keeps every character offset the sub-run reports (a
|
|
37
|
+
* parse error's span, say) pointing at the right place in the *outer* query's own editor. Not interleaved into
|
|
38
|
+
* the outer query's own step-by-step trace — the subquery's cost is real and already paid by the time this
|
|
39
|
+
* returns, but it is reported as one resolved fact (`emit.ts`'s WHERE-clause narration names the count), not a
|
|
40
|
+
* second parallel playback.
|
|
41
|
+
*/
|
|
42
|
+
function resolveOne(expr, subquery, sql, run) {
|
|
43
|
+
const inner = run(' '.repeat(subquery.span.from) + sql.slice(subquery.span.from, subquery.span.to));
|
|
44
|
+
if (inner.error) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: {
|
|
48
|
+
message: `The IN subquery failed: ${inner.error.message}`,
|
|
49
|
+
...(inner.error.hint ? { hint: inner.error.hint } : {}),
|
|
50
|
+
from: subquery.span.from,
|
|
51
|
+
to: subquery.span.to,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const values = distinctColumnValues(inner.rows, subquery.column.name);
|
|
56
|
+
const items = values.map((value) => literalFor(value, subquery.span));
|
|
57
|
+
return { ok: true, expr: { ...expr, items } };
|
|
58
|
+
}
|
|
59
|
+
/** `resolveOne`, recursively, for every `IN (subquery)` anywhere in `expr` — under any nesting of `AND`/`OR`/`NOT`. */
|
|
60
|
+
function resolveInSubqueries(expr, sql, run) {
|
|
61
|
+
if (expr.kind === 'in' && expr.subquery)
|
|
62
|
+
return resolveOne(expr, expr.subquery, sql, run);
|
|
63
|
+
if (expr.kind === 'and' || expr.kind === 'or') {
|
|
64
|
+
const left = resolveInSubqueries(expr.left, sql, run);
|
|
65
|
+
if (!left.ok)
|
|
66
|
+
return left;
|
|
67
|
+
const right = resolveInSubqueries(expr.right, sql, run);
|
|
68
|
+
if (!right.ok)
|
|
69
|
+
return right;
|
|
70
|
+
return { ok: true, expr: { ...expr, left: left.expr, right: right.expr } };
|
|
71
|
+
}
|
|
72
|
+
if (expr.kind === 'not') {
|
|
73
|
+
const operand = resolveInSubqueries(expr.operand, sql, run);
|
|
74
|
+
if (!operand.ok)
|
|
75
|
+
return operand;
|
|
76
|
+
return { ok: true, expr: { ...expr, operand: operand.expr } };
|
|
77
|
+
}
|
|
78
|
+
// Every other kind's own children are arithmetic/columns/literals — `parsePredicate`'s grammar never lets a
|
|
79
|
+
// predicate (so never an `IN (subquery)`) nest inside one of those.
|
|
80
|
+
return { ok: true, expr };
|
|
81
|
+
}
|
|
82
|
+
/** `where`, with every `IN (subquery)` it contains resolved — or the first error one of them hit. `undefined` in, `undefined` out. */
|
|
83
|
+
export function resolveWhereSubqueries(where, sql, run) {
|
|
84
|
+
if (!where)
|
|
85
|
+
return { ok: true, where };
|
|
86
|
+
const resolved = resolveInSubqueries(where, sql, run);
|
|
87
|
+
return resolved.ok ? { ok: true, where: resolved.expr } : resolved;
|
|
88
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace buffer: an `emit` callback threaded down into every operator. The
|
|
3
|
+
* query runs to completion and the array is returned. Simplest thing that
|
|
4
|
+
* works, and it works because playback is pre-computed anyway (plan.md §7.8).
|
|
5
|
+
*/
|
|
6
|
+
export function createTracer() {
|
|
7
|
+
const events = [];
|
|
8
|
+
const emit = (label, body, options) => {
|
|
9
|
+
const event = { ...body, id: events.length, label };
|
|
10
|
+
if (options?.predictable)
|
|
11
|
+
event.predictable = options.predictable;
|
|
12
|
+
if (options?.dwell !== undefined)
|
|
13
|
+
event.dwell = options.dwell;
|
|
14
|
+
events.push(event);
|
|
15
|
+
};
|
|
16
|
+
return { emit, drain: () => events.slice() };
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single contract between the engine and the UI.
|
|
3
|
+
*
|
|
4
|
+
* Rules for everything in `src/engine`:
|
|
5
|
+
* - pure and synchronous (no DOM, no React, no async, no I/O)
|
|
6
|
+
* - every value here must be structurally cloneable, so the engine stays
|
|
7
|
+
* worker-ready even though v0 runs it on the main thread
|
|
8
|
+
*/
|
|
9
|
+
export const STAGES = [
|
|
10
|
+
'parse',
|
|
11
|
+
'plan',
|
|
12
|
+
'optimize',
|
|
13
|
+
'execute',
|
|
14
|
+
'buffer',
|
|
15
|
+
'index',
|
|
16
|
+
'lock',
|
|
17
|
+
'result',
|
|
18
|
+
];
|
|
19
|
+
export const DEFAULT_ENGINE_OPTIONS = {
|
|
20
|
+
bufferFrames: 8,
|
|
21
|
+
rowsPerPage: 4,
|
|
22
|
+
policy: 'lru',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The stages this build of the engine actually executes. Everything else in
|
|
26
|
+
* `STAGES` is scaffolding the UI must show as unbuilt — plan.md §2 makes
|
|
27
|
+
* honesty about ground truth a top-line goal, and a pipeline that looks
|
|
28
|
+
* complete when it is not breaks that. Grows one milestone at a time.
|
|
29
|
+
*/
|
|
30
|
+
export const IMPLEMENTED_STAGES = [
|
|
31
|
+
'parse',
|
|
32
|
+
'plan',
|
|
33
|
+
'optimize',
|
|
34
|
+
'buffer',
|
|
35
|
+
'index',
|
|
36
|
+
'execute',
|
|
37
|
+
'lock',
|
|
38
|
+
'result',
|
|
39
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arithmetic on `SqlValue`s (plan.md §25.4 B1b) — pure, and free of the parser
|
|
3
|
+
* and the executor so that both can use it: the executor to evaluate
|
|
4
|
+
* `price * qty`, the parser and optimizer to fold `-5` or `2 + 3` into a
|
|
5
|
+
* literal (a folded literal is what lets `WHERE id = 2 + 3` become an index
|
|
6
|
+
* lookup). The rules follow SQLite where it is well-defined, and PostgreSQL
|
|
7
|
+
* where SQLite is permissive — each choice is a ledger entry or a comment.
|
|
8
|
+
*/
|
|
9
|
+
const NUMERIC_TEXT = /^\s*[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?\s*$/i;
|
|
10
|
+
/**
|
|
11
|
+
* A value as a number, or `null` when it is not one.
|
|
12
|
+
*
|
|
13
|
+
* NULL stays NULL. A boolean is 0 or 1. A string that *is* a number (`'5'`,
|
|
14
|
+
* `' 2.5 '`) is that number. Any other string is not a number, so it yields
|
|
15
|
+
* NULL — the arithmetic result is then NULL. SQLite reads such text as 0,
|
|
16
|
+
* silently (ledger: `arithmetic-on-text`); NULL says "this is not a number"
|
|
17
|
+
* without inventing a value.
|
|
18
|
+
*/
|
|
19
|
+
export function toNumber(value) {
|
|
20
|
+
if (value === null)
|
|
21
|
+
return null;
|
|
22
|
+
if (typeof value === 'number')
|
|
23
|
+
return value;
|
|
24
|
+
if (typeof value === 'boolean')
|
|
25
|
+
return value ? 1 : 0;
|
|
26
|
+
return NUMERIC_TEXT.test(value) ? Number(value) : null;
|
|
27
|
+
}
|
|
28
|
+
/** `-0` is not a value SQL users ever see; keep it out of results and out of `toEqual`. */
|
|
29
|
+
const clean = (n) => (n === 0 ? 0 : n);
|
|
30
|
+
/**
|
|
31
|
+
* `left op right`. NULL in, NULL out. `x / 0` and `x % 0` are NULL (SQLite's
|
|
32
|
+
* rule — PostgreSQL raises an error, which this engine has no channel for at
|
|
33
|
+
* run time). `/` between two whole numbers is **integer division**, truncating
|
|
34
|
+
* toward zero (`7 / 2` is 3, `-7 / 2` is -3), in both SQLite and PostgreSQL;
|
|
35
|
+
* anything else divides exactly. Numbers here carry no int/real distinction,
|
|
36
|
+
* so a REAL column holding 3.0 divides as the integer 3 — ledger:
|
|
37
|
+
* `division-of-whole-reals`.
|
|
38
|
+
*/
|
|
39
|
+
export function arithmetic(op, left, right) {
|
|
40
|
+
const a = toNumber(left);
|
|
41
|
+
const b = toNumber(right);
|
|
42
|
+
if (a === null || b === null)
|
|
43
|
+
return null;
|
|
44
|
+
switch (op) {
|
|
45
|
+
case '+':
|
|
46
|
+
return clean(a + b);
|
|
47
|
+
case '-':
|
|
48
|
+
return clean(a - b);
|
|
49
|
+
case '*':
|
|
50
|
+
return clean(a * b);
|
|
51
|
+
case '/':
|
|
52
|
+
if (b === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return clean(Number.isInteger(a) && Number.isInteger(b) ? Math.trunc(a / b) : a / b);
|
|
55
|
+
case '%':
|
|
56
|
+
return b === 0 ? null : clean(a % b);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Unary minus. NULL stays NULL; a non-number is NULL. */
|
|
60
|
+
export function negate(value) {
|
|
61
|
+
const n = toNumber(value);
|
|
62
|
+
return n === null ? null : clean(-n);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A total order over `SqlValue`, `-1`/`0`/`1`: NULL sorts before everything and equals only NULL, two numbers
|
|
66
|
+
* compare numerically, anything else compares as text (a boolean as `'0'`/`'1'`) — an **index's** order, not SQL's
|
|
67
|
+
* `ORDER BY` (`exec/sort.ts`'s own `compareAsc` sorts NULLs *last*, Postgres's default there, deliberately the
|
|
68
|
+
* opposite rule). Shared by a table's own B+Tree indexes (`index/btree.ts`'s `compareScalar`), a clustered table's
|
|
69
|
+
* physical layout (`storage/page.ts`), and `ANALYZE`'s histogram and correlation statistics (plan.md §25.4 C1) —
|
|
70
|
+
* kept here once so none of the three can quietly drift from another.
|
|
71
|
+
*/
|
|
72
|
+
export function compareValues(a, b) {
|
|
73
|
+
if (a === null || b === null)
|
|
74
|
+
return a === b ? 0 : a === null ? -1 : 1;
|
|
75
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
76
|
+
return a - b;
|
|
77
|
+
const sa = typeof a === 'boolean' ? String(Number(a)) : String(a);
|
|
78
|
+
const sb = typeof b === 'boolean' ? String(Number(b)) : String(b);
|
|
79
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
80
|
+
}
|