UNPKG

@thi.ng/rstream-query

Version:

@thi.ng/rstream based triple store & reactive query engine

367 lines (366 loc) 10 kB
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 };