@thi.ng/rstream-query
Version:
@thi.ng/rstream based triple store & reactive query engine
367 lines (366 loc) • 10 kB
JavaScript
import { join } from "@thi.ng/associative/join";
import { equiv } from "@thi.ng/equiv";
import { assert } from "@thi.ng/errors/assert";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { min3id } from "@thi.ng/math/interval";
import { serialize } from "@thi.ng/rstream-dot";
import { __nextID } from "@thi.ng/rstream/idgen";
import { Stream } from "@thi.ng/rstream/stream";
import { sync } from "@thi.ng/rstream/sync";
import { assocObj } from "@thi.ng/transducers/assoc-obj";
import { comp } from "@thi.ng/transducers/comp";
import { dedupe } from "@thi.ng/transducers/dedupe";
import { map } from "@thi.ng/transducers/map";
import { mapIndexed } from "@thi.ng/transducers/map-indexed";
import { transduce } from "@thi.ng/transducers/transduce";
import { patternVars, resolvePathPattern } from "./pattern.js";
import { isQVar, qvarResolver } from "./qvar.js";
import {
bindVars,
filterSolutions,
indexSel,
intersect2,
intersect3,
joinSolutions,
limitSolutions,
resultTriples
} from "./xforms.js";
class TripleStore {
NEXT_ID;
freeIDs;
triples;
indexS;
indexP;
indexO;
indexSelections;
queries;
allIDs;
streamAll;
streamS;
streamP;
streamO;
constructor(triples) {
this.triples = [];
this.freeIDs = [];
this.queries = /* @__PURE__ */ new Map();
this.indexS = /* @__PURE__ */ new Map();
this.indexP = /* @__PURE__ */ new Map();
this.indexO = /* @__PURE__ */ new Map();
this.indexSelections = {
s: /* @__PURE__ */ new Map(),
p: /* @__PURE__ */ new Map(),
o: /* @__PURE__ */ new Map()
};
this.streamS = new Stream({ id: "S", closeOut: "never" });
this.streamP = new Stream({ id: "P", closeOut: "never" });
this.streamO = new Stream({ id: "O", closeOut: "never" });
this.streamAll = new Stream({ id: "ALL", closeOut: "never" });
this.allIDs = /* @__PURE__ */ new Set();
this.NEXT_ID = 0;
if (triples) {
this.into(triples);
}
}
*[Symbol.iterator]() {
for (let t of this.triples) {
if (t) {
yield t;
}
}
}
has(t) {
return this.get(t) !== void 0;
}
get(t, notFound) {
const id = this.findTriple(
this.indexS.get(t[0]),
this.indexP.get(t[1]),
this.indexO.get(t[2]),
t
);
return id !== -1 ? this.triples[id] : notFound;
}
add(t) {
let s = this.indexS.get(t[0]);
let p = this.indexP.get(t[1]);
let o = this.indexO.get(t[2]);
if (this.findTriple(s, p, o, t) !== -1) return false;
const id = this.nextID();
const is = s || /* @__PURE__ */ new Set();
const ip = p || /* @__PURE__ */ new Set();
const io = o || /* @__PURE__ */ new Set();
this.triples[id] = t;
is.add(id);
ip.add(id);
io.add(id);
this.allIDs.add(id);
!s && this.indexS.set(t[0], is);
!p && this.indexP.set(t[1], ip);
!o && this.indexO.set(t[2], io);
this.broadcastTriple(is, ip, io, t);
return true;
}
into(triples) {
let ok = true;
for (let f of triples) {
ok = this.add(f) && ok;
}
return ok;
}
delete(t) {
let s = this.indexS.get(t[0]);
let p = this.indexP.get(t[1]);
let o = this.indexO.get(t[2]);
const id = this.findTriple(s, p, o, t);
if (id === -1) return false;
s.delete(id);
!s.size && this.indexS.delete(t[0]);
p.delete(id);
!p.size && this.indexP.delete(t[1]);
o.delete(id);
!o.size && this.indexO.delete(t[2]);
this.allIDs.delete(id);
delete this.triples[id];
this.freeIDs.push(id);
this.broadcastTriple(s, p, o, t);
return true;
}
/**
* Replaces triple `a` with `b`, *iff* `a` is actually in the store.
* Else does nothing.
*
* @param a -
* @param b -
*/
replace(a, b) {
if (this.delete(a)) {
return this.add(b);
}
return false;
}
addPatternQuery(pattern, id, emitTriples = true) {
let results;
const [s, p, o] = pattern;
if (s == null && p == null && o == null) {
results = this.streamAll;
} else {
const key = JSON.stringify(pattern);
if (!(results = this.queries.get(key))) {
const qs = this.getIndexSelection(this.streamS, s, "s");
const qp = this.getIndexSelection(this.streamP, p, "p");
const qo = this.getIndexSelection(this.streamO, o, "o");
let src;
let xform = intersect2;
if (s == null && p == null) {
src = { a: qo, b: qs };
} else if (s == null && o == null) {
src = { a: qp, b: qs };
} else if (p == null && o == null) {
src = { a: qs, b: qp };
} else {
src = { s: qs, p: qp, o: qo };
xform = intersect3;
}
results = sync({
id,
src,
xform,
reset: true
});
this.queries.set(key, results);
__submit(this.indexS, qs, s);
__submit(this.indexP, qp, p);
__submit(this.indexO, qo, o);
}
}
return emitTriples ? results.transform(resultTriples(this)) : results;
}
/**
* Creates a new parametric query using given pattern with at least
* 1 query variable. Query vars are strings with `?` prefix. The
* rest of the string is considered the variable name.
*
* ```js
* g.addParamQuery(["?a", "friend", "?b"]);
* ```
*
* Internally, the query pattern is translated into a basic param
* query with an additional result transformation to resolve the
* stated query variable solutions. Returns a rstream subscription
* emitting arrays of solution objects like:
*
* ```js
* [{ a: "asterix", b: "obelix" }, { a: "romeo", b: "julia" }]
* ```
*
* @param pattern -
* @param id -
*/
addParamQuery([s, p, o], id) {
const vs = isQVar(s);
const vp = isQVar(p);
const vo = isQVar(o);
const resolve = qvarResolver(vs, vp, vo, s, p, o);
if (!resolve) {
illegalArgs("at least 1 query variable is required in pattern");
}
id || (id = `query-${__nextID()}`);
const query = this.addPatternQuery(
[vs ? null : s, vp ? null : p, vo ? null : o],
id + "-raw"
);
return query.transform(
map((triples) => {
const res = /* @__PURE__ */ new Set();
for (let f of triples) {
res.add(resolve(f));
}
return res;
}),
dedupe(equiv),
{ id }
);
}
/**
* Converts the given path pattern into a number of sub-queries and
* return a rstream subscription of re-joined result solutions. If
* `maxLen` is given and greater than the number of actual path
* predicates, the predicates are repeated.
*
* @param path -
* @param maxDepth -
* @param id -
*/
addPathQuery(path, maxDepth = path[1].length, id) {
return this.addMultiJoin(
this.addParamQueries(resolvePathPattern(path, maxDepth)[0]),
patternVars(path),
id
);
}
/**
* Like {@link TripleStore.addMultiJoin}, but optimized for only two
* input queries. Returns a rstream subscription computing the
* natural join of the given input query results.
*
* @param id -
* @param a -
* @param b -
*/
addJoin(a, b, id) {
return sync({
id,
src: { a, b },
xform: comp(
map(({ a: a2, b: b2 }) => join(a2, b2)),
dedupe(equiv)
)
});
}
addMultiJoin(queries, keepVars, id) {
const src = transduce(
mapIndexed((i, q) => [
String(i),
q
]),
assocObj(),
queries
);
let xforms = [
joinSolutions(Object.keys(src).length),
dedupe(equiv)
];
keepVars && xforms.push(filterSolutions(keepVars));
return sync({
id,
src,
xform: comp.apply(null, xforms)
});
}
/**
* Compiles given query spec into a number of sub-queries and result
* transformations. Returns rstream subscription of final result
* sets. See {@link QuerySpec} docs for further details.
*
* @param spec -
*/
addQueryFromSpec(spec) {
let query;
let curr;
for (let q of spec.q) {
if (__isWhereQuery(q)) {
curr = this.addMultiJoin(this.addParamQueries(q.where));
} else if (__isPathQuery(q)) {
curr = this.addPathQuery(q.path);
}
query && curr && (curr = this.addJoin(query, curr));
query = curr;
}
assert(!!query, "illegal query spec");
let xforms = [];
spec.limit && xforms.push(limitSolutions(spec.limit));
spec.bind && xforms.push(bindVars(spec.bind));
spec.select && xforms.push(filterSolutions(spec.select));
if (xforms.length) {
query = query.transform(...xforms);
}
return query;
}
toDot(opts) {
return serialize(
[this.streamS, this.streamP, this.streamO, this.streamAll],
opts
);
}
nextID() {
return this.freeIDs.length ? this.freeIDs.pop() : this.NEXT_ID++;
}
broadcastTriple(s, p, o, t) {
this.streamAll.next(this.allIDs);
this.streamS.next({ index: s, key: t[0] });
this.streamP.next({ index: p, key: t[1] });
this.streamO.next({ index: o, key: t[2] });
}
findTriple(s, p, o, f) {
if (s && p && o) {
const triples = this.triples;
const index = [s, p, o][min3id(s.size, p.size, o.size)];
for (let id of index) {
if (equiv(triples[id], f)) {
return id;
}
}
}
return -1;
}
getIndexSelection(stream, key, id) {
if (key == null) {
return this.streamAll;
}
let sel = this.indexSelections[id].get(key);
if (!sel) {
this.indexSelections[id].set(
key,
sel = stream.transform(indexSel(key), { id })
);
}
return sel;
}
addParamQueries(patterns) {
return map(
(q) => this.addParamQuery(q),
patterns
);
}
}
const __submit = (index, stream, key) => {
if (key != null) {
const ids = index.get(key);
ids && stream.next({ index: ids, key });
}
};
const __isWhereQuery = (q) => !!q.where;
const __isPathQuery = (q) => !!q.path;
export {
TripleStore
};