import test from "node:test"; import assert from "node:assert"; import initSqlJs from "sql.js/dist/sql-asm.js"; import { covers, minimize, unionDiff } from "../lib/common/expression/synth.ts"; import { parse, stringify } from "../lib/common/expression/parser.ts"; const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"]; const DECIMAL_VALUES = [null, 0, -10, 10]; let db; async function query(filter: string): Promise> { if (!db) { const sql = await initSqlJs(); db = new sql.Database(); db.run( "CREATE TABLE test (id INTEGER PRIMARY KEY, string STRING, decimal DECIMAL(4,2))", ); const stmt = db.prepare("INSERT INTO test (string, decimal) VALUES (?, ?)"); const count = STRING_VALUES.length * DECIMAL_VALUES.length; for (let i = 0; i < count; ++i) { const str = i % STRING_VALUES.length; const dec = Math.trunc(i / STRING_VALUES.length) % DECIMAL_VALUES.length; stmt.run([STRING_VALUES[str], DECIMAL_VALUES[dec]]); } stmt.free(); } const res = db.exec(`SELECT id FROM test WHERE ${filter}`); if (!res.length) return new Set(); return new Set(res[0].values.flat()); } function setsEqual(set1: Set, set2: Set): boolean { if (set1.size !== set2.size) return false; for (const s of set1) if (!set2.has(s)) return false; return true; } function getPermutations(...arrs: any[][]): any[][] { const count = arrs.reduce((total, arr) => total * arr.length, 1); const res = []; for (let i = 0; i < count; ++i) { let j = i; const row = []; for (const arr of arrs) { const v = arr[j % arr.length]; j = Math.trunc(j / arr.length); row.push(v); } res.push(row); } return res; } void test("minimize", async () => { const cases: string[] = []; cases.push("null"); cases.push("false"); cases.push("true"); cases.push("string"); cases.push("(string + decimal) IS NULL"); cases.push("(string + decimal) = NULL"); cases.push("COALESCE(string, decimal) = 0"); for (const [s1, s2, s3, op1, op2] of getPermutations( STRING_VALUES.filter((s) => s), STRING_VALUES.filter((s) => s), STRING_VALUES.filter((s) => s), [">", "=", "<"], ["<>", ">="], )) { cases.push( `string ${op1} "${s1}" OR string ='${s2}' OR NOT string ${op2} '${s3}'`, ); } for (const [s1, s2] of getPermutations( STRING_VALUES.filter((s) => s), STRING_VALUES.filter((s) => s), )) cases.push(`string > "${s1}" AND string < '${s2}'`); for (const c of cases) { const res1 = await query(c); const min = stringify(minimize(parse(c), true)); const res2 = await query(min); assert.strictEqual(setsEqual(res1, res2), true); } }); void test("unionDiff", async () => { const cases = [ "true", "decimal > 0", "decimal > 10", "UPPER(string || decimal) LIKE 'AB10'", "COALESCE(string, decimal) = 0", ]; for (const [c1, c2] of getPermutations(cases, cases)) { const res1 = await query(c1); const res2 = await query(c2); const [union, diff] = unionDiff(parse(c1), parse(c2)); const res3 = await query(stringify(union)); const res4 = await query(stringify(diff)); const unionSet = new Set([...res1, ...res2]); const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r))); assert.strictEqual(setsEqual(res3, unionSet), true); assert.strictEqual(setsEqual(res4, diffSet), true); } }); void test("covers", async () => { assert.strictEqual(covers(false, false), true); assert.strictEqual(covers(false, parse("decimal > 5 AND decimal < 3")), true); assert.strictEqual(covers(parse("true"), parse("decimal > 0")), true); assert.strictEqual(covers(parse("true"), parse("false")), true); assert.strictEqual(covers(parse("false"), parse("false")), true); assert.strictEqual(covers(parse("false"), parse("decimal > 0")), false); assert.strictEqual(covers(parse("decimal >= 0"), parse("decimal > 0")), true); assert.strictEqual( covers(parse("decimal > 0"), parse("decimal >= 0")), false, ); const cases = [ ["decimal >= 0", "decimal > 0"], ["decimal > 0", "decimal > 5"], ["string IS NOT NULL", "string = 'a'"], ["true", "decimal > 0"], ]; for (const [c1, c2] of cases) { const res1 = await query(c1); const res2 = await query(c2); const coversResult = covers(parse(c1), parse(c2)); const actuallyCovers = Array.from(res2).every((r) => res1.has(r)); assert.strictEqual( coversResult, actuallyCovers, `covers(${c1}, ${c2}) should match actual coverage`, ); } });