UNPKG

@jahed/sparql-engine

Version:

SPARQL query engine for servers and web browsers.

435 lines (386 loc) 12.1 kB
// SPDX-License-Identifier: MIT import type { Quad_Predicate, Quad_Subject } from "@rdfjs/types"; import { isNull, isUndefined } from "lodash-es"; import { stringToTerm, termToString } from "rdf-string"; import type { ValuePatternRow } from "sparqljs"; import type { EngineTriple, EngineTripleValue } from "../types.ts"; import { isVariable } from "../utils/rdf.ts"; type Term = EngineTripleValue; export type BindingsRecord = Record<string, Term>; /** * A set of mappings from a variable to a RDF Term. * @abstract */ export abstract class Bindings { private readonly _properties: Map<string, any>; constructor() { this._properties = new Map(); } /** * The number of mappings in the set * @return The number of mappings in the set */ abstract get size(): number; /** * Returns True if the set is empty, False otherwise * @return True if the set is empty, False otherwise */ abstract get isEmpty(): boolean; /** * Get an iterator over the SPARQL variables in the set * @return An iterator over the SPARQL variables in the set */ abstract variables(): IterableIterator<string>; /** * Get an iterator over the RDF terms in the set * @return An iterator over the RDF terms in the set */ abstract values(): IterableIterator<Term>; /** * Get the RDF Term associated with a SPARQL variable * @param variable - SPARQL variable * @return The RDF Term associated with the given SPARQL variable */ abstract get(variable: string): Term | null; /** * Test if mappings exists for a SPARQL variable * @param variable - SPARQL variable * @return True if a mappings exists for this variable, False otherwise */ abstract has(variable: string): boolean; /** * Add a mapping SPARQL variable -> RDF Term to the set * @param variable - SPARQL variable * @param value - RDF Term */ abstract set(variable: string, value: Term): void; /** * Get metadata attached to the set using a key * @param key - Metadata key * @return The metadata associated with the given key */ getProperty(key: string): any { return this._properties.get(key); } /** * Check if a metadata with a given key is attached to the set * @param key - Metadata key * @return Tur if the metadata exists, False otherwise */ hasProperty(key: string): boolean { return this._properties.has(key); } /** * Attach metadata to the set * @param key - Key associated to the value * @param value - Value to attach */ setProperty(key: string, value: any): void { this._properties.set(key, value); } /** * Invoke a callback on each mapping * @param callback - Callback to invoke * @return */ abstract forEach(callback: (variable: string, value: Term) => void): void; /** * Remove all mappings from the set * @return */ abstract clear(): void; /** * Returns an empty set of mappings * @return An empty set of mappings */ abstract empty(): Bindings; /** * Serialize the set of mappings as a plain JS Object * @return The set of mappings as a plain JS Object */ toObject(): BindingsRecord { return this.reduce<BindingsRecord>((acc, variable, value) => { acc[variable] = value; return acc; }, {}); } /** * Serialize the set of mappings as a string * @return The set of mappings as a string */ toString(): string { const value = this.reduce((acc, variable, value) => { return `${acc} ${variable} -> ${termToString(value)},`; }, "{"); return value.substring(0, value.length - 1) + " }"; } /** * Creates a deep copy of the set of mappings * @return A deep copy of the set */ clone(): Bindings { const cloned = this.empty(); // copy properties then values if (this._properties.size > 0) { this._properties.forEach((value, key) => { cloned.setProperty(key, value); }); } this.forEach((variable, value) => { cloned.set(variable, value); }); return cloned; } /** * Test the equality between two sets of mappings * @param other - A set of mappings * @return True if the two sets are equal, False otherwise */ equals(other: Bindings): boolean { if (this.size !== other.size) { return false; } for (let variable in other.variables()) { if (!this.has(variable) || this.get(variable) !== other.get(variable)) { return false; } } return true; } /** * Bound a triple pattern using the set of mappings, i.e., substitute variables in the triple pattern * @param triple - Triple pattern * @return An new, bounded triple pattern */ bound(triple: EngineTriple): EngineTriple { const newTriple = Object.assign({}, triple); if (isVariable(triple.subject) && this.has(triple.subject.value)) { newTriple.subject = this.get(triple.subject.value) as Quad_Subject; } if (isVariable(triple.predicate) && this.has(triple.predicate.value)) { newTriple.predicate = this.get(triple.predicate.value) as Quad_Predicate; } if (isVariable(triple.object) && this.has(triple.object.value)) { newTriple.object = this.get(triple.object.value)!; } return newTriple; } /** * Creates a new bindings with additionnal mappings * @param values - Pairs [variable, value] to add to the set * @return A new Bindings with the additionnal mappings */ extendMany(values: Array<[string, Term]>): Bindings { const cloned = this.clone(); values.forEach((v) => { cloned.set(v[0], v[1]); }); return cloned; } /** * Perform the union of the set of mappings with another set * @param other - Set of mappings * @return The Union set of mappings */ union(other: Bindings): Bindings { const cloned = this.clone(); other.forEach((variable, value) => { cloned.set(variable, value); }); return cloned; } /** * Perform the intersection of the set of mappings with another set * @param other - Set of mappings * @return The intersection set of mappings */ intersection(other: Bindings): Bindings { const res = this.empty(); this.forEach((variable, value) => { if (other.has(variable) && other.get(variable) === value) { res.set(variable, value); } }); return res; } /** * Performs a set difference with another set of mappings, i.e., A.difference(B) returns all mappings that are in A and not in B. * @param other - Set of mappings * @return The results of the set difference */ difference(other: Bindings): Bindings { return this.filter((variable: string, value: Term) => { return !other.has(variable) || value !== other.get(variable); }); } /** * Test if the set of bindings is a subset of another set of mappings. * @param other - Superset of mappings * @return Ture if the set of bindings is a subset of another set of mappings, False otherwise */ isSubset(other: Bindings): boolean { return Array.from(this.variables()).every((v: string) => { return other.has(v) && other.get(v) === this.get(v); }); } /** * Creates a new set of mappings using a function to transform the current set * @param mapper - Transformation function (variable, value) => [string, string] * @return A new set of mappings */ map( mapper: (variable: string, value: Term) => [string | null, Term | null] ): Bindings { const result = this.empty(); this.forEach((variable, value) => { let [newVar, newValue] = mapper(variable, value); if ( !( isNull(newVar) || isUndefined(newVar) || isNull(newValue) || isUndefined(newValue) ) ) { result.set(newVar, newValue); } }); return result; } /** * Same as map, but only transform variables * @param mapper - Transformation function * @return A new set of mappings */ mapVariables( mapper: (variable: string, value: Term) => string | null ): Bindings { return this.map((variable, value) => [mapper(variable, value), value]); } /** * Same as map, but only transform values * @param mapper - Transformation function * @return A new set of mappings */ mapValues(mapper: (variable: string, value: Term) => Term | null): Bindings { return this.map((variable, value) => [variable, mapper(variable, value)]); } /** * Filter mappings from the set of mappings using a predicate function * @param predicate - Predicate function * @return A new set of mappings */ filter(predicate: (variable: string, value: Term) => boolean): Bindings { return this.map((variable, value) => { if (predicate(variable, value)) { return [variable, value]; } return [null, null]; }); } /** * Reduce the set of mappings to a value which is the accumulated result of running each element in collection thru a reducing function, where each successive invocation is supplied the return value of the previous. * @param reducer - Reducing function * @param start - Value used to start the accumulation * @return The accumulated value */ reduce<T>( reducer: (acc: T, variable: string, value: Term) => T, start: T ): T { let acc: T = start; this.forEach((variable, value) => { acc = reducer(acc, variable, value); }); return acc; } /** * Test if some mappings in the set pass a predicate function * @param predicate - Function to test for each mapping * @return True if some mappings in the set some the predicate function, False otheriwse */ some(predicate: (variable: string, value: Term) => boolean): boolean { let res = false; this.forEach((variable, value) => { res = res || predicate(variable, value); }); return res; } /** * Test if every mappings in the set pass a predicate function * @param predicate - Function to test for each mapping * @return True if every mappings in the set some the predicate function, False otheriwse */ every(predicate: (variable: string, value: Term) => boolean): boolean { let res = true; this.forEach((variable, value) => { res = res && predicate(variable, value); }); return res; } } /** * A set of mappings from a variable to a RDF Term, implements using a HashMap */ export class BindingBase extends Bindings { private readonly _content: Map<string, Term>; constructor() { super(); this._content = new Map(); } get size(): number { return this._content.size; } get isEmpty(): boolean { return this.size === 0; } /** * Creates a set of mappings from a plain Javascript Object * @param obj - Source object to turn into a set of mappings * @return A set of mappings */ static fromObject(obj: Record<string, Term>): Bindings { const res = new BindingBase(); for (let key in obj) { res.set(key, obj[key]); } return res; } static fromValuePatternRow(row: ValuePatternRow): Bindings { const res = new BindingBase(); for (let variableString in row) { const v = row[variableString]; if (v) { res.set(stringToTerm(variableString).value, v); } } return res; } variables(): IterableIterator<string> { return this._content.keys(); } values(): IterableIterator<Term> { return this._content.values(); } get(variable: string): Term | null { if (this._content.has(variable)) { return this._content.get(variable)!; } return null; } has(variable: string): boolean { return this._content.has(variable); } set(variable: string, value: Term): void { this._content.set(variable, value); } clear(): void { this._content.clear(); } empty(): Bindings { return new BindingBase(); } forEach(callback: (variable: string, value: Term) => void): void { this._content.forEach((value, variable) => callback(variable, value)); } }