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
This commit is contained in:
1 parent
75908e624f
commit
6f3ce6f7c8
71 files changed
+4045
-3449
No files matched your search
+6
-9
@@ -2,13 +2,13 @@ import test from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { EJSON } from "bson";
|
||||
import { Filter } from "mongodb";
|
||||
import { stringify, parse } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import { convertOldPrecondition } from "../lib/db/util.ts";
|
||||
import { toMongoQuery } from "../lib/db/synth.ts";
|
||||
|
||||
void test("convertOldPrecondition", () => {
|
||||
const tests = [
|
||||
[{}, "true"],
|
||||
[{}, "TRUE"],
|
||||
[{ test: "test" }, 'test = "test"'],
|
||||
[{ test: { $eq: "test" } }, 'test = "test"'],
|
||||
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
|
||||
@@ -31,7 +31,7 @@ void test("convertOldPrecondition", () => {
|
||||
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
|
||||
[
|
||||
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||
'test = "test" OR (test <> "test" OR test IS NULL)',
|
||||
'test = "test" OR test <> "test" OR test IS NULL',
|
||||
],
|
||||
[
|
||||
{ test: { $gte: "test1", $ne: "test2" } },
|
||||
@@ -54,7 +54,7 @@ void test("convertOldPrecondition", () => {
|
||||
|
||||
for (const t of tests) {
|
||||
assert.strictEqual(
|
||||
stringify(convertOldPrecondition(t[0] as Record<string, unknown>)),
|
||||
convertOldPrecondition(t[0] as Record<string, unknown>).toString(),
|
||||
t[1],
|
||||
);
|
||||
}
|
||||
@@ -86,7 +86,6 @@ void test("toMongoQuery", async () => {
|
||||
"Param1 <> 1657844103524",
|
||||
{
|
||||
"Param1._value": { $ne: 1657844103524 },
|
||||
|
||||
$and: [
|
||||
{
|
||||
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
|
||||
@@ -114,7 +113,6 @@ void test("toMongoQuery", async () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
"LOWER(Param1) LIKE 'value'",
|
||||
{
|
||||
@@ -146,7 +144,7 @@ void test("toMongoQuery", async () => {
|
||||
];
|
||||
|
||||
for (const [expStr, expect] of queries) {
|
||||
const exp = parse(expStr);
|
||||
const exp = Expression.parse(expStr);
|
||||
let query = toMongoQuery(exp, "devices");
|
||||
if (query) query = EJSON.serialize(query);
|
||||
assert.deepStrictEqual(query, expect);
|
||||
@@ -156,11 +154,10 @@ void test("toMongoQuery", async () => {
|
||||
["Param1 = Param2", "Right-hand operand must be a literal value"],
|
||||
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
|
||||
["NOW() = 1", "Left-hand operand must be a parameter"],
|
||||
["param{param2} = 1", "Left-hand operand must be a parameter"],
|
||||
];
|
||||
|
||||
for (const [expStr, err] of failQueries) {
|
||||
const exp = parse(expStr);
|
||||
const exp = Expression.parse(expStr);
|
||||
assert.throws(() => toMongoQuery(exp, "devices"), {
|
||||
message: err,
|
||||
});
|
||||
|
||||
+22
-19
@@ -7,7 +7,7 @@ import VersionedMap from "../lib/versioned-map.ts";
|
||||
import { Attributes } from "../lib/types.ts";
|
||||
|
||||
void test("getAliasDeclarations", () => {
|
||||
const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b");
|
||||
const path = Path.parse("a.[aa = 10 AND bb.[aaa = 100].cc = 1].b");
|
||||
const decs = device.getAliasDeclarations(path, 99);
|
||||
|
||||
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
|
||||
@@ -32,66 +32,69 @@ void test("unpack", () => {
|
||||
changes: new Set<string>(),
|
||||
};
|
||||
|
||||
device.set(deviceData, Path.parse("a.1.b"), now, {
|
||||
device.set(deviceData, "a.1.b", now, {
|
||||
value: [now, ["b", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.c"), now, {
|
||||
device.set(deviceData, "a.1.c", now, {
|
||||
value: [now, ["c", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.1.a"), now, {
|
||||
device.set(deviceData, "a.1.a.1.a", now, {
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.1.b"), now, {
|
||||
device.set(deviceData, "a.1.a.1.b", now, {
|
||||
value: [now, ["b1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.1.c"), now, {
|
||||
device.set(deviceData, "a.1.a.1.c", now, {
|
||||
value: [now, ["c1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.2.a"), now, {
|
||||
device.set(deviceData, "a.1.a.2.a", now, {
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.2.b"), now, {
|
||||
device.set(deviceData, "a.1.a.2.b", now, {
|
||||
value: [now, ["b2", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.2.c"), now, {
|
||||
device.set(deviceData, "a.1.a.2.c", now, {
|
||||
value: [now, ["c2", "xsd:string"]],
|
||||
});
|
||||
|
||||
device.set(deviceData, Path.parse("a.2.b"), now, {
|
||||
device.set(deviceData, "a.2.b", now, {
|
||||
value: [now, ["b", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.c"), now, {
|
||||
device.set(deviceData, "a.2.c", now, {
|
||||
value: [now, ["c", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.1.a"), now, {
|
||||
device.set(deviceData, "a.2.a.1.a", now, {
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.1.b"), now, {
|
||||
device.set(deviceData, "a.2.a.1.b", now, {
|
||||
value: [now, ["b1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.1.c"), now, {
|
||||
device.set(deviceData, "a.2.a.1.c", now, {
|
||||
value: [now, ["c1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.2.a"), now, {
|
||||
device.set(deviceData, "a.2.a.2.a", now, {
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.2.b"), now, {
|
||||
device.set(deviceData, "a.2.a.2.b", now, {
|
||||
value: [now, ["c1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.2.c"), now, {
|
||||
device.set(deviceData, "a.2.a.2.c", now, {
|
||||
value: [now, ["b1", "xsd:string"]],
|
||||
});
|
||||
|
||||
let unpacked: Path[];
|
||||
unpacked = device.unpack(
|
||||
deviceData,
|
||||
Path.parse("a.[b:b,c:c].a.[b:b1,c:c1].a"),
|
||||
Path.parse("a.[b='b' AND c='c'].a.[b='b1' AND c='c1'].a"),
|
||||
);
|
||||
assert.strictEqual(unpacked.length, 2);
|
||||
assert.strictEqual(unpacked[0].toString(), "a.1.a.1.a");
|
||||
assert.strictEqual(unpacked[1].toString(), "a.2.a.1.a");
|
||||
|
||||
unpacked = device.unpack(deviceData, Path.parse("a.*.a.[b:c1,c:b1].a"));
|
||||
unpacked = device.unpack(
|
||||
deviceData,
|
||||
Path.parse("a.*.a.[b='c1' AND c='b1'].a"),
|
||||
);
|
||||
assert.strictEqual(unpacked.length, 1);
|
||||
assert.strictEqual(unpacked[0].toString(), "a.2.a.2.a");
|
||||
});
|
||||
+10
-10
@@ -1,13 +1,12 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert";
|
||||
import initSqlJs from "sql.js/dist/sql-asm.js";
|
||||
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import {
|
||||
bookmarkToExpression,
|
||||
paginate,
|
||||
toBookmark,
|
||||
} from "../lib/common/expression/pagination.ts";
|
||||
import { Expression } from "../lib/types.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import { covers, minimize } from "../lib/common/expression/synth.ts";
|
||||
|
||||
const VALUES = [null, -1, false, "a"];
|
||||
@@ -79,15 +78,15 @@ async function testPaginate(
|
||||
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
|
||||
.join(", ");
|
||||
|
||||
const allMatches = await query(`${stringify(q2)} ORDER BY ${orderBy}`);
|
||||
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(
|
||||
`${stringify(fulfilled)} ORDER BY ${orderBy}`,
|
||||
`${fulfilled.toString()} ORDER BY ${orderBy}`,
|
||||
);
|
||||
|
||||
const diffMatches = await query(`${stringify(diff)} ORDER BY ${orderBy}`);
|
||||
const diffMatches = await query(`${diff.toString()} ORDER BY ${orderBy}`);
|
||||
assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]);
|
||||
|
||||
if (allMatches.length === fulfilledMatches.length) return;
|
||||
@@ -98,14 +97,14 @@ async function testPaginate(
|
||||
);
|
||||
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
|
||||
const bookmarkFilter = bookmarkToExpression(bookmark, sort);
|
||||
const capped = ["AND", q2, bookmarkFilter];
|
||||
const capped = Expression.and(q2, bookmarkFilter);
|
||||
|
||||
assert.ok(!covers(q1, capped));
|
||||
const cappedMatches = await query(
|
||||
`(${stringify(capped)}) ORDER BY ${orderBy}`,
|
||||
`(${capped.toString()}) ORDER BY ${orderBy}`,
|
||||
);
|
||||
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
|
||||
const min = minimize(["OR", q1, capped]);
|
||||
const min = minimize(Expression.or(q1, capped));
|
||||
await testPaginate(min, q2, sort);
|
||||
}
|
||||
|
||||
@@ -115,6 +114,7 @@ void test("paginate", async () => {
|
||||
const sortOrders = getAllSortOrders(params);
|
||||
|
||||
for (const [q1, q2] of cases)
|
||||
for (const sort of sortOrders)
|
||||
await testPaginate(parse(q1), parse(q2), sort);
|
||||
for (const sort of sortOrders) {
|
||||
await testPaginate(Expression.parse(q1), Expression.parse(q2), sort);
|
||||
}
|
||||
});
|
||||
+24
-28
@@ -3,67 +3,63 @@ import assert from "node:assert";
|
||||
import Path from "../lib/common/path.ts";
|
||||
import PathSet from "../lib/common/path-set.ts";
|
||||
|
||||
void test("depth", () => {
|
||||
const pathSet = new PathSet();
|
||||
assert.strictEqual(pathSet.depth, 0);
|
||||
pathSet.add(Path.parse(""));
|
||||
assert.strictEqual(pathSet.depth, 1);
|
||||
pathSet.add(Path.parse("a"));
|
||||
assert.strictEqual(pathSet.depth, 2);
|
||||
});
|
||||
|
||||
void test("add", () => {
|
||||
const pathSet = new PathSet();
|
||||
pathSet.add(Path.parse("a"));
|
||||
pathSet.add(Path.parse("a"));
|
||||
assert.strictEqual(pathSet.find(Path.parse("a"), true, true, 99).length, 1);
|
||||
pathSet.add("a");
|
||||
pathSet.add("a");
|
||||
assert.strictEqual(
|
||||
pathSet.findCompat(Path.parse("a"), true, true, 99).length,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
void test("get", () => {
|
||||
const pathSet = new PathSet();
|
||||
pathSet.add(Path.parse("a.*"));
|
||||
pathSet.add(Path.parse("a.a"));
|
||||
pathSet.add(Path.parse("*.*"));
|
||||
pathSet.add("a.*");
|
||||
pathSet.add("a.a");
|
||||
pathSet.add("*.*");
|
||||
|
||||
assert.strictEqual(pathSet.get(Path.parse("a.*")).toString(), "a.*");
|
||||
assert.strictEqual(pathSet.get(Path.parse("*.a")), null);
|
||||
assert.strictEqual(pathSet.get("a.*").toString(), "a.*");
|
||||
assert.equal(pathSet.get("*.a"), null);
|
||||
});
|
||||
|
||||
void test("find", () => {
|
||||
const pathSet = new PathSet();
|
||||
pathSet.add(Path.parse("a"));
|
||||
pathSet.add(Path.parse("a.*"));
|
||||
pathSet.add(Path.parse("a.a"));
|
||||
pathSet.add(Path.parse("*.a"));
|
||||
pathSet.add(Path.parse("*.*"));
|
||||
pathSet.add("a");
|
||||
pathSet.add("a.*");
|
||||
pathSet.add("a.a");
|
||||
pathSet.add("*.a");
|
||||
pathSet.add("*.*");
|
||||
|
||||
assert.deepStrictEqual(
|
||||
pathSet.find(Path.parse(""), true, true, 1).map((p) => p.toString()),
|
||||
pathSet.findCompat(Path.root, true, true, 1).map((p) => p.toString()),
|
||||
["a"],
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
pathSet.find(Path.parse(""), false, false, 2).map((p) => p.toString()),
|
||||
pathSet.findCompat(Path.root, false, false, 2).map((p) => p.toString()),
|
||||
["a", "a.*", "a.a", "*.a", "*.*"],
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
pathSet.find(Path.parse("a.*"), false, false).map((p) => p.toString()),
|
||||
pathSet
|
||||
.findCompat(Path.parse("a.*"), false, false)
|
||||
.map((p) => p.toString()),
|
||||
["a.*"],
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
pathSet.find(Path.parse("a.*"), false, true).map((p) => p.toString()),
|
||||
pathSet.findCompat(Path.parse("a.*"), false, true).map((p) => p.toString()),
|
||||
["a.*", "a.a"],
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
pathSet.find(Path.parse("a.*"), true, false).map((p) => p.toString()),
|
||||
pathSet.findCompat(Path.parse("a.*"), true, false).map((p) => p.toString()),
|
||||
["a.*", "*.*"],
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
pathSet.find(Path.parse("a.*"), true, true).map((p) => p.toString()),
|
||||
pathSet.findCompat(Path.parse("a.*"), true, true).map((p) => p.toString()),
|
||||
["a.*", "a.a", "*.a", "*.*"],
|
||||
);
|
||||
});
|
||||
+138
-15
@@ -8,38 +8,161 @@ void test("parse", () => {
|
||||
assert.throws(() => Path.parse(".a"));
|
||||
assert.throws(() => Path.parse("a."));
|
||||
assert.throws(() => Path.parse("a..b"));
|
||||
assert.throws(() => Path.parse("b*"));
|
||||
assert.throws(() => Path.parse("*b"));
|
||||
assert.doesNotThrow(() => Path.parse("b*"));
|
||||
assert.doesNotThrow(() => Path.parse("*b"));
|
||||
assert.throws(() => Path.parse("a.b c.d"));
|
||||
assert.throws(() => Path.parse("a["));
|
||||
assert.throws(() => Path.parse("a[b"));
|
||||
assert.throws(() => Path.parse("a[b:"));
|
||||
assert.throws(() => Path.parse('a[b:"waef]'));
|
||||
assert.doesNotThrow(() => Path.parse("[]"));
|
||||
assert.doesNotThrow(() => Path.parse("[a:]"));
|
||||
assert.doesNotThrow(() => Path.parse("*"));
|
||||
assert.doesNotThrow(() => Path.parse(""));
|
||||
assert.throws(() => Path.parse(""));
|
||||
});
|
||||
|
||||
void test("toString", () => {
|
||||
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
||||
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
||||
const path1 = Path.parse('abc.[ abc=123 and def=" abc " ].123');
|
||||
const path2 = Path.parse('abc.[abc = 123 AND def = " abc "].123');
|
||||
assert.strictEqual(path1.toString(), path2.toString());
|
||||
});
|
||||
|
||||
void test("slice", () => {
|
||||
const path = Path.parse("a.*.b.[x:y].c");
|
||||
const path = Path.parse(`a.*.b.[x = "y"].c`);
|
||||
const sliced = path.slice(1, -1);
|
||||
assert.strictEqual(sliced.toString(), '*.b.[x:"y"]');
|
||||
assert.strictEqual(sliced.toString(), '*.b.[x = "y"]');
|
||||
assert.strictEqual(sliced.alias, 0b100);
|
||||
assert.strictEqual(sliced.wildcard, 0b1);
|
||||
|
||||
const path2 = Path.parse("a.b:c.d");
|
||||
|
||||
// Trim from right into colon region
|
||||
assert.strictEqual(path2.slice(0, 3).toString(), "a.b:c");
|
||||
assert.strictEqual(path2.slice(0, 3).colon, 1);
|
||||
|
||||
// Trim from right past colon region
|
||||
assert.strictEqual(path2.slice(0, 2).toString(), "a.b");
|
||||
assert.strictEqual(path2.slice(0, 2).colon, 0);
|
||||
|
||||
// Start exactly at colon boundary (all-colon result)
|
||||
assert.strictEqual(path2.slice(2, 4).toString(), ":c.d");
|
||||
assert.strictEqual(path2.slice(2, 4).colon, 2);
|
||||
|
||||
// Start past colon boundary (colon dropped)
|
||||
assert.strictEqual(path2.slice(3, 4).toString(), "d");
|
||||
assert.strictEqual(path2.slice(3, 4).colon, 0);
|
||||
|
||||
// Span across boundary from both sides
|
||||
assert.strictEqual(path2.slice(1, 3).toString(), "b:c");
|
||||
assert.strictEqual(path2.slice(1, 3).colon, 1);
|
||||
});
|
||||
|
||||
void test("concat", () => {
|
||||
const path1 = Path.parse("a");
|
||||
const path2 = Path.parse("*.[a:b]");
|
||||
const concat = path1.concat(path2);
|
||||
assert.strictEqual(concat.toString(), 'a.*.[a:"b"]');
|
||||
assert.strictEqual(concat.alias, 0b100);
|
||||
assert.strictEqual(concat.wildcard, 0b10);
|
||||
// Alias and wildcard propagation
|
||||
const c0 = Path.parse("a").concat(Path.parse('*.[a = "b"]'));
|
||||
assert.strictEqual(c0.toString(), 'a.*.[a = "b"]');
|
||||
assert.strictEqual(c0.alias, 0b100);
|
||||
assert.strictEqual(c0.wildcard, 0b10);
|
||||
|
||||
// Left plain, right all-colon
|
||||
const allColon = Path.parse("a:b.c").slice(1, 3);
|
||||
const c1 = Path.parse("a.b").concat(allColon);
|
||||
assert.strictEqual(c1.toString(), "a.b:b.c");
|
||||
assert.strictEqual(c1.colon, 2);
|
||||
|
||||
// Left colon, right all-colon
|
||||
const c2 = Path.parse("a:b").concat(Path.parse("a.b:c.d").slice(2, 4));
|
||||
assert.strictEqual(c2.toString(), "a:b.c.d");
|
||||
assert.strictEqual(c2.colon, 3);
|
||||
|
||||
// Both have mixed colon — should throw
|
||||
assert.throws(() => Path.parse("a:b").concat(Path.parse("c:d")));
|
||||
});
|
||||
|
||||
void test("old alias format", () => {
|
||||
// Empty brackets
|
||||
const empty = Path.parse("a.[].b");
|
||||
assert.strictEqual(empty.alias, 0b10);
|
||||
assert.strictEqual(empty.toString(), "a.[TRUE].b");
|
||||
|
||||
// Single key-value
|
||||
const single = Path.parse("a.[b:c].d");
|
||||
assert.strictEqual(single.alias, 0b10);
|
||||
assert.strictEqual(single.toString(), 'a.[b = "c"].d');
|
||||
|
||||
// Multiple key-value pairs
|
||||
const multi = Path.parse("a.[b:1,c:2].d");
|
||||
assert.strictEqual(multi.alias, 0b10);
|
||||
assert.strictEqual(multi.toString(), 'a.[b = "1" AND c = "2"].d');
|
||||
|
||||
// Key with empty value
|
||||
const emptyVal = Path.parse("a.[b:].d");
|
||||
assert.strictEqual(emptyVal.toString(), 'a.[b = ""].d');
|
||||
|
||||
// Value containing colons (split on first : only)
|
||||
const colonVal = Path.parse("a.[b:c:d].e");
|
||||
assert.strictEqual(colonVal.toString(), 'a.[b = "c:d"].e');
|
||||
|
||||
// Value containing spaces (trimmed)
|
||||
const spaceVal = Path.parse("a.[b:hello world].c");
|
||||
assert.strictEqual(spaceVal.toString(), 'a.[b = "hello world"].c');
|
||||
|
||||
// Unquoted values are trimmed
|
||||
const trimmed = Path.parse("a.[b: hello ].c");
|
||||
assert.strictEqual(trimmed.toString(), 'a.[b = "hello"].c');
|
||||
|
||||
// Whitespace around keys is trimmed
|
||||
const keySpace = Path.parse("a.[ b : c ].d");
|
||||
assert.strictEqual(keySpace.toString(), 'a.[b = "c"].d');
|
||||
|
||||
// Equivalence with new format
|
||||
const oldFmt = Path.parse("x.[a:1,b:2].y");
|
||||
const newFmt = Path.parse('x.[a = "1" AND b = "2"].y');
|
||||
assert.strictEqual(oldFmt.toString(), newFmt.toString());
|
||||
|
||||
// Nested old-format alias
|
||||
const nested = Path.parse("a.[b.[x:1].c:2].d");
|
||||
assert.strictEqual(nested.toString(), 'a.[b.[x = "1"].c = "2"].d');
|
||||
|
||||
// New SQL format still works
|
||||
const sql = Path.parse("a.[b = 1 AND c = 2].d");
|
||||
assert.strictEqual(sql.toString(), "a.[b = 1 AND c = 2].d");
|
||||
|
||||
// Double-quoted value containing closing bracket
|
||||
const quotedBracket = Path.parse('a.[b:"hello]world"].c');
|
||||
assert.strictEqual(quotedBracket.toString(), 'a.[b = "hello]world"].c');
|
||||
|
||||
// Double-quoted value containing comma
|
||||
const quotedComma = Path.parse('a.[b:"hello,world"].c');
|
||||
assert.strictEqual(quotedComma.toString(), 'a.[b = "hello,world"].c');
|
||||
|
||||
// Double-quoted value with escape sequences (JSON semantics)
|
||||
const escaped = Path.parse('a.[b:"hello\\"world"].c');
|
||||
assert.strictEqual(escaped.toString(), 'a.[b = "hello\\"world"].c');
|
||||
|
||||
// Single-quoted value
|
||||
const singleQuoted = Path.parse("a.[b:'value'].c");
|
||||
assert.strictEqual(singleQuoted.toString(), 'a.[b = "value"].c');
|
||||
|
||||
// Invalid old format (no colon) still throws
|
||||
assert.throws(() => Path.parse("[abc]"));
|
||||
});
|
||||
|
||||
void test("slice concat round-trip", () => {
|
||||
const path = Path.parse("a.b:c.d");
|
||||
|
||||
// Round-trips at every split position up to and including the boundary
|
||||
for (let i = 1; i <= path.paramLength; i++) {
|
||||
const rejoined = path.slice(0, i).concat(path.slice(i));
|
||||
assert.strictEqual(rejoined.toString(), path.toString());
|
||||
assert.strictEqual(rejoined.colon, path.colon);
|
||||
}
|
||||
|
||||
// Split inside the colon region: right half loses colon,
|
||||
// but concat still extends the left's attr region
|
||||
const left = path.slice(0, 3);
|
||||
const right = path.slice(3);
|
||||
assert.strictEqual(left.colon, 1);
|
||||
assert.strictEqual(right.colon, 0);
|
||||
const rejoined = left.concat(right);
|
||||
assert.strictEqual(rejoined.toString(), "a.b:c.d");
|
||||
assert.strictEqual(rejoined.colon, 2);
|
||||
});
|
||||
+34
-14
@@ -2,7 +2,7 @@ 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";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
|
||||
const DECIMAL_VALUES = [null, 0, -10, 10];
|
||||
@@ -86,7 +86,7 @@ void test("minimize", async () => {
|
||||
|
||||
for (const c of cases) {
|
||||
const res1 = await query(c);
|
||||
const min = stringify(minimize(parse(c), true));
|
||||
const min = minimize(Expression.parse(c), true).toString();
|
||||
const res2 = await query(min);
|
||||
assert.strictEqual(setsEqual(res1, res2), true);
|
||||
}
|
||||
@@ -104,9 +104,9 @@ void test("unionDiff", async () => {
|
||||
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 [union, diff] = unionDiff(Expression.parse(c1), Expression.parse(c2));
|
||||
const res3 = await query(union.toString());
|
||||
const res4 = await query(diff.toString());
|
||||
|
||||
const unionSet = new Set([...res1, ...res2]);
|
||||
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
|
||||
@@ -117,15 +117,35 @@ void test("unionDiff", async () => {
|
||||
});
|
||||
|
||||
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")),
|
||||
covers(Expression.parse("false"), Expression.parse("false")),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
covers(
|
||||
Expression.parse("false"),
|
||||
Expression.parse("decimal > 5 AND decimal < 3"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
covers(Expression.parse("true"), Expression.parse("decimal > 0")),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
covers(Expression.parse("true"), Expression.parse("false")),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
covers(Expression.parse("false"), Expression.parse("decimal > 0")),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
covers(Expression.parse("decimal >= 0"), Expression.parse("decimal > 0")),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
covers(Expression.parse("decimal > 0"), Expression.parse("decimal >= 0")),
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -139,7 +159,7 @@ void test("covers", async () => {
|
||||
for (const [c1, c2] of cases) {
|
||||
const res1 = await query(c1);
|
||||
const res2 = await query(c2);
|
||||
const coversResult = covers(parse(c1), parse(c2));
|
||||
const coversResult = covers(Expression.parse(c1), Expression.parse(c2));
|
||||
const actuallyCovers = Array.from(res2).every((r) => res1.has(r));
|
||||
|
||||
assert.strictEqual(
|
||||
|
||||
Reference in new issue
Block a user