Files
genieacs/test/pagination.ts
Zaid Humaimidi 6f3ce6f7c8 refactor: replace array-based expressions with typed class hierarchy
Replace the old Lisp-like array expression representation
(["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in
an abstract Expression class (Literal, Parameter, Binary, Unary,
FunctionCall, Conditional). This gives compile-time type safety
to expression construction, evaluation, and pattern matching.

Key changes:
- New lib/common/expression.ts defines the class hierarchy with
  map/evaluate/toString methods
- Rewrite parser as hand-rolled recursive descent, removing the
  parsimmon dependency
- Extend Path with a colon separator to unify parameter identity
  and attributes into a single address space (e.g.
  "Device.WiFi.SSID.1.Name:writable"). The colon field tracks
  how many trailing segments are attribute qualifiers. PathSet
  rewritten with separate param/attr segment indexes
- Flatten device attribute representation in the UI to
  colon-delimited keys mirroring the new path syntax
2026-03-12 14:58:20 -07:00

121 lines
3.6 KiB
TypeScript

import test from "node:test";
import assert from "node:assert";
import initSqlJs from "sql.js/dist/sql-asm.js";
import {
bookmarkToExpression,
paginate,
toBookmark,
} from "../lib/common/expression/pagination.ts";
import Expression from "../lib/common/expression.ts";
import { covers, minimize } from "../lib/common/expression/synth.ts";
const VALUES = [null, -1, false, "a"];
const PARAMS = ["param1", "param2"];
let db;
async function query(filter: string): Promise<{ id: string }[]> {
if (!db) {
const sql = await initSqlJs();
db = new sql.Database();
db.run(`CREATE TABLE test (id INTEGER PRIMARY KEY, ${PARAMS.join(", ")})`);
const stmt = db.prepare(
`INSERT INTO test (${PARAMS.join(", ")}) VALUES (${PARAMS.map(
() => "?",
).join(", ")})`,
);
const count = VALUES.length ** PARAMS.length;
for (let i = 0; i < count; ++i) {
const values: (boolean | number | string)[] = [];
for (let j = 0; j < PARAMS.length; ++j)
values.push(VALUES[Math.trunc(i / VALUES.length ** j) % VALUES.length]);
stmt.run(values);
}
stmt.free();
}
const res = db.exec(`SELECT * FROM test WHERE ${filter}`);
if (!res.length) return [];
return res[0].values.map((row) =>
Object.fromEntries(row.map((v, i) => [res[0].columns[i], v])),
);
}
function getAllSortOrders(columns: string[]): Array<Record<string, number>> {
const sortOrders: Array<Record<string, number>> = [];
function generateOrders(
remaining: string[],
current: Record<string, number>,
): void {
if (remaining.length === 0) {
sortOrders.push({ ...current });
return;
}
for (const column of remaining) {
const newRemaining = remaining.filter((c) => c !== column);
current[column] = -1;
generateOrders(newRemaining, current);
current[column] = 1;
generateOrders(newRemaining, current);
delete current[column];
}
}
generateOrders(columns, {});
return sortOrders;
}
async function testPaginate(
q1: Expression,
q2: Expression,
sort: Record<string, number>,
): Promise<void> {
const orderBy = Object.entries(sort)
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
.join(", ");
const allMatches = await query(`${q2.toString()} ORDER BY ${orderBy}`);
const [fulfilled, diff] = paginate(q1, q2, sort);
assert.ok(covers(q1, fulfilled));
const fulfilledMatches = await query(
`${fulfilled.toString()} ORDER BY ${orderBy}`,
);
const diffMatches = await query(`${diff.toString()} ORDER BY ${orderBy}`);
assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]);
if (allMatches.length === fulfilledMatches.length) return;
const nextMatches = allMatches.slice(
fulfilledMatches.length,
fulfilledMatches.length + 1,
);
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
const bookmarkFilter = bookmarkToExpression(bookmark, sort);
const capped = Expression.and(q2, bookmarkFilter);
assert.ok(!covers(q1, capped));
const cappedMatches = await query(
`(${capped.toString()}) ORDER BY ${orderBy}`,
);
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
const min = minimize(Expression.or(q1, capped));
await testPaginate(min, q2, sort);
}
void test("paginate", async () => {
const cases: [string, string][] = [["param2 < 'a'", "param1 >= 'a'"]];
const params = ["id", ...PARAMS];
const sortOrders = getAllSortOrders(params);
for (const [q1, q2] of cases)
for (const sort of sortOrders) {
await testPaginate(Expression.parse(q1), Expression.parse(q2), sort);
}
});