pi-mono/packages/chord/test/store/expression.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

152 lines
5.5 KiB
TypeScript

// Tests of the table expression language: the builders and their compilation into the data vocabulary.
import { describe, expect, it } from "vitest";
import { bool, int, nul, num, obj, opt, str } from "../../src/contracts/index.ts";
import {
add,
and,
coalesce,
compileSet,
compileWhere,
concat,
eq,
gt,
gte,
inArray,
isNull,
lt,
lte,
max,
ne,
not,
or,
prefix,
replacePrefix,
sub,
when,
} from "../../src/store/expression.ts";
import { json } from "../../src/store/json-column.ts";
import type { ColumnRefs } from "../../src/store/query.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("compiling conditions", () => {
it("compiles every operator into data", () => {
const where = and(
eq(refs.path, "a"),
ne(refs.n, 1),
lt(refs.n, 2),
lte(refs.x, 2.5),
gt(refs.n, 0),
gte(refs.path, "a"),
inArray(refs.n, [1, 2]),
isNull(refs.note),
prefix(refs.path, "a/"),
or(not(eq(refs.b, true))),
);
expect(compileWhere(T, where, "where")).toEqual({
and: [
{ eq: ["path", "a"] },
{ ne: ["n", 1] },
{ lt: ["n", 2] },
{ lte: ["x", 2.5] },
{ gt: ["n", 0] },
{ gte: ["path", "a"] },
{ inArray: ["n", [1, 2]] },
{ isNull: "note" },
{ prefix: ["path", "a/"] },
{ or: [{ not: { eq: ["b", true] } }] },
],
});
});
it("refuses undefined and null operands (rule 16), wrong types, json and unknown columns", () => {
expect(() => compileWhere(T, eq(refs.note, undefined as never), "where")).toThrow("ask with isNull");
expect(() => compileWhere(T, eq(refs.note, null as never), "where")).toThrow(args);
expect(() => compileWhere(T, eq(refs.n, "1" as never), "where")).toThrow(args);
expect(() => compileWhere(T, eq(refs.n, 1.5), "where")).toThrow(args);
expect(() => compileWhere(T, eq(refs.meta as never, 1), "where")).toThrow("not queryable");
expect(() => compileWhere(T, eq({ column: "nope" } as never, 1), "where")).toThrow("expected a column reference");
expect(() => compileWhere(T, "x", "where")).toThrow("expected a condition");
expect(() => compileWhere(T, prefix(refs.n as never, "a"), "where")).toThrow("str() column");
});
it("makes text operands storable", () => {
expect(compileWhere(T, eq(refs.path, "a\uD800"), "where")).toEqual({ eq: ["path", "a\uFFFD"] });
expect(() => compileWhere(T, prefix(refs.path, "a\0"), "where")).toThrow("NUL");
});
});
describe("compiling sets", () => {
it("compiles plain values and expressions into one vocabulary, a clear as null (rule 14)", () => {
expect(compileSet(T, { path: "p", note: undefined, b: true, meta: { a: 1 } }, "update")).toEqual({
path: "p",
note: null,
b: true,
meta: { json: { a: 1 } },
});
const set = compileSet(
T,
{
n: add(refs.n, 1),
count: sub(coalesce(refs.count, 0), refs.n),
x: max(refs.x, 1.5),
path: replacePrefix(concat(refs.path, "/"), "a/", "b/"),
note: when(gt(refs.n, 1), refs.path, refs.note),
},
"update",
);
expect(JSON.parse(JSON.stringify(set))).toEqual({
n: { add: [{ col: "n" }, 1] },
count: { sub: [{ coalesce: [{ col: "count" }, 0] }, { col: "n" }] },
x: { max: [{ col: "x" }, 1.5] },
path: { replacePrefix: [{ concat: [{ col: "path" }, "/"] }, "a/", "b/"] },
note: { when: [{ gt: ["n", 1] }, { col: "path" }, { col: "note" }] },
});
});
it("refuses an empty set, key columns (rule 13), unknown columns and clears of non-opt columns", () => {
expect(() => compileSet(T, {}, "update")).toThrow("empty");
expect(() => compileSet(T, { id: "x" }, "update")).toThrow("cannot change");
expect(() => compileSet(T, { nope: 1 }, "update")).toThrow('no column "nope"');
expect(() => compileSet(T, { path: undefined }, "update")).toThrow("cannot be cleared");
});
it("undefined clears an opt column; null is a json(...) cell's JSON null and refused for scalar columns", () => {
expect(compileSet(T, { note: undefined, meta: undefined }, "update")).toEqual({ note: null, meta: null });
const Nullable = defineTable("n", {
version: 1,
columns: { id: str(), data: json(nul(), { version: 1 }) },
key: ["id"],
});
expect(compileSet(Nullable, { data: null }, "update")).toEqual({ data: { json: null } });
expect(() => compileSet(T, { note: null as never }, "update")).toThrow("null is no str value");
});
it("refuses integer arithmetic outside int columns (rule 10), type mismatches and nullable results into non-opt columns", () => {
expect(() => compileSet(T, { x: add(refs.n, 1) }, "update")).toThrow("add gives int");
expect(() => compileSet(T, { path: max(refs.n, 1) as never }, "update")).toThrow("gives int or num");
expect(() => compileSet(T, { path: refs.n as never }, "update")).toThrow('column "n" is int, not str');
expect(() => compileSet(T, { n: add(refs.count, 1) }, "update")).toThrow("may be NULL");
expect(() => compileSet(T, { path: refs.note }, "update")).toThrow("may be NULL");
expect(compileSet(T, { path: coalesce(refs.note, "d") }, "update")).toEqual({
path: { coalesce: [{ col: "note" }, "d"] },
});
expect(() => compileSet(T, { meta: refs.meta as never }, "update")).toThrow("plain values");
});
});