UNPKG

@jahed/sparql-engine

Version:

SPARQL query engine for servers and web browsers.

155 lines 7.17 kB
// SPDX-License-Identifier: MIT import { isArray, merge, uniqBy } from "lodash-es"; import { termToString } from "rdf-string"; import { Bindings } from "../../rdf/bindings.js"; import { isVariable } from "../../utils/rdf.js"; import CUSTOM_AGGREGATES from "./custom-aggregates.js"; import CUSTOM_OPERATIONS from "./custom-operations.js"; import SPARQL_AGGREGATES from "./sparql-aggregates.js"; import SPARQL_OPERATIONS from "./sparql-operations.js"; /** * Test if a SPARQL expression is a SPARQL operation * @param expr - SPARQL expression, in sparql.js format * @return True if the SPARQL expression is a SPARQL operation, False otherwise */ function isOperation(expr) { return "operator" in expr; } /** * Test if a SPARQL expression is a SPARQL aggregation * @param expr - SPARQL expression, in sparql.js format * @return True if the SPARQL expression is a SPARQL aggregation, False otherwise */ function isAggregation(expr) { return "aggregation" in expr; } /** * Test if a SPARQL expression is a SPARQL function call (like a custom function) * @param expr - SPARQL expression, in sparql.js format * @return True if the SPARQL expression is a SPARQL function call, False otherwise */ function isFunctionCall(expr) { return "function" in expr; } /** * Compile and evaluate a SPARQL expression (found in FILTER clauses, for example) */ export class SPARQLExpression { _expression; /** * Constructor * @param expression - SPARQL expression */ constructor(expression, customFunctions) { // merge custom operations defined by the framework & by the user const customs = merge({}, CUSTOM_OPERATIONS, customFunctions); this._expression = this._compileExpression(expression, customs); } /** * Recursively compile a SPARQL expression into a function * @param expression - SPARQL expression * @return Compiled SPARQL expression */ _compileExpression(expression, customFunctions) { // case 1: the expression is a SPARQL variable to bound or a RDF term if ("termType" in expression) { if (isVariable(expression)) { return async (bindings) => { return bindings.get(expression.value); }; } return async () => expression; } else if (isArray(expression)) { // case 2: the expression is a list of RDF terms // because IN and NOT IN expressions accept arrays as argument return async () => expression; } else if (isOperation(expression)) { // case 3: a SPARQL operation, so we recursively compile each argument // and then evaluate the expression const args = expression.args.map((arg) => this._compileExpression(arg, customFunctions)); if (!(expression.operator in SPARQL_OPERATIONS)) { throw new Error(`Unsupported SPARQL operation: ${expression.operator}`); } const operation = SPARQL_OPERATIONS[expression.operator]; return async (bindings) => operation(...(await Promise.all(args.map((arg) => arg(bindings))))); } else if (isAggregation(expression)) { const aggVariable = expression.expression; // case 3: a SPARQL aggregation if (!(expression.aggregation in SPARQL_AGGREGATES)) { throw new Error(`Unsupported SPARQL aggregation: ${expression.aggregation}`); } if (!isVariable(aggVariable)) { throw new Error(`SPARQL aggregation expression must be a variable: ${aggVariable}`); } const aggregation = SPARQL_AGGREGATES[expression.aggregation]; return async (bindings) => { if (bindings.hasProperty("__aggregate")) { let rows = bindings.getProperty("__aggregate"); if (expression.distinct) { rows[aggVariable.value] = uniqBy(rows[aggVariable.value], (term) => termToString(term)); } return aggregation(aggVariable, rows, expression.separator); } throw new SyntaxError(`SPARQL aggregation error: you are trying to use the ${expression.aggregation} SPARQL aggregate outside of an aggregation query.`); }; } else if (isFunctionCall(expression)) { // last case: the expression is a custom function let customFunction; let isAggregate = false; const functionName = typeof expression.function === "string" ? expression.function : termToString(expression.function); // custom aggregations defined by the framework const functionNameLower = functionName.toLowerCase(); if (functionNameLower in CUSTOM_AGGREGATES) { isAggregate = true; customFunction = CUSTOM_AGGREGATES[functionNameLower]; } else if (functionName in customFunctions) { // custom operations defined by the user & the framework customFunction = customFunctions[functionName]; } else { throw new SyntaxError(`Custom function could not be found: ${functionName}`); } if (isAggregate) { return async (bindings) => { if (bindings.hasProperty("__aggregate")) { const rows = bindings.getProperty("__aggregate"); return customFunction(...expression.args, rows); } throw new SyntaxError(`SPARQL aggregation error: you are trying to use the ${functionName} SPARQL aggregate outside of an aggregation query.`); }; } return async (bindings) => { try { const args = expression.args.map((args) => this._compileExpression(args, customFunctions)); return customFunction(...(await Promise.all(args.map((arg) => arg(bindings))))); } catch (e) { // In section 10 of the sparql docs (https://www.w3.org/TR/sparql11-query/#assignment) it states: // "If the evaluation of the expression produces an error, the variable remains unbound for that solution but the query evaluation continues." // unfortunately this means the error is silent unless some logging is introduced here, // which is probably not desired unless a logging framework is introduced return null; } }; } console.log({ expression }); throw new Error(`Unsupported SPARQL operation type.`); } /** * Evaluate the expression using a set of mappings * @param bindings - Set of mappings * @return Results of the evaluation */ async evaluate(bindings) { return this._expression(bindings); } } //# sourceMappingURL=sparql-expression.js.map