pi-mono/packages/chord/test/store/evaluate.test.ts
Mario Zechner e9c0ff4d84 feat(chord): Chord v2 is the package's root
src/next is src (the main export), src/next-node is src/node with short subpaths (/http, /workers, /plugins,
/plugins/runtime, /app, /surfaces, /web, /react, /tui, /tui/launcher); the foundations get /json and /watch;
Chord v1 moves to src/old (/old, /old/node) until p2 runs on v2; tests and docs move likewise (test/, docs/,
test/old, docs/old). Durable and env import the foundations from their subpaths.
2026-11-02 22:36:55 +00:00

211 lines
7.9 KiB
TypeScript

// Tests of the JavaScript evaluator of the table vocabulary, and that it agrees with SQLite on the same nodes.
import { describe, expect, it } from "vitest";
import { BACKGROUND_CONTEXT as context } from "../../src/context/index.ts";
import { bool, int, num, obj, opt, str } from "../../src/contracts/index.ts";
import type { SqliteValue } from "../../src/sqlite/index.ts";
import { openNodeSqliteDatabase } from "../../src/sqlite/node.ts";
import { applySet, type EvaluatedRow, matches } from "../../src/store/evaluate.ts";
import {
add,
and,
coalesce,
compileSet,
compileWhere,
concat,
eq,
gt,
gte,
inArray,
isNull,
lt,
lte,
max,
min,
ne,
not,
or,
prefix,
replacePrefix,
sub,
type WhereNode,
when,
} from "../../src/store/expression.ts";
import { json } from "../../src/store/json-column.ts";
import type { ColumnRefs } from "../../src/store/query.ts";
import { createTableSql, TableSql, whereSql } from "../../src/store/sql.ts";
import { defineTable } from "../../src/store/table.ts";
const T = defineTable("t", {
version: 1,
columns: {
id: str(),
path: str(),
n: int(),
x: num(),
b: bool(),
note: opt(str()),
count: opt(int()),
meta: opt(json(obj({ a: int() }), { version: 1 })),
},
key: ["id"],
});
type C = typeof T extends { "~columns"?: infer X } ? NonNullable<X> : never;
const refs = T.refs as ColumnRefs<C>;
const args = expect.objectContaining({ code: "invalid", reason: "args" });
describe("the evaluator", () => {
const row: EvaluatedRow = { id: "r", path: "a/x", n: 2, x: 1, b: true, count: 3 };
it("follows SQL's NULL rules: a comparison with NULL never matches, not and ne included", () => {
const where = (condition: unknown) => compileWhere(T, condition, "where");
expect(matches(where(eq(refs.note, "a")), row)).toBe(false);
expect(matches(where(ne(refs.note, "a")), row)).toBe(false);
expect(matches(where(not(eq(refs.note, "a"))), row)).toBe(false);
expect(matches(where(or(isNull(refs.note), ne(refs.note, "a"))), row)).toBe(true);
expect(matches(where(and()), row)).toBe(true);
expect(matches(where(or()), row)).toBe(false);
expect(matches(where(inArray(refs.n, [])), row)).toBe(false);
});
it("evaluates every right side against the row before (a swap)", () => {
const Swap = defineTable("s", { version: 1, columns: { id: str(), a: str(), b: str() }, key: ["id"] });
const set = compileSet(
Swap,
{ a: (Swap.refs as { b: unknown }).b, b: (Swap.refs as { a: unknown }).a },
"update",
);
expect(applySet(set, { id: "1", a: "x", b: "y" }, "s")).toEqual({ id: "1", a: "y", b: "x" });
});
it("leaves a value without the prefix unchanged and refuses integers outside the safe range (rule 10)", () => {
expect(applySet(compileSet(T, { path: replacePrefix(refs.path, "b/", "c/") }, "u"), row, "t").path).toBe("a/x");
expect(applySet(compileSet(T, { path: replacePrefix(refs.path, "a/", "") }, "u"), row, "t").path).toBe("x");
const big = { ...row, n: Number.MAX_SAFE_INTEGER };
expect(() => applySet(compileSet(T, { n: add(refs.n, 1) }, "u"), big, "t")).toThrow(args);
});
});
describe("one semantics: the evaluator agrees with SQLite", () => {
const ROWS: EvaluatedRow[] = [
{ id: "1", path: "a/x", n: 1, x: 0.5, b: true, note: "z", count: 1 },
{ id: "2", path: "a/\u{1F600}", n: 2, x: 2, b: false },
{ id: "3", path: "a\uFFFF", n: -3, x: -1, b: true, note: "" },
{ id: "4", path: "b", n: 0, x: 0, b: false, count: 0 },
{ id: "5", path: "", n: 9, x: 9.5, b: true, note: "a/" },
{ id: "6", path: "\u{10FFFF}", n: 5, x: 1, b: false, count: -2 },
];
const CONDITIONS = [
eq(refs.note, "z"),
ne(refs.note, "z"),
not(ne(refs.count, 0)),
lt(refs.path, "a\uFFFF"),
gte(refs.path, "a/\u{1F600}"),
prefix(refs.path, "a"),
prefix(refs.path, ""),
prefix(refs.path, "\u{10FFFF}"),
inArray(refs.path, ["b", "", "x"]),
inArray(refs.b, [true]),
or(isNull(refs.count), gt(refs.x, 1)),
and(not(isNull(refs.note)), lte(refs.n, 1)),
not(or(eq(refs.b, true), lt(refs.count, 0))),
];
async function table() {
const db = await openNodeSqliteDatabase(":memory:");
await db.run(createTableSql("t", T.record, T.key));
const sql = new TableSql("t", T.columns, T.rows.physical, T.key);
for (const row of ROWS) await db.run(sql.put, ...T.rows.put(row, 1).params);
return { db, sql };
}
it("selects the rows the evaluator matches", async () => {
const { db } = await table();
for (const condition of CONDITIONS) {
const node = compileWhere(T, condition, "where");
const params: SqliteValue[] = [];
const text = whereSql(node, params);
const rows = await db.all<{ id: string }>(`SELECT id FROM t WHERE ${text} ORDER BY id`, ...params);
const expected = ROWS.filter((row) => matches(node, row)).map(({ id }) => id);
expect([JSON.stringify(node), rows.map(({ id }) => id)]).toEqual([JSON.stringify(node), expected]);
}
await db.close();
});
it("agrees on every operator's NULL truth under not, and, or and when", async () => {
const PREDICATES = [
eq(refs.note, "z"),
ne(refs.note, "z"),
lt(refs.count, 1),
lte(refs.count, 0),
gt(refs.note, "a"),
gte(refs.count, 0),
inArray(refs.note, ["z", ""]),
inArray(refs.note, []),
prefix(refs.note, "a"),
prefix(refs.note, ""),
isNull(refs.count),
];
const WRAPPED = PREDICATES.flatMap((p) => [
p,
not(p),
and(p, eq(refs.b, true)),
or(p, eq(refs.b, true)),
not(and(p, eq(refs.b, true))),
not(or(p, eq(refs.b, false))),
]);
const { db } = await table();
for (const condition of WRAPPED) {
const node = compileWhere(T, condition, "where");
const params: SqliteValue[] = [];
const text = whereSql(node, params);
const rows = await db.all<{ id: string }>(`SELECT id FROM t WHERE ${text} ORDER BY id`, ...params);
const expected = ROWS.filter((row) => matches(node, row)).map(({ id }) => id);
expect([JSON.stringify(node), rows.map(({ id }) => id)]).toEqual([JSON.stringify(node), expected]);
}
await db.close();
for (const condition of WRAPPED) {
const { db, sql } = await table();
const node = compileSet(T, { n: when(condition, 1, 2) }, "updateWhere");
const statement = sql.updateWhere({ and: [] } as WhereNode, node, new Map());
await db.run(statement.sql, ...statement.params);
const stored = await db.all<{ [column: string]: SqliteValue }>(`SELECT * FROM t ORDER BY id`);
const read = stored.map((row) => T.rows.decode(row, context));
expect([JSON.stringify(node), read]).toEqual([
JSON.stringify(node),
ROWS.map((row) => applySet(node, row, "t")),
]);
await db.close();
}
});
it("binds a long inArray list as one parameter (beyond SQLite's 32,766 variables)", async () => {
const { db } = await table();
const values = Array.from({ length: 40_000 }, (_, i) => `v${i}`);
values.push("b");
const node = compileWhere(T, inArray(refs.path, values), "where");
const params: SqliteValue[] = [];
const text = whereSql(node, params);
expect(params).toHaveLength(1);
const rows = await db.all<{ id: string }>(`SELECT id FROM t WHERE ${text}`, ...params);
expect(rows.map(({ id }) => id)).toEqual(["4"]);
await db.close();
});
it("updates rows as the evaluator does", async () => {
const SETS = [
{ n: add(refs.n, 10), count: sub(coalesce(refs.count, 5), 1) },
{ path: replacePrefix(refs.path, "a/", "\u{1F600}/"), note: concat(refs.path, "!") },
{ x: min(refs.x, 1), n: max(refs.n, 1), note: when(prefix(refs.path, "a"), refs.path, refs.note) },
{ note: refs.path, path: coalesce(refs.note, "none"), count: undefined },
];
for (const set of SETS) {
const { db, sql } = await table();
const node = compileSet(T, set, "updateWhere");
const statement = sql.updateWhere({ and: [] } as WhereNode, node, new Map());
await db.run(statement.sql, ...statement.params);
const stored = await db.all<{ [column: string]: SqliteValue }>(`SELECT * FROM t ORDER BY id`);
const read = stored.map((row) => T.rows.decode(row, context));
expect(read).toEqual(ROWS.map((row) => applySet(node, row, "t")));
await db.close();
}
});
});