UNPKG

rdf-stores

Version:

A TypeScript/JavaScript implementation of the RDF/JS store interface with support for quoted triples.

388 lines 18.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RdfStore = void 0; const asynciterator_1 = require("asynciterator"); const rdf_data_factory_1 = require("rdf-data-factory"); const rdf_terms_1 = require("rdf-terms"); const DatasetCoreWrapper_1 = require("./dataset/DatasetCoreWrapper"); const TermDictionaryNumberRecordFullTerms_1 = require("./dictionary/TermDictionaryNumberRecordFullTerms"); const TermDictionaryQuotedIndexed_1 = require("./dictionary/TermDictionaryQuotedIndexed"); const RdfStoreIndexNestedMapQuoted_1 = require("./index/RdfStoreIndexNestedMapQuoted"); const OrderUtils_1 = require("./OrderUtils"); /** * An RDF store allows quads to be stored and fetched, based on one or more customizable indexes. */ class RdfStore { constructor(options) { this.features = { quotedTripleFiltering: true }; this._size = 0; this.options = options; this.dataFactory = options.dataFactory; this.dictionary = options.dictionary; this.indexesWrapped = RdfStore.constructIndexesWrapped(options); this.indexesWrappedComponentOrders = this.indexesWrapped.map(indexThis => indexThis.componentOrder); } /** * Create an RDF store with default settings. * Concretely, this store stores triples in GSPO, GPOS, and GOSP order, * and makes use of in-memory number dictionary encoding. */ static createDefault() { return new RdfStore({ indexCombinations: RdfStore.DEFAULT_INDEX_COMBINATIONS, indexConstructor: subOptions => new RdfStoreIndexNestedMapQuoted_1.RdfStoreIndexNestedMapQuoted(subOptions), dictionary: new TermDictionaryQuotedIndexed_1.TermDictionaryQuotedIndexed(new TermDictionaryNumberRecordFullTerms_1.TermDictionaryNumberRecordFullTerms()), dataFactory: new rdf_data_factory_1.DataFactory(), }); } /** * Internal helper to create index objects. * @param options The RDF store options object. */ static constructIndexesWrapped(options) { const indexes = []; if (options.indexCombinations.length === 0) { throw new Error('At least one index combination is required'); } for (const componentOrder of options.indexCombinations) { if (!RdfStore.isCombinationValid(componentOrder)) { throw new Error(`Invalid index combination: ${componentOrder}`); } indexes.push({ index: options.indexConstructor(options), componentOrder, componentOrderInverse: Object.fromEntries(componentOrder.map((value, key) => [value, key])), }); } return indexes; } /** * Check if a given quad term order is valid. * @param combination A quad term order. */ static isCombinationValid(combination) { for (const quadTermName of rdf_terms_1.QUAD_TERM_NAMES) { if (!combination.includes(quadTermName)) { return false; } } return combination.length === 4; } /** * The number of quads in this store. */ get size() { return this._size; } /** * Add a quad to the store. * @param quad An RDF quad. * @return boolean If the quad was not yet present in the index. */ addQuad(quad) { const quadEncoded = [ this.dictionary.encode(quad.subject), this.dictionary.encode(quad.predicate), this.dictionary.encode(quad.object), this.dictionary.encode(quad.graph), ]; let newQuad = false; for (const indexWrapped of this.indexesWrapped) { // Before sending the quad to the index, make sure its components are ordered corresponding to the index's order. newQuad = indexWrapped.index .set((0, OrderUtils_1.orderQuadComponents)(indexWrapped.componentOrder, quadEncoded), true); } if (newQuad) { this._size++; return true; } return false; } /** * Remove a quad from the store. * @param quad An RDF quad. * @return boolean If the quad was present in the index. */ removeQuad(quad) { const quadEncoded = [ this.dictionary.encodeOptional(quad.subject), this.dictionary.encodeOptional(quad.predicate), this.dictionary.encodeOptional(quad.object), this.dictionary.encodeOptional(quad.graph), ]; // We can quickly return false if the quad is not present in the dictionary // eslint-disable-next-line unicorn/no-useless-undefined if (quadEncoded.includes(undefined)) { return false; } let wasPresent = false; for (const indexWrapped of this.indexesWrapped) { // Before sending the quad to the index, make sure its components are ordered corresponding to the index's order. wasPresent = indexWrapped.index .remove((0, OrderUtils_1.orderQuadComponents)(indexWrapped.componentOrder, quadEncoded)); if (!wasPresent) { break; } } if (wasPresent) { this._size--; return true; } return false; } /** * Removes all streamed quads. * @param stream A stream of quads */ remove(stream) { stream.on('data', quad => this.removeQuad(quad)); return stream; } /** * All quads matching the pattern will be removed. * @param subject The optional subject. * @param predicate The optional predicate. * @param object The optional object. * @param graph The optional graph. */ removeMatches(subject, predicate, object, graph) { return this.remove(this.match(subject, predicate, object, graph)); } /** * Deletes the given named graph. * @param graph The graph term or string to match. */ deleteGraph(graph) { if (typeof graph === 'string') { graph = this.dataFactory.namedNode(graph); } return this.removeMatches(undefined, undefined, undefined, graph); } /** * Import the given stream of quads into the store. * @param stream A stream of RDF quads. */ import(stream) { stream.on('data', (quad) => this.addQuad(quad)); return stream; } /** * Returns a generator producing all quads matching the pattern. * @param subject The optional subject. * @param predicate The optional predicate. * @param object The optional object. * @param graph The optional graph. */ *readQuads(subject, predicate, object, graph) { // Check if our dictionary and our indexes have quoted pattern support const indexesSupportQuotedPatterns = Boolean(this.dictionary.features.quotedTriples) && Object.values(this.indexesWrapped).every(wrapped => wrapped.index.features.quotedTripleFiltering); // Construct a quad pattern array const [quadComponents, requireQuotedTripleFiltering] = (0, OrderUtils_1.quadToPattern)(subject, predicate, object, graph, indexesSupportQuotedPatterns); // Determine the best index for this pattern const indexWrapped = this.indexesWrapped[(0, OrderUtils_1.getBestIndex)(this.indexesWrappedComponentOrders, quadComponents)]; // Re-order the quad pattern based on this best index's component order const quadComponentsOrdered = (0, OrderUtils_1.orderQuadComponents)(indexWrapped.componentOrder, quadComponents); // Call the best index's find method. // eslint-disable-next-line unicorn/no-array-callback-reference for (const decomposedQuad of indexWrapped.index.find(quadComponentsOrdered)) { // De-order the resulting quad components into the normal SPOG order for quad creation. const quad = this.dataFactory.quad(decomposedQuad[indexWrapped.componentOrderInverse.subject], decomposedQuad[indexWrapped.componentOrderInverse.predicate], decomposedQuad[indexWrapped.componentOrderInverse.object], decomposedQuad[indexWrapped.componentOrderInverse.graph]); if (requireQuotedTripleFiltering) { if ((0, rdf_terms_1.matchPattern)(quad, subject, predicate, object, graph)) { yield quad; } } else { yield quad; } } } /** * Returns an array containing all quads matching the pattern. * @param subject The optional subject. * @param predicate The optional predicate. * @param object The optional object. * @param graph The optional graph. */ getQuads(subject, predicate, object, graph) { return [...this.readQuads(subject, predicate, object, graph)]; } /** * Returns a stream that produces all quads matching the pattern. * @param subject The optional subject. * @param predicate The optional predicate. * @param object The optional object. * @param graph The optional graph. */ match(subject, predicate, object, graph) { return (0, asynciterator_1.wrap)(this.readQuads(subject, predicate, object, graph)); } /** * Returns a generator producing all quads matching the pattern. * @param subject The subject, which can be a variable. * @param predicate The predicate, which can be a variable. * @param object The object, which can be a variable. * @param graph The graph, which can be a variable. */ *readBindings(bindingsFactory, subject, predicate, object, graph) { // Check if our dictionary and our indexes have quoted pattern support const indexesSupportQuotedPatterns = Boolean(this.dictionary.features.quotedTriples) && Object.values(this.indexesWrapped).every(wrapped => wrapped.index.features.quotedTripleFiltering); // Construct a quad pattern array const [quadComponents, requireQuotedTripleFiltering] = (0, OrderUtils_1.quadToPattern)(subject, predicate, object, graph, indexesSupportQuotedPatterns); // Determine the best index for this pattern const indexWrapped = this.indexesWrapped[(0, OrderUtils_1.getBestIndex)(this.indexesWrappedComponentOrders, quadComponents)]; // Re-order the quad pattern based on this best index's component order const quadComponentsOrdered = (0, OrderUtils_1.orderQuadComponents)(indexWrapped.componentOrder, quadComponents); const ids = (0, OrderUtils_1.encodeOptionalTerms)(quadComponentsOrdered, this.dictionary); // Abort if any of the terms does not exist in the dictionary if (!ids) { return; } // Collect variables to bind const terms = (0, OrderUtils_1.orderQuadComponents)(indexWrapped.componentOrder, [subject, predicate, object, graph]); const variableIndexes = []; for (let i = 0; i < terms.length; i++) { if (terms[i].termType === 'Variable' || terms[i].termType === 'Quad') { variableIndexes.push(i); } } // Check if we need to do post-filtering for overlapping variables let shouldFilterIndexes = false; const filterIndexes = terms.map((variable, i) => { const equalVariables = []; for (let j = i + 1; j < terms.length; j++) { if (variable.equals(terms[j])) { equalVariables.push(j); shouldFilterIndexes = true; } } return equalVariables; }); // Call the best index's find method. for (const decomposedQuadEncoded of indexWrapped.index .findEncoded(ids, quadComponentsOrdered)) { let skipBinding = false; let checkForBindingConflicts = false; const bindingsEntries = []; for (const i of variableIndexes) { // If we had overlapping variables, potentially exclude this binding if values for variable are unequal if (shouldFilterIndexes) { const filterI = filterIndexes[i]; if (filterI) { for (const j of filterI) { if (decomposedQuadEncoded[i] !== decomposedQuadEncoded[j]) { skipBinding = true; break; } } } if (skipBinding) { break; } } const decodedTerm = this.dictionary.decode(decomposedQuadEncoded[i]); // Handle quoted triples // TODO: it may be possible to implement a more efficient of findEncoded if requireQuotedTripleFiltering is // false that would return bindings instead of quads. The following could then be skipped. // variableIndexes would also need to be changed to check requireQuotedTripleFiltering (see readQuads). if (terms[i].termType === 'Quad') { if (decodedTerm.termType === 'Quad') { // If the term is a quad, it may also contain nested variables, // so we need to extract those additional bindings. const additionalBindings = (0, rdf_terms_1.matchPatternMappings)(decodedTerm, terms[i], { returnMappings: true }); if (additionalBindings) { checkForBindingConflicts = true; for (const [key, value] of Object.entries(additionalBindings)) { const variable = this.dataFactory.variable(key); if (bindingsEntries.some(entry => entry[0].equals(variable) && !entry[1].equals(value))) { // Skip this binding if we find conflicting variable bindings skipBinding = true; break; } bindingsEntries.push([variable, value]); } continue; } } skipBinding = true; break; } // If for the current bindings object, we previously found a quoted quad term that bound variables within it, // make sure that later bindings to this variable from other terms don't conflict. if (checkForBindingConflicts && bindingsEntries .some(entry => entry[0].equals(terms[i]) && !entry[1].equals(decodedTerm))) { // Skip this binding if we find conflicting variable bindings skipBinding = true; break; } bindingsEntries.push([terms[i], decodedTerm]); } if (!skipBinding) { // Create and yield the bindings object yield bindingsFactory.bindings(bindingsEntries); } } } /** * Returns an array containing all bindings matching the pattern. * @param bindingsFactory The factory that will be used to create bindings. * @param subject The subject, which can be a variable. * @param predicate The predicate, which can be a variable. * @param object The object, which can be a variable. * @param graph The graph, which can be a variable. */ getBindings(bindingsFactory, subject, predicate, object, graph) { return [...this.readBindings(bindingsFactory, subject, predicate, object, graph)]; } /** * Returns a stream that produces all quads matching the pattern. * @param bindingsFactory The factory that will be used to create bindings. * @param subject The subject, which can be a variable. * @param predicate The predicate, which can be a variable. * @param object The object, which can be a variable. * @param graph The graph, which can be a variable. */ matchBindings(bindingsFactory, subject, predicate, object, graph) { return (0, asynciterator_1.wrap)(this.readBindings(bindingsFactory, subject, predicate, object, graph)); } /** * Returns the exact cardinality of the quads matching the pattern. * @param subject The optional subject. * @param predicate The optional predicate. * @param object The optional object. * @param graph The optional graph. */ countQuads(subject, predicate, object, graph) { // Check if our dictionary and our indexes have quoted pattern support const indexesSupportQuotedPatterns = Boolean(this.dictionary.features.quotedTriples) && Object.values(this.indexesWrapped).every(wrapped => wrapped.index.features.quotedTripleFiltering); // Construct a quad pattern array const [quadComponents] = (0, OrderUtils_1.quadToPattern)(subject, predicate, object, graph, indexesSupportQuotedPatterns); // Optimize all-variables pattern if (quadComponents.every(quadComponent => quadComponent === undefined)) { return this.size; } // Determine the best index for this pattern const indexWrapped = this.indexesWrapped[(0, OrderUtils_1.getBestIndex)(this.indexesWrappedComponentOrders, quadComponents)]; // Re-order the quad pattern based on this best index's component order const quadComponentsOrdered = (0, OrderUtils_1.orderQuadComponents)(indexWrapped.componentOrder, quadComponents); // Call the best index's count method. return indexWrapped.index.count(quadComponentsOrdered); } /** * Wrap this store inside a DatasetCore interface. * Any mutations in either this store or the wrapper will propagate to each other. */ asDataset() { return new DatasetCoreWrapper_1.DatasetCoreWrapper(this); } } exports.RdfStore = RdfStore; RdfStore.DEFAULT_INDEX_COMBINATIONS = [ ['graph', 'subject', 'predicate', 'object'], ['graph', 'predicate', 'object', 'subject'], ['graph', 'object', 'subject', 'predicate'], ]; //# sourceMappingURL=RdfStore.js.map