Bundling code now with Rollup for its flat bundling and tree shaking support. Front end code still goes through Webpack for browser support.
251 lines
7.4 KiB
JavaScript
251 lines
7.4 KiB
JavaScript
"use strict";
|
|
|
|
import { map, parseLikePattern } from "./expression-parser";
|
|
|
|
function or(cnf1, cnf2) {
|
|
if (cnf1.length === 0) return cnf2;
|
|
else if (cnf2.length === 0) return cnf1;
|
|
|
|
if (cnf1.length > cnf2.length) {
|
|
const c = cnf1;
|
|
cnf1 = cnf2;
|
|
cnf2 = c;
|
|
}
|
|
|
|
const res = [];
|
|
for (let i = 0; i < cnf1.length; ++i)
|
|
for (let j = i; j < cnf2.length; ++j) res.push(cnf1[i].concat(cnf2[j]));
|
|
|
|
return res;
|
|
}
|
|
|
|
export function booleanCnf(exp) {
|
|
const ranges = new Map();
|
|
const likePatterns = new Map();
|
|
map(exp, e => {
|
|
if (!Array.isArray(e)) return e;
|
|
if (e[0] === "LIKE" || e[0] === "NOT LIKE") {
|
|
if (
|
|
typeof e[2] === "string" &&
|
|
(e.length >= 4 || (e[3] != null && !Array.isArray(e[3])))
|
|
) {
|
|
const key = JSON.stringify(e[1]);
|
|
let s = likePatterns.get(key);
|
|
if (!s) likePatterns.set(key, (s = new Map()));
|
|
s.set(`${e[3] || ""}:${e[2]}`, parseLikePattern(e[2], e[3]));
|
|
}
|
|
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(e[0])) {
|
|
if (!Array.isArray(e[2])) {
|
|
const key = `${JSON.stringify(e[1])}:${typeof e[2]}`;
|
|
let s = ranges.get(key);
|
|
if (!s) ranges.set(key, (s = new Set()));
|
|
s.add(e[2]);
|
|
} else if (!Array.isArray(e[1])) {
|
|
const key = `${JSON.stringify(e[2])}:${typeof e[1]}`;
|
|
let s = ranges.get(key);
|
|
if (!s) ranges.set(key, (s = new Set()));
|
|
s.add(e[2]);
|
|
}
|
|
}
|
|
return e;
|
|
});
|
|
|
|
for (const [k, v] of ranges) {
|
|
if (k.endsWith(":number"))
|
|
ranges.set(k, Array.from(v).sort((a, b) => a - b));
|
|
else ranges.set(k, Array.from(v).sort());
|
|
}
|
|
|
|
const expressions = new Map();
|
|
const mutuallyExclusive = new Map();
|
|
|
|
function getVariable(lhs, op, rhs = "") {
|
|
const key = `${lhs}:${op}:${rhs}`;
|
|
let f = expressions.get(key);
|
|
if (!f) {
|
|
expressions.set(key, (f = expressions.size + 1));
|
|
let m = mutuallyExclusive.get(lhs);
|
|
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
|
|
m.add(f);
|
|
if (rhs || op !== "null") m.add(getVariable(lhs, "null"));
|
|
}
|
|
return f;
|
|
}
|
|
|
|
function getComparisonVariables(lhs, from, fromInclusive, to, toInclusive) {
|
|
const type = from != null ? typeof from : typeof to;
|
|
const range = ranges.get(`${lhs}:${type}`);
|
|
const vars = [];
|
|
|
|
if (from == null) vars.push(`${lhs}:--${range[0]}`);
|
|
|
|
for (let i = 0; i < range.length; ++i) {
|
|
if (
|
|
i > 0 &&
|
|
(from == null || range[i] > from) &&
|
|
(to == null || range[i] <= to)
|
|
)
|
|
vars.push(`${lhs}:${range[i - 1]}--${range[i]}`);
|
|
|
|
if (
|
|
(range[i] > from && range[i] < to) ||
|
|
(range[i] === from && fromInclusive) ||
|
|
(range[i] === to && toInclusive)
|
|
)
|
|
vars.push(`${lhs}:${range[i]}`);
|
|
}
|
|
|
|
if (to == null) vars.push(`${lhs}:${range[range.length - 1]}--`);
|
|
|
|
return vars.map(key => {
|
|
let f = expressions.get(key);
|
|
if (!f) {
|
|
expressions.set(key, (f = expressions.size + 1));
|
|
let m = mutuallyExclusive.get(lhs);
|
|
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
|
|
m.add(f);
|
|
}
|
|
return f;
|
|
});
|
|
}
|
|
|
|
function getLikeVariables(lhs, pat, esc) {
|
|
const vars = [];
|
|
const pats = likePatterns.get(lhs);
|
|
pat = pats.get(`${esc || ""}:${pat}`);
|
|
for (const p of pats.values()) {
|
|
if (pat === p || likePatternIncludes(pat, p))
|
|
vars.push(`${lhs}:like:${JSON.stringify(p)}`);
|
|
}
|
|
|
|
return vars.map(key => {
|
|
let f = expressions.get(key);
|
|
if (!f) expressions.set(key, (f = expressions.size + 1));
|
|
return f;
|
|
});
|
|
}
|
|
|
|
const comparisonOperators = {
|
|
"=": v => [v, true, v, true],
|
|
">": v => [v, false, null, null],
|
|
">=": v => [v, true, null, null],
|
|
"<": v => [null, null, v, false],
|
|
"<=": v => [null, null, v, true]
|
|
};
|
|
|
|
function recursive(clause, negate) {
|
|
let op = clause[0];
|
|
|
|
if ((op === "AND" && !negate) || (op === "OR" && negate)) {
|
|
let res = [];
|
|
for (let i = 1; i < clause.length; ++i)
|
|
res = res.concat(recursive(clause[i], negate));
|
|
return res;
|
|
} else if ((op === "OR" && !negate) || (op === "AND" && negate)) {
|
|
let res = [];
|
|
for (let i = 1; i < clause.length; ++i)
|
|
res = or(res, recursive(clause[i], negate));
|
|
return res;
|
|
} else if (op === "NOT") {
|
|
return recursive(clause[1], !negate);
|
|
} else if (op === "<>") {
|
|
const vars = getComparisonVariables(
|
|
JSON.stringify(clause[1]),
|
|
...comparisonOperators["="](clause[2])
|
|
);
|
|
if (!negate) return vars.map(x => [0 - x]);
|
|
else return [vars];
|
|
} else if (op === "IS NULL") {
|
|
const v = getVariable(JSON.stringify(clause[1]), "null");
|
|
if (negate) return [[0 - v]];
|
|
else return [[v]];
|
|
} else if (op === "IS NOT NULL") {
|
|
const v = getVariable(JSON.stringify(clause[1]), "null");
|
|
if (!negate) return [[0 - v]];
|
|
else return [[v]];
|
|
} else if (op in comparisonOperators) {
|
|
if (!Array.isArray(clause[2])) {
|
|
const vars = getComparisonVariables(
|
|
JSON.stringify(clause[1]),
|
|
...comparisonOperators[op](clause[2])
|
|
);
|
|
if (negate) return vars.map(x => [0 - x]);
|
|
else return [vars];
|
|
} else if (!Array.isArray(clause[1])) {
|
|
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
|
|
const vars = getComparisonVariables(
|
|
JSON.stringify(clause[2]),
|
|
...comparisonOperators[op](clause[1])
|
|
);
|
|
if (negate) return vars.map(x => [0 - x]);
|
|
else return [vars];
|
|
} else {
|
|
let lhs = JSON.stringify(clause[1]);
|
|
let rhs = JSON.stringify(clause[2]);
|
|
// For consistency
|
|
if (rhs > lhs) {
|
|
const t = rhs;
|
|
rhs = lhs;
|
|
lhs = t;
|
|
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
|
|
}
|
|
|
|
const v = getVariable(lhs, op, rhs);
|
|
if (negate) return [[0 - v]];
|
|
else return [[v]];
|
|
}
|
|
} else if (
|
|
(op === "LIKE" || op === "NOT LIKE") &&
|
|
!Array.isArray(clause[2]) &&
|
|
clause[2] != null &&
|
|
(clause.length >= 4 || clause[3] != null)
|
|
) {
|
|
if (op === "NOT LIKE") negate = !negate;
|
|
const vars = getLikeVariables(
|
|
JSON.stringify(clause[1]),
|
|
clause[2],
|
|
clause[3]
|
|
);
|
|
if (negate) return vars.map(x => [0 - x]);
|
|
else return [vars];
|
|
} else {
|
|
const v = getVariable(JSON.stringify(clause), "");
|
|
if (negate) return [[0 - v]];
|
|
else return [[v]];
|
|
}
|
|
}
|
|
|
|
const cnf = recursive(exp, false);
|
|
|
|
for (const m of mutuallyExclusive.values()) {
|
|
const ar = Array.from(m);
|
|
for (let i = 0; i < ar.length; ++i)
|
|
for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]);
|
|
}
|
|
|
|
return { vars: expressions.size, clauses: cnf };
|
|
}
|
|
|
|
function likePatternIncludes(pat1, pat2, idx1 = 0, idx2 = 0) {
|
|
while (idx1 < pat1.length && idx2 < pat2.length) {
|
|
if (pat1[idx1] === "\\%") {
|
|
for (let i = idx2; i <= pat2.length; ++i)
|
|
if (likePatternIncludes(pat1, pat2, idx1 + 1, i)) return true;
|
|
return false;
|
|
} else if (pat2[idx2] === "\\%") {
|
|
if (pat1[idx1] !== "\\%") return false;
|
|
} else if (pat1[idx1] === "\\_") {
|
|
// Ignore
|
|
} else if (pat2[idx2] === "\\_") {
|
|
return false;
|
|
} else if (pat2[idx2] !== pat1[idx1]) {
|
|
return false;
|
|
}
|
|
++idx1;
|
|
++idx2;
|
|
}
|
|
|
|
if (idx1 === pat1.length && idx2 === pat2.length) return true;
|
|
return false;
|
|
}
|