@jahed/sparql-engine
Version:
SPARQL query engine for servers and web browsers.
155 lines • 7.17 kB
JavaScript
// 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