Improve pagination/sorting
This commit is contained in:
1 parent
f8e6e93602
commit
f3068c5355
6 files changed
+380
-98
No files matched your search
@@ -0,0 +1,148 @@
|
|||||||
|
import { complement } from "espresso-iisojs";
|
||||||
|
import { Expression } from "../../types";
|
||||||
|
import normalize from "./normalize";
|
||||||
|
import { Clause, SynthContext } from "./synth";
|
||||||
|
import { and, or } from "./util";
|
||||||
|
|
||||||
|
type Bookmark = Record<string, null | boolean | number | string>;
|
||||||
|
|
||||||
|
type Minterm = number[];
|
||||||
|
|
||||||
|
export function toBookmark(
|
||||||
|
sort: Record<string, number>,
|
||||||
|
row: unknown,
|
||||||
|
): Bookmark {
|
||||||
|
const bookmark: Bookmark = {};
|
||||||
|
for (const param of Object.keys(sort)) {
|
||||||
|
let v = row[param];
|
||||||
|
if (v != null && typeof v === "object") v = v.value?.[0];
|
||||||
|
bookmark[param] = v;
|
||||||
|
}
|
||||||
|
return bookmark;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookmarkToExpression(
|
||||||
|
bookmark: Bookmark,
|
||||||
|
sort: Record<string, number>,
|
||||||
|
): Expression {
|
||||||
|
return Object.entries(sort)
|
||||||
|
.reverse()
|
||||||
|
.reduce((cur, kv) => {
|
||||||
|
const [param, asc] = kv;
|
||||||
|
if (asc < 0) {
|
||||||
|
if (bookmark[param] == null) {
|
||||||
|
return or(
|
||||||
|
["IS NOT NULL", ["PARAM", param]],
|
||||||
|
and(["IS NULL", ["PARAM", param]], cur),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let f = null;
|
||||||
|
f = or(f, [">", ["PARAM", param], bookmark[param]]);
|
||||||
|
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
||||||
|
} else {
|
||||||
|
let f: Expression = ["IS NULL", ["PARAM", param]];
|
||||||
|
if (bookmark[param] == null) return and(f, cur);
|
||||||
|
f = or(f, ["<", ["PARAM", param], bookmark[param]]);
|
||||||
|
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
||||||
|
}
|
||||||
|
}, true as Expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCover(
|
||||||
|
context: SynthContext,
|
||||||
|
minterm: Minterm,
|
||||||
|
allSort: [string, number][],
|
||||||
|
): Minterm[] {
|
||||||
|
if (!allSort.length) return [[]];
|
||||||
|
const [param, sort] = allSort[0];
|
||||||
|
|
||||||
|
const cov: Minterm = [];
|
||||||
|
const nextCov: Minterm = [];
|
||||||
|
|
||||||
|
if (sort > 0) {
|
||||||
|
const lhs = new Clause.Exp(["PARAM", param]);
|
||||||
|
const isNull = context.getVar(new Clause.IsNull(lhs));
|
||||||
|
cov.push((isNull << 2) ^ 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const m of minterm) {
|
||||||
|
const clause = context.getClause(m >>> 2);
|
||||||
|
if (clause instanceof Clause.IsNull) {
|
||||||
|
const lhs = clause.operand.expression();
|
||||||
|
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
|
||||||
|
if (lhs[1] !== param) continue;
|
||||||
|
nextCov.push(m);
|
||||||
|
if (sort < 0 && m & 1) cov.push(m, m ^ 1);
|
||||||
|
} else if (clause instanceof Clause.Compare) {
|
||||||
|
const lhs = clause.lhs.expression();
|
||||||
|
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
|
||||||
|
if (lhs[1] !== param) continue;
|
||||||
|
if (!(m & 1) && sort > 0) continue;
|
||||||
|
nextCov.push(m);
|
||||||
|
|
||||||
|
const negate = (m ^ (m >> 1)) & 1;
|
||||||
|
|
||||||
|
if (sort > 0) {
|
||||||
|
if (
|
||||||
|
(clause.op === "=" && !negate) ||
|
||||||
|
(clause.op === ">" && !negate) ||
|
||||||
|
(clause.op === "<" && negate)
|
||||||
|
) {
|
||||||
|
const c = new Clause.Compare(clause.lhs, ">", clause.rhs);
|
||||||
|
const v = context.getVar(c);
|
||||||
|
cov.push((v << 2) ^ 3);
|
||||||
|
}
|
||||||
|
} else if (sort < 0) {
|
||||||
|
if (
|
||||||
|
(clause.op === "=" && !negate) ||
|
||||||
|
(clause.op === ">" && negate) ||
|
||||||
|
(clause.op === "<" && !negate)
|
||||||
|
) {
|
||||||
|
const c = new Clause.Compare(clause.lhs, "<", clause.rhs);
|
||||||
|
const v = context.getVar(c);
|
||||||
|
cov.push((v << 2) ^ 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = getCover(context, minterm, allSort.slice(1));
|
||||||
|
|
||||||
|
return [cov, ...next.map((n) => [...nextCov, ...n])];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function paginate(
|
||||||
|
fetched: Expression,
|
||||||
|
toFetch: Expression,
|
||||||
|
sort: Record<string, number>,
|
||||||
|
): [Expression, Expression] {
|
||||||
|
fetched = normalize(fetched);
|
||||||
|
if (!fetched) return [false, toFetch];
|
||||||
|
|
||||||
|
toFetch = normalize(toFetch);
|
||||||
|
if (!toFetch) return [false, toFetch];
|
||||||
|
|
||||||
|
const synth1 = Clause.fromExpression(fetched);
|
||||||
|
const synth2 = Clause.fromExpression(toFetch);
|
||||||
|
|
||||||
|
const context = new SynthContext();
|
||||||
|
|
||||||
|
const expr1Minterms = synth1.true(context);
|
||||||
|
const expr2MintermsC = complement(synth2.true(context));
|
||||||
|
|
||||||
|
const gaps = context.sanitizeMinterms(
|
||||||
|
complement([
|
||||||
|
...expr1Minterms,
|
||||||
|
...expr2MintermsC,
|
||||||
|
...context.getDcSet([...expr1Minterms, ...expr2MintermsC]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cover = gaps.flatMap((m) => getCover(context, m, Object.entries(sort)));
|
||||||
|
const minterms1 = context.minimize(complement([...cover, ...expr2MintermsC]));
|
||||||
|
const minterms2 = context.minimize(
|
||||||
|
complement([...minterms1, ...expr2MintermsC]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [context.toExpression(minterms1), context.toExpression(minterms2)];
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export abstract class SynthContextBase<
|
|||||||
public variables = new Map<string, number>();
|
public variables = new Map<string, number>();
|
||||||
protected clauses = new Map<number, U>();
|
protected clauses = new Map<number, U>();
|
||||||
|
|
||||||
protected getVar(c: U): number {
|
public getVar(c: U): number {
|
||||||
const str = c.toString();
|
const str = c.toString();
|
||||||
let idx = this.variables.get(str);
|
let idx = this.variables.get(str);
|
||||||
if (idx == null) {
|
if (idx == null) {
|
||||||
@@ -23,7 +23,7 @@ export abstract class SynthContextBase<
|
|||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getClause(v: number): U {
|
public getClause(v: number): U {
|
||||||
return this.clauses.get(v);
|
return this.clauses.get(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +34,16 @@ export abstract class SynthContextBase<
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
canLower(idx: number, set: Set<number>): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bias(a: number, b: number): number {
|
||||||
|
// Bias towards 1 then lower index
|
||||||
|
return (b ^ 1) - (a ^ 1);
|
||||||
|
}
|
||||||
|
|
||||||
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
|
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
|
||||||
return minterms;
|
return minterms;
|
||||||
}
|
}
|
||||||
@@ -41,9 +51,18 @@ export abstract class SynthContextBase<
|
|||||||
minimize(minterms: Minterm[], dcSet: Minterm[] = []): Minterm[] {
|
minimize(minterms: Minterm[], dcSet: Minterm[] = []): Minterm[] {
|
||||||
minterms = this.sanitizeMinterms(minterms);
|
minterms = this.sanitizeMinterms(minterms);
|
||||||
const canRaise = this.canRaise.bind(this);
|
const canRaise = this.canRaise.bind(this);
|
||||||
return espresso(minterms, [...this.getDcSet(minterms), ...dcSet], {
|
const canLower = this.canLower.bind(this);
|
||||||
|
const bias = this.bias.bind(this);
|
||||||
|
|
||||||
|
return espresso(
|
||||||
|
minterms,
|
||||||
|
[...this.getDcSet([...minterms, ...dcSet]), ...dcSet],
|
||||||
|
{
|
||||||
canRaise,
|
canRaise,
|
||||||
});
|
canLower,
|
||||||
|
bias,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,6 +745,18 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
return !(idx & 1) || !set.has(idx ^ 3);
|
return !(idx & 1) || !set.has(idx ^ 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canLower(idx: number, set: Set<number>): boolean {
|
||||||
|
if (idx & 1) return true;
|
||||||
|
const clause = this.getClause(idx >> 2);
|
||||||
|
if (clause instanceof Clause.IsNull) return true;
|
||||||
|
return set.has(idx ^ 3) || set.has(idx ^ 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bias(a: number, b: number): number {
|
||||||
|
// Bias towards 1 then true
|
||||||
|
return ((b & 3) ^ 3) - ((a & 3) ^ 3);
|
||||||
|
}
|
||||||
|
|
||||||
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
|
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
|
||||||
const res = [] as number[][];
|
const res = [] as number[][];
|
||||||
|
|
||||||
@@ -736,8 +767,18 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
const minterm: number[] = [];
|
const minterm: number[] = [];
|
||||||
const perms: number[][] = [];
|
const perms: number[][] = [];
|
||||||
for (const [k, v] of merged) {
|
for (const [k, v] of merged) {
|
||||||
if (v === 0b1010) continue loop;
|
if ((v & 0b0011) === 0b0011) continue loop;
|
||||||
const isNullVars = [...this.clauses.get(k).getNullables()].map((c) =>
|
if ((v & 0b1100) === 0b1100) continue loop;
|
||||||
|
const clause = this.clauses.get(k);
|
||||||
|
if (!clause) throw new Error("Invalid literal");
|
||||||
|
if (clause instanceof Clause.IsNull) {
|
||||||
|
if (v === 0b0100) minterm.push((k << 2) ^ 2);
|
||||||
|
else if (v === 0b1000) minterm.push((k << 2) ^ 3);
|
||||||
|
else throw new Error("Invalid literal");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((v & 0b1010) === 0b1010) continue loop;
|
||||||
|
const isNullVars = [...clause.getNullables()].map((c) =>
|
||||||
this.getVar(c),
|
this.getVar(c),
|
||||||
);
|
);
|
||||||
const t = k << 2;
|
const t = k << 2;
|
||||||
@@ -773,7 +814,25 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
if (!s.length) return true;
|
if (!s.length) return true;
|
||||||
const conjs: Expression[] = [];
|
const conjs: Expression[] = [];
|
||||||
for (const i of s) {
|
for (const i of s) {
|
||||||
let expr = this.getClause(i >>> 2).expression() as Expression;
|
const clause = this.getClause(i >>> 2);
|
||||||
|
if (!clause) throw new Error("Invalid literal");
|
||||||
|
if (clause instanceof Clause.IsNull) {
|
||||||
|
if (!(i & 2)) throw new Error("Invalid literal");
|
||||||
|
} else if (!(i & 1)) {
|
||||||
|
// Should never be reached if minimized correctly
|
||||||
|
const isNullVars = [...clause.getNullables()].map((c) =>
|
||||||
|
this.getVar(c),
|
||||||
|
);
|
||||||
|
conjs.push(
|
||||||
|
this.toExpression([
|
||||||
|
[i ^ 3],
|
||||||
|
...isNullVars.map((n) => [(n << 2) ^ 3]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let expr = clause.expression() as Expression;
|
||||||
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
|
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
|
||||||
if (
|
if (
|
||||||
Array.isArray(expr) &&
|
Array.isArray(expr) &&
|
||||||
@@ -882,16 +941,11 @@ export function unionDiff(
|
|||||||
|
|
||||||
const expr2Minterms = synth2.true(context);
|
const expr2Minterms = synth2.true(context);
|
||||||
const expr1Minterms = synth1.true(context);
|
const expr1Minterms = synth1.true(context);
|
||||||
const expr1NullMinterms = synth1.null(context);
|
|
||||||
const expr1FalseMinterms = synth1.false(context);
|
|
||||||
|
|
||||||
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
|
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
|
||||||
|
|
||||||
const diff = context.minimize(
|
const diff = context.minimize(
|
||||||
complement([
|
complement([...expr1Minterms, ...complement(expr2Minterms)]),
|
||||||
...complement([...expr1NullMinterms, ...expr1FalseMinterms]),
|
|
||||||
...complement(expr2Minterms),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [context.toExpression(union), context.toExpression(diff)];
|
return [context.toExpression(union), context.toExpression(diff)];
|
||||||
@@ -911,8 +965,8 @@ export function covers(expr1: Expression, expr2: Expression): boolean {
|
|||||||
const expr2Minterms = synt2.true(context);
|
const expr2Minterms = synt2.true(context);
|
||||||
|
|
||||||
return tautology([
|
return tautology([
|
||||||
...complement(expr2Minterms),
|
...context.sanitizeMinterms(complement(expr2Minterms)),
|
||||||
...context.getDcSet([...expr2Minterms, ...expr1Minterms]),
|
...context.getDcSet([...expr2Minterms, ...expr1Minterms]),
|
||||||
...expr1Minterms,
|
...context.sanitizeMinterms(expr1Minterms),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
Generated
+7
-7
@@ -11,7 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breejs/later": "^4.1.0",
|
"@breejs/later": "^4.1.0",
|
||||||
"bson": "^4.7.2",
|
"bson": "^4.7.2",
|
||||||
"espresso-iisojs": "^1.0.7",
|
"espresso-iisojs": "^1.0.8",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.0.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
@@ -3055,9 +3055,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/espresso-iisojs": {
|
"node_modules/espresso-iisojs": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/espresso-iisojs/-/espresso-iisojs-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/espresso-iisojs/-/espresso-iisojs-1.0.8.tgz",
|
||||||
"integrity": "sha512-yHEY+CC9wqEyA1N+KC03xmyN+lFPZdpXaLUxf6vrofE0cMXXIkOg08tXXPmq5fnOoLgxRfm5ikrmYD8/gIhRig=="
|
"integrity": "sha512-S3D62BA/jBUCIQJ3VlBGMSxGwPyyV7ttduiJvyaD4dWyhiC/9TnJTaiurx4r8bqcYZ1J0KELliub4xo8neKjSA=="
|
||||||
},
|
},
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
@@ -7297,9 +7297,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"espresso-iisojs": {
|
"espresso-iisojs": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/espresso-iisojs/-/espresso-iisojs-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/espresso-iisojs/-/espresso-iisojs-1.0.8.tgz",
|
||||||
"integrity": "sha512-yHEY+CC9wqEyA1N+KC03xmyN+lFPZdpXaLUxf6vrofE0cMXXIkOg08tXXPmq5fnOoLgxRfm5ikrmYD8/gIhRig=="
|
"integrity": "sha512-S3D62BA/jBUCIQJ3VlBGMSxGwPyyV7ttduiJvyaD4dWyhiC/9TnJTaiurx4r8bqcYZ1J0KELliub4xo8neKjSA=="
|
||||||
},
|
},
|
||||||
"esquery": {
|
"esquery": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breejs/later": "^4.1.0",
|
"@breejs/later": "^4.1.0",
|
||||||
"bson": "^4.7.2",
|
"bson": "^4.7.2",
|
||||||
"espresso-iisojs": "^1.0.7",
|
"espresso-iisojs": "^1.0.8",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.0.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"node": ">=12.13.0"
|
"node": ">=12.13.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "esbuild --log-level=warning --bundle --platform=node --target=node18 --packages=external --outdir=test test/*.ts && node --test test/*.js && rm test/*.js",
|
"test": "esbuild --log-level=warning --bundle --platform=node --target=node18 --packages=external --sourcemap=inline --outdir=test test/*.ts && node --test --enable-source-maps test/*.js && rm test/*.js",
|
||||||
"lint": "esbuild build/lint.ts --bundle --platform=node --target=node16 --packages=external | node",
|
"lint": "esbuild build/lint.ts --bundle --platform=node --target=node16 --packages=external | node",
|
||||||
"build": "esbuild build/build.ts --bundle --platform=node --target=node12 --packages=external | node"
|
"build": "esbuild build/build.ts --bundle --platform=node --target=node12 --packages=external | node"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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 { 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(`${stringify(q2)} ORDER BY ${orderBy}`);
|
||||||
|
const [fulfilled, diff] = paginate(q1, q2, sort);
|
||||||
|
assert.ok(covers(q1, fulfilled));
|
||||||
|
|
||||||
|
const fulfilledMatches = await query(
|
||||||
|
`${stringify(fulfilled)} ORDER BY ${orderBy}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const diffMatches = await query(`${stringify(diff)} 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 = ["AND", q2, bookmarkFilter];
|
||||||
|
|
||||||
|
assert.ok(!covers(q1, capped));
|
||||||
|
const cappedMatches = await query(
|
||||||
|
`(${stringify(capped)}) ORDER BY ${orderBy}`,
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
|
||||||
|
const min = minimize(["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(parse(q1), parse(q2), sort);
|
||||||
|
});
|
||||||
+34
-74
@@ -7,7 +7,12 @@ import * as notifications from "./notifications.ts";
|
|||||||
import { configSnapshot, genieacsVersion } from "./config.ts";
|
import { configSnapshot, genieacsVersion } from "./config.ts";
|
||||||
import { QueueTask } from "./task-queue.ts";
|
import { QueueTask } from "./task-queue.ts";
|
||||||
import { PingResult } from "../lib/ping.ts";
|
import { PingResult } from "../lib/ping.ts";
|
||||||
import { unionDiff, covers } from "../lib/common/expression/synth.ts";
|
import { unionDiff } from "../lib/common/expression/synth.ts";
|
||||||
|
import {
|
||||||
|
bookmarkToExpression,
|
||||||
|
paginate,
|
||||||
|
toBookmark,
|
||||||
|
} from "../lib/common/expression/pagination.ts";
|
||||||
|
|
||||||
const memoizedStringify = memoize(stringify);
|
const memoizedStringify = memoize(stringify);
|
||||||
const memoizedEvaluate = memoize(evaluate);
|
const memoizedEvaluate = memoize(evaluate);
|
||||||
@@ -26,6 +31,7 @@ const queries = {
|
|||||||
fulfilling: new WeakSet(),
|
fulfilling: new WeakSet(),
|
||||||
accessed: new WeakMap(),
|
accessed: new WeakMap(),
|
||||||
value: new WeakMap(),
|
value: new WeakMap(),
|
||||||
|
unsatisfied: new WeakMap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Resources {
|
interface Resources {
|
||||||
@@ -224,46 +230,11 @@ export function count(resourceType: string, filter: Expression): QueryResponse {
|
|||||||
return queryResponse;
|
return queryResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
function limitFilter(
|
|
||||||
filter: Expression,
|
|
||||||
sort: Record<string, number>,
|
|
||||||
bookmark,
|
|
||||||
): Expression {
|
|
||||||
const sortSort = (a, b): number => Math.abs(b[1]) - Math.abs(a[1]);
|
|
||||||
const arr = Object.entries(sort).sort(sortSort).reverse();
|
|
||||||
return and(
|
|
||||||
filter,
|
|
||||||
arr.reduce((cur, kv) => {
|
|
||||||
const [param, asc] = kv;
|
|
||||||
if (asc <= 0) {
|
|
||||||
if (bookmark[param] == null) {
|
|
||||||
return or(
|
|
||||||
["IS NOT NULL", ["PARAM", param]],
|
|
||||||
and(["IS NULL", ["PARAM", param]], cur),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let f = null;
|
|
||||||
f = or(f, [">", ["PARAM", param], bookmark[param]]);
|
|
||||||
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
|
||||||
} else {
|
|
||||||
let f: Expression = ["IS NULL", ["PARAM", param]];
|
|
||||||
if (bookmark[param] == null) return and(f, cur);
|
|
||||||
f = or(f, ["<", ["PARAM", param], bookmark[param]]);
|
|
||||||
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
|
||||||
}
|
|
||||||
}, true as Expression),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareFunction(sort: {
|
function compareFunction(sort: {
|
||||||
[param: string]: number;
|
[param: string]: number;
|
||||||
}): (a: any, b: any) => number {
|
}): (a: any, b: any) => number {
|
||||||
const sortEntries = Object.entries(sort).sort(
|
|
||||||
(a, b) => Math.abs(b[1]) - Math.abs(a[1]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (a, b) => {
|
return (a, b) => {
|
||||||
for (const [param, asc] of sortEntries) {
|
for (const [param, asc] of Object.entries(sort)) {
|
||||||
let v1 = a[param];
|
let v1 = a[param];
|
||||||
let v2 = b[param];
|
let v2 = b[param];
|
||||||
if (v1 != null && typeof v1 === "object") {
|
if (v1 != null && typeof v1 === "object") {
|
||||||
@@ -306,24 +277,6 @@ function findMatches(resourceType, filter, sort, limit): any[] {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferQuery(resourceType, queryResponse): void {
|
|
||||||
const limit = queries.limit.get(queryResponse);
|
|
||||||
let filter = queries.filter.get(queryResponse);
|
|
||||||
filter = unpackExpression(filter);
|
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
|
||||||
const sort = queries.sort.get(queryResponse);
|
|
||||||
if (bookmark || !limit) {
|
|
||||||
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
|
||||||
if (covers(resources[resourceType].combinedFilter, filter))
|
|
||||||
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
queries.value.set(
|
|
||||||
queryResponse,
|
|
||||||
findMatches(resourceType, filter, sort, limit),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetch(
|
export function fetch(
|
||||||
resourceType: string,
|
resourceType: string,
|
||||||
filter: Expression,
|
filter: Expression,
|
||||||
@@ -331,7 +284,6 @@ export function fetch(
|
|||||||
): QueryResponse {
|
): QueryResponse {
|
||||||
const filterStr = memoizedStringify(filter);
|
const filterStr = memoizedStringify(filter);
|
||||||
const sort = Object.assign({}, options.sort);
|
const sort = Object.assign({}, options.sort);
|
||||||
for (const [k, v] of Object.entries(sort)) sort[k] += Math.sign(v);
|
|
||||||
|
|
||||||
const limit = options.limit || 0;
|
const limit = options.limit || 0;
|
||||||
if (resourceType === "devices")
|
if (resourceType === "devices")
|
||||||
@@ -347,7 +299,16 @@ export function fetch(
|
|||||||
queries.filter.set(queryResponse, filter);
|
queries.filter.set(queryResponse, filter);
|
||||||
queries.limit.set(queryResponse, limit);
|
queries.limit.set(queryResponse, limit);
|
||||||
queries.sort.set(queryResponse, sort);
|
queries.sort.set(queryResponse, sort);
|
||||||
inferQuery(resourceType, queryResponse);
|
const [satisfied, diff] = paginate(
|
||||||
|
resources[resourceType].combinedFilter,
|
||||||
|
unpackExpression(filter),
|
||||||
|
sort,
|
||||||
|
);
|
||||||
|
const matches = findMatches(resourceType, satisfied, sort, limit);
|
||||||
|
queries.value.set(queryResponse, matches);
|
||||||
|
if (!diff || matches.length >= limit)
|
||||||
|
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
||||||
|
else queries.unsatisfied.set(queryResponse, diff);
|
||||||
return queryResponse;
|
return queryResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,11 +373,18 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
queries.fulfilling.add(queryResponse);
|
queries.fulfilling.add(queryResponse);
|
||||||
toFetchAll[resourceType] = toFetchAll[resourceType] || [];
|
toFetchAll[resourceType] = toFetchAll[resourceType] || [];
|
||||||
toFetchAll[resourceType].push(queryResponse);
|
toFetchAll[resourceType].push(queryResponse);
|
||||||
const limit = queries.limit.get(queryResponse);
|
let limit = queries.limit.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (limit) {
|
if (limit) {
|
||||||
let filter = queries.filter.get(queryResponse);
|
let filter = queries.filter.get(queryResponse);
|
||||||
filter = unpackExpression(filter);
|
filter = unpackExpression(filter);
|
||||||
|
|
||||||
|
const unsatisfied = queries.unsatisfied.get(queryResponse);
|
||||||
|
if (unsatisfied) {
|
||||||
|
limit -= queries.value.get(queryResponse).length;
|
||||||
|
filter = unsatisfied;
|
||||||
|
}
|
||||||
|
|
||||||
allPromises.push(
|
allPromises.push(
|
||||||
xhrRequest({
|
xhrRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -431,20 +399,9 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
}),
|
}),
|
||||||
background: true,
|
background: true,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
|
queries.unsatisfied.delete(queryResponse);
|
||||||
if ((res as any[]).length) {
|
if ((res as any[]).length) {
|
||||||
// Generate bookmark object
|
queries.bookmark.set(queryResponse, toBookmark(sort, res[0]));
|
||||||
const bm = Object.keys(sort).reduce((b, k) => {
|
|
||||||
if (res[0][k] != null) {
|
|
||||||
if (typeof res[0][k] === "object") {
|
|
||||||
if (res[0][k].value != null) b[k] = res[0][k].value[0];
|
|
||||||
} else {
|
|
||||||
b[k] = res[0][k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b;
|
|
||||||
}, {});
|
|
||||||
queries.bookmark.set(queryResponse, bm);
|
|
||||||
} else {
|
} else {
|
||||||
queries.bookmark.delete(queryResponse);
|
queries.bookmark.delete(queryResponse);
|
||||||
}
|
}
|
||||||
@@ -467,7 +424,8 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
|
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
if (bookmark)
|
||||||
|
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
||||||
combinedFilter = or(combinedFilter, filter);
|
combinedFilter = or(combinedFilter, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +445,8 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
const limit = queries.limit.get(queryResponse);
|
const limit = queries.limit.get(queryResponse);
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
if (bookmark)
|
||||||
|
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
||||||
|
|
||||||
queries.value.set(
|
queries.value.set(
|
||||||
queryResponse,
|
queryResponse,
|
||||||
@@ -540,7 +499,8 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
const limit = queries.limit.get(queryResponse);
|
const limit = queries.limit.get(queryResponse);
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
if (bookmark)
|
||||||
|
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
||||||
|
|
||||||
queries.value.set(
|
queries.value.set(
|
||||||
queryResponse,
|
queryResponse,
|
||||||
|
|||||||
Reference in new issue
Block a user