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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predicate evaluation with SQL's three-valued logic.
|
|
3
|
+
*
|
|
4
|
+
* A comparison against NULL is UNKNOWN, not false — and only TRUE passes a
|
|
5
|
+
* filter. That distinction is the reason `WHERE age > 30` and
|
|
6
|
+
* `WHERE NOT (age <= 30)` do not partition a table with NULLs, which is a
|
|
7
|
+
* genuine and commonly-missed piece of SQL semantics, so it is modelled rather
|
|
8
|
+
* than flattened to booleans.
|
|
9
|
+
*
|
|
10
|
+
* plan.md §25.4 B1 adds `OR`, `NOT`, `IS [NOT] NULL`, `IN` and `LIKE`, each with
|
|
11
|
+
* its own truth table below. The two that surprise people:
|
|
12
|
+
*
|
|
13
|
+
* NOT UNKNOWN is UNKNOWN so `NOT (x = 1)` still rejects the NULL rows
|
|
14
|
+
* x NOT IN (1, NULL) is never TRUE because `x <> NULL` is UNKNOWN for every x
|
|
15
|
+
*
|
|
16
|
+
* The tests in `oracle/` (ternary-logic partitioning, and a differential run
|
|
17
|
+
* against SQLite) check these tables against an outside authority.
|
|
18
|
+
*/
|
|
19
|
+
import { aggregateKeyOf, columnKey } from "../parser/index.js";
|
|
20
|
+
import { arithmetic, negate as negateValue } from "../value.js";
|
|
21
|
+
export function evaluateValue(expr, row) {
|
|
22
|
+
switch (expr.kind) {
|
|
23
|
+
case 'literal':
|
|
24
|
+
return expr.value;
|
|
25
|
+
case 'column':
|
|
26
|
+
return row[columnKey(expr)] ?? null;
|
|
27
|
+
// Arithmetic is a value (plan.md §25.4 B1b): NULL in, NULL out; `x / 0` is NULL; whole-number `/` truncates.
|
|
28
|
+
case 'arith':
|
|
29
|
+
return arithmetic(expr.op, evaluateValue(expr.left, row), evaluateValue(expr.right, row));
|
|
30
|
+
case 'neg':
|
|
31
|
+
return negateValue(evaluateValue(expr.operand, row));
|
|
32
|
+
// A HAVING aggregate is a value the aggregate operator already computed for this group (plan.md §25.4 B2).
|
|
33
|
+
case 'aggregate':
|
|
34
|
+
return row[aggregateKeyOf(expr.fn, expr.arg)] ?? null;
|
|
35
|
+
default:
|
|
36
|
+
return evaluatePredicate(expr, row);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Exported for `operators.ts`'s `HashAggregate` — MIN/MAX order values the same way a `<`/`>` predicate does. */
|
|
40
|
+
export function compare(op, left, right) {
|
|
41
|
+
if (left === null || right === null)
|
|
42
|
+
return null; // UNKNOWN
|
|
43
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
44
|
+
return compareOrdered(op, left, right);
|
|
45
|
+
}
|
|
46
|
+
const a = typeof left === 'boolean' ? String(Number(left)) : String(left);
|
|
47
|
+
const b = typeof right === 'boolean' ? String(Number(right)) : String(right);
|
|
48
|
+
return compareOrdered(op, a, b);
|
|
49
|
+
}
|
|
50
|
+
function compareOrdered(op, a, b) {
|
|
51
|
+
switch (op) {
|
|
52
|
+
case '=':
|
|
53
|
+
return a === b;
|
|
54
|
+
case '<>':
|
|
55
|
+
return a !== b;
|
|
56
|
+
case '<':
|
|
57
|
+
return a < b;
|
|
58
|
+
case '>':
|
|
59
|
+
return a > b;
|
|
60
|
+
case '<=':
|
|
61
|
+
return a <= b;
|
|
62
|
+
case '>=':
|
|
63
|
+
return a >= b;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A canonical string key for `=` comparison, exported for `operators.ts`'s
|
|
68
|
+
* hash join (plan.md §22.2): two non-null values collide under this key
|
|
69
|
+
* exactly when `compare('=', a, b)` above would say `true` — the same
|
|
70
|
+
* coercion, just computed once per value instead of pairwise. Never called
|
|
71
|
+
* on `null` — a NULL join key is filtered out before it would reach this,
|
|
72
|
+
* the same "NULL never matches NULL" rule `compare` itself enforces.
|
|
73
|
+
*/
|
|
74
|
+
export function equalityKeyOf(value) {
|
|
75
|
+
return typeof value === 'boolean' ? String(Number(value)) : String(value);
|
|
76
|
+
}
|
|
77
|
+
const textOf = (value) => equalityKeyOf(value);
|
|
78
|
+
const likeCache = new Map();
|
|
79
|
+
/**
|
|
80
|
+
* `LIKE`: `%` matches any run of characters (including none), `_` exactly one,
|
|
81
|
+
* and everything else matches itself. **Case-sensitive**, as in PostgreSQL —
|
|
82
|
+
* SQLite's default is case-insensitive for ASCII, one of the documented
|
|
83
|
+
* differences in `oracle/ledger.ts`. There is no escape character, so a literal
|
|
84
|
+
* `%` or `_` cannot be matched; a pattern is a teaching device here, not a
|
|
85
|
+
* regular expression engine.
|
|
86
|
+
*/
|
|
87
|
+
export function likeMatches(text, pattern) {
|
|
88
|
+
let re = likeCache.get(pattern);
|
|
89
|
+
if (!re) {
|
|
90
|
+
const body = [...pattern]
|
|
91
|
+
.map((ch) => (ch === '%' ? '[\\s\\S]*' : ch === '_' ? '[\\s\\S]' : ch.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&')))
|
|
92
|
+
.join('');
|
|
93
|
+
re = new RegExp(`^${body}$`, 's');
|
|
94
|
+
likeCache.set(pattern, re);
|
|
95
|
+
}
|
|
96
|
+
return re.test(text);
|
|
97
|
+
}
|
|
98
|
+
const negate = (t, negated) => (negated && t !== null ? !t : t);
|
|
99
|
+
export function evaluatePredicate(expr, row) {
|
|
100
|
+
switch (expr.kind) {
|
|
101
|
+
case 'compare':
|
|
102
|
+
return compare(expr.op, evaluateValue(expr.left, row), evaluateValue(expr.right, row));
|
|
103
|
+
case 'and': {
|
|
104
|
+
const left = evaluatePredicate(expr.left, row);
|
|
105
|
+
const right = evaluatePredicate(expr.right, row);
|
|
106
|
+
// FALSE dominates: FALSE AND UNKNOWN is FALSE, not UNKNOWN.
|
|
107
|
+
if (left === false || right === false)
|
|
108
|
+
return false;
|
|
109
|
+
if (left === null || right === null)
|
|
110
|
+
return null;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
case 'or': {
|
|
114
|
+
const left = evaluatePredicate(expr.left, row);
|
|
115
|
+
const right = evaluatePredicate(expr.right, row);
|
|
116
|
+
// TRUE dominates: TRUE OR UNKNOWN is TRUE, not UNKNOWN.
|
|
117
|
+
if (left === true || right === true)
|
|
118
|
+
return true;
|
|
119
|
+
if (left === null || right === null)
|
|
120
|
+
return null;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
case 'not': {
|
|
124
|
+
const operand = evaluatePredicate(expr.operand, row);
|
|
125
|
+
return operand === null ? null : !operand; // NOT UNKNOWN is UNKNOWN
|
|
126
|
+
}
|
|
127
|
+
case 'isNull': {
|
|
128
|
+
// The one test that is never UNKNOWN — it exists to ask about NULL directly.
|
|
129
|
+
const isNull = evaluateValue(expr.operand, row) === null;
|
|
130
|
+
return expr.negated ? !isNull : isNull;
|
|
131
|
+
}
|
|
132
|
+
case 'in': {
|
|
133
|
+
const value = evaluateValue(expr.operand, row);
|
|
134
|
+
if (value === null)
|
|
135
|
+
return null; // NULL IN (…) is UNKNOWN, whatever the list holds
|
|
136
|
+
let sawUnknown = false;
|
|
137
|
+
for (const item of expr.items) {
|
|
138
|
+
const equal = compare('=', value, evaluateValue(item, row));
|
|
139
|
+
if (equal === true)
|
|
140
|
+
return negate(true, expr.negated);
|
|
141
|
+
if (equal === null)
|
|
142
|
+
sawUnknown = true;
|
|
143
|
+
}
|
|
144
|
+
// Not found: FALSE — unless a NULL in the list left the answer unknown.
|
|
145
|
+
return negate(sawUnknown ? null : false, expr.negated);
|
|
146
|
+
}
|
|
147
|
+
case 'like': {
|
|
148
|
+
const value = evaluateValue(expr.operand, row);
|
|
149
|
+
const pattern = evaluateValue(expr.pattern, row);
|
|
150
|
+
if (value === null || pattern === null)
|
|
151
|
+
return null;
|
|
152
|
+
return negate(likeMatches(textOf(value), textOf(pattern)), expr.negated);
|
|
153
|
+
}
|
|
154
|
+
case 'aggregate':
|
|
155
|
+
case 'arith':
|
|
156
|
+
case 'neg': {
|
|
157
|
+
// A number used where a truth value is wanted: non-zero is TRUE, NULL is UNKNOWN.
|
|
158
|
+
const value = evaluateValue(expr, row);
|
|
159
|
+
return value === null ? null : Boolean(value);
|
|
160
|
+
}
|
|
161
|
+
case 'literal':
|
|
162
|
+
return expr.value === null ? null : Boolean(expr.value);
|
|
163
|
+
case 'column': {
|
|
164
|
+
const value = row[columnKey(expr)] ?? null;
|
|
165
|
+
return value === null ? null : Boolean(value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Only TRUE passes a WHERE clause. UNKNOWN does not. */
|
|
170
|
+
export function passes(expr, row) {
|
|
171
|
+
if (!expr)
|
|
172
|
+
return true;
|
|
173
|
+
return evaluatePredicate(expr, row) === true;
|
|
174
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes an `INSERT`: writes the new row onto the heap page it lands on —
|
|
3
|
+
* the last page, if it has room, or a fresh one otherwise — and, if the
|
|
4
|
+
* table is indexed, adds its key to the B+Tree with the real `insert`
|
|
5
|
+
* algorithm, real leaf/internal splits included (plan.md §22.2, the write
|
|
6
|
+
* path's second statement).
|
|
7
|
+
*
|
|
8
|
+
* There is no scan and nothing to find, so unlike `DELETE` this needs no
|
|
9
|
+
* find-then-mutate pass: one row, one page, at most one tree insert.
|
|
10
|
+
*/
|
|
11
|
+
import { enforceUniqueOnInsert } from "./unique.js";
|
|
12
|
+
import { entryFor, height, indexNameOf, indexSpecsOf, insert, keyText, keyValuesOf, pageCountFor } from "../index/index.js";
|
|
13
|
+
/** Where the new row lands: the last heap page, if it has room, or a fresh one. */
|
|
14
|
+
function targetPage(ctx) {
|
|
15
|
+
const lastPage = ctx.heap.pages[ctx.heap.pages.length - 1];
|
|
16
|
+
const hasRoom = lastPage.rows.length < ctx.options.rowsPerPage;
|
|
17
|
+
return hasRoom
|
|
18
|
+
? { pageId: lastPage.pageId, slot: lastPage.rows.length, isNewPage: false }
|
|
19
|
+
: { pageId: pageCountFor(ctx.heap, ctx.trees), slot: 0, isNewPage: true };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One clause summarising what the B+Tree insert had to do beyond the plain
|
|
23
|
+
* add. Exported: `exec/update.ts` reuses it for the "add the new key" half
|
|
24
|
+
* of a reindex.
|
|
25
|
+
*/
|
|
26
|
+
export function summarizeInsert(nodesBefore, nodesAfter, heightBefore, heightAfter) {
|
|
27
|
+
if (nodesAfter === nodesBefore)
|
|
28
|
+
return 'the leaf had room, so no split was needed';
|
|
29
|
+
const added = nodesAfter - nodesBefore;
|
|
30
|
+
return `a split added ${added} node${added === 1 ? '' : 's'}${heightAfter > heightBefore ? `, and the tree grew to height ${heightAfter}` : ''}`;
|
|
31
|
+
}
|
|
32
|
+
/** Adds `row`'s entry to every index the table declares, skipping one whose leading column is NULL. */
|
|
33
|
+
function insertIntoIndex(ctx, row, pointer) {
|
|
34
|
+
for (const spec of indexSpecsOf(ctx.table)) {
|
|
35
|
+
const name = indexNameOf(spec.columns);
|
|
36
|
+
const tree = ctx.trees[name];
|
|
37
|
+
if (!tree)
|
|
38
|
+
continue;
|
|
39
|
+
const values = keyValuesOf(row, spec.columns);
|
|
40
|
+
if (values[0] === null)
|
|
41
|
+
continue; // a NULL leading column is never indexed — nothing to add
|
|
42
|
+
const nodesBefore = Object.keys(tree.nodes).length;
|
|
43
|
+
const heightBefore = height(tree);
|
|
44
|
+
const { key, pointer: stored } = entryFor(spec.columns, row, pointer, ctx.table.clusteredKey);
|
|
45
|
+
insert(tree, key, stored);
|
|
46
|
+
const nodesAfter = Object.keys(tree.nodes).length;
|
|
47
|
+
const heightAfter = height(tree);
|
|
48
|
+
ctx.emit(`The \`${name}\` B+Tree gets key ${keyText(values)} — ${summarizeInsert(nodesBefore, nodesAfter, heightBefore, heightAfter)}.`, { stage: 'execute', op: 'Insert', rowsProduced: 1 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function executeInsert(row, ctx) {
|
|
52
|
+
// A UNIQUE index is asked first — a rejected INSERT must not have touched the heap or any other tree.
|
|
53
|
+
enforceUniqueOnInsert(ctx, row);
|
|
54
|
+
const { pageId, slot, isNewPage } = targetPage(ctx);
|
|
55
|
+
ctx.locks.acquire(pageId, 'exclusive', isNewPage
|
|
56
|
+
? `Take an exclusive lock on page ${pageId} — a fresh page for the new row, since the last one is full.`
|
|
57
|
+
: `Take an exclusive lock on page ${pageId} — INSERT is about to add a row to it.`);
|
|
58
|
+
if (isNewPage) {
|
|
59
|
+
const written = ctx.pool.write(pageId);
|
|
60
|
+
if (written)
|
|
61
|
+
ctx.pool.unpin(written.frameId);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const fetched = ctx.pool.fetch(pageId);
|
|
65
|
+
if (fetched)
|
|
66
|
+
ctx.pool.unpin(fetched.frameId);
|
|
67
|
+
ctx.pool.markDirty(pageId);
|
|
68
|
+
}
|
|
69
|
+
ctx.emit(isNewPage
|
|
70
|
+
? `Insert writes the new row onto a fresh page, ${pageId} — the last page was full.`
|
|
71
|
+
: `Insert writes the new row onto page ${pageId}, which had room for it.`, { stage: 'execute', op: 'Insert', rowsProduced: 1 });
|
|
72
|
+
ctx.locks.release(pageId, 'exclusive');
|
|
73
|
+
insertIntoIndex(ctx, row, { pageId, slot });
|
|
74
|
+
return { row };
|
|
75
|
+
}
|