@jahed/sparql-engine
Version:
SPARQL query engine for servers and web browsers.
575 lines (507 loc) • 17 kB
text/typescript
// SPDX-License-Identifier: MIT
import {
addMilliseconds,
isAfter,
isBefore,
isEqual,
subMilliseconds,
} from "date-fns";
import { isNull } from "lodash-es";
import type { EngineTripleValue } from "../../types.ts";
import { parseISO8601 } from "../../utils/date.ts";
import {
asJS,
createBoolean,
createDate,
createDecimal,
createInteger,
createLangLiteral,
createTypedLiteral,
literalIsBoolean,
literalIsDate,
literalIsNumeric,
RDF,
shallowCloneTerm,
termIsBNode,
termIsIRI,
termIsLiteral,
UNBOUND,
XSD_integer,
} from "../../utils/rdf.ts";
type Term = EngineTripleValue;
function digestOperation(hashType: string): (v: Term) => Promise<Term> {
return async (v) => {
return RDF.literal(
toHex(
await crypto.subtle.digest(hashType, new TextEncoder().encode(v.value))
)
);
};
}
function toHex(buffer: ArrayBuffer): string {
const result: string[] = [];
for (const byte of new Uint8Array(buffer)) {
result.push(byte.toString(16).padStart(2, "0"));
}
return result.join("");
}
/**
* Implementation of SPARQL operations found in FILTERS
* All arguments are pre-compiled from string to an intermediate representation.
* All possible intermediate representation are gathered in the `src/rdf-terms.js` file,
* and are used to represents RDF Terms.
* Each SPARQL operation is also expected to return the same kind of intermediate representation.
*/
export default {
/*
COALESCE function https://www.w3.org/TR/sparql11-query/#func-coalesce
*/
coalesce: function (baseValue: Term | null, defaultValue: Term | null): Term {
if (!isNull(baseValue)) {
return baseValue;
} else if (!isNull(defaultValue)) {
return defaultValue;
}
return UNBOUND;
},
/*
IF function https://www.w3.org/TR/sparql11-query/#func-if
*/
if: function (
booleanValue: Term | null,
valueIfTrue: Term | null,
valueIfFalse: Term | null
): Term {
if (isNull(booleanValue) || isNull(valueIfTrue) || isNull(valueIfFalse)) {
throw new SyntaxError(
`SPARQL expression error: some arguments of an IF function are unbound. Got IF(${booleanValue}, ${valueIfTrue}, ${valueIfFalse})`
);
}
if (
termIsLiteral(booleanValue) &&
(literalIsBoolean(booleanValue) || literalIsNumeric(booleanValue))
) {
return asJS(booleanValue.value, booleanValue.datatype.value)
? valueIfTrue
: valueIfFalse;
}
throw new SyntaxError(
`SPARQL expression error: you are using an IF function whose first argument is expected to be a boolean, but instead got ${booleanValue}`
);
},
/*
XQuery & XPath functions https://www.w3.org/TR/sparql11-query/#OperatorMapping
*/
"+": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createDate(addMilliseconds(valueA, valueB.getTime()));
}
return createTypedLiteral(valueA + valueB, a.datatype.value);
}
return RDF.literal(asJS(a.value, null) + asJS(b.value, null));
},
"-": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createDate(subMilliseconds(valueA, valueB.getTime()));
}
return createTypedLiteral(valueA - valueB, a.datatype.value);
}
throw new SyntaxError(
`SPARQL expression error: cannot substract non-Literals ${a} and ${b}`
);
},
"*": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createDate(new Date(valueA.getTime() * valueB.getTime()));
}
return createTypedLiteral(valueA * valueB, a.datatype.value);
}
throw new SyntaxError(
`SPARQL expression error: cannot multiply non-Literals ${a} and ${b}`
);
},
"/": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createDate(new Date(valueA.getTime() / valueB.getTime()));
}
return createTypedLiteral(valueA / valueB, a.datatype.value);
}
throw new SyntaxError(
`SPARQL expression error: cannot divide non-Literals ${a} and ${b}`
);
},
"=": function (a: Term, b: Term): Term {
return createBoolean(a.equals(b));
},
"!=": function (a: Term, b: Term): Term {
return createBoolean(!a.equals(b));
},
"<": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createBoolean(isBefore(valueA, valueB));
}
return createBoolean(valueA < valueB);
}
return createBoolean(a.value < b.value);
},
"<=": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createBoolean(
isEqual(valueA, valueB) || isBefore(valueA, valueB)
);
}
return createBoolean(valueA <= valueB);
}
return createBoolean(a.value <= b.value);
},
">": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createBoolean(isAfter(valueA, valueB));
}
return createBoolean(valueA > valueB);
}
return createBoolean(a.value > b.value);
},
">=": function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
const valueA = asJS(a.value, a.datatype.value);
const valueB = asJS(b.value, b.datatype.value);
if (literalIsDate(a) && literalIsDate(b)) {
return createBoolean(
isEqual(valueA, valueB) || isAfter(valueA, valueB)
);
}
return createBoolean(valueA >= valueB);
}
return createBoolean(a.value >= b.value);
},
"!": function (a: Term): Term {
if (termIsLiteral(a) && literalIsBoolean(a)) {
return createBoolean(!asJS(a.value, a.datatype.value));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the negation of a non boolean literal ${a}`
);
},
"&&": function (a: Term, b: Term): Term {
if (
termIsLiteral(a) &&
termIsLiteral(b) &&
literalIsBoolean(a) &&
literalIsBoolean(b)
) {
return createBoolean(
asJS(a.value, a.datatype.value) && asJS(b.value, b.datatype.value)
);
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the conjunction of non boolean literals ${a} and ${b}`
);
},
"||": function (a: Term, b: Term): Term {
if (
termIsLiteral(a) &&
termIsLiteral(b) &&
literalIsBoolean(a) &&
literalIsBoolean(b)
) {
return createBoolean(
asJS(a.value, a.datatype.value) || asJS(b.value, b.datatype.value)
);
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the disjunction of non boolean literals ${a} and ${b}`
);
},
/*
SPARQL Functional forms https://www.w3.org/TR/sparql11-query/#func-forms
*/
bound: function (a: Term) {
return createBoolean(!isNull(a));
},
sameterm: function (a: Term, b: Term): Term {
return createBoolean(a.value === b.value);
},
in: function (a: Term, b: Term[]): Term {
return createBoolean(b.some((elt) => a.equals(elt)));
},
notin: function (a: Term, b: Term[]): Term {
return createBoolean(!b.some((elt) => a.equals(elt)));
},
/*
Functions on RDF Terms https://www.w3.org/TR/sparql11-query/#func-rdfTerms
*/
isiri: function (a: Term): Term {
return createBoolean(termIsIRI(a));
},
isblank: function (a: Term): Term {
return createBoolean(termIsBNode(a));
},
isliteral: function (a: Term): Term {
return createBoolean(termIsLiteral(a));
},
isnumeric: function (a: Term): Term {
return createBoolean(termIsLiteral(a) && literalIsNumeric(a));
},
str: function (a: Term): Term {
return RDF.literal(a.value);
},
lang: function (a: Term): Term {
if (termIsLiteral(a)) {
return RDF.literal(a.language.toLowerCase());
}
return RDF.literal("");
},
datatype: function (a: Term): Term {
if (termIsLiteral(a)) {
return a.datatype;
}
return RDF.literal("");
},
iri: function (a: Term): Term {
return RDF.namedNode(a.value);
},
bnode: function (a?: Term): Term {
if (a === undefined) {
return RDF.blankNode();
}
return RDF.blankNode(a.value);
},
strdt: function (x: Term, datatype: Term): Term {
return createTypedLiteral(x.value, datatype.value);
},
strlang: function (x: Term, lang: Term): Term {
return createLangLiteral(x.value, lang.value);
},
uuid: function (): Term {
return RDF.namedNode(`urn:uuid:${crypto.randomUUID()}`);
},
struuid: function (): Term {
return RDF.literal(crypto.randomUUID());
},
/*
Functions on Strings https://www.w3.org/TR/sparql11-query/#func-strings
*/
strlen: function (a: Term): Term {
return createInteger(a.value.length);
},
substr: function (str: Term, index: Term, length?: Term): Term {
const indexValue = asJS(index.value, XSD_integer);
if (indexValue < 1) {
throw new SyntaxError(
"SPARQL SUBSTR error: the index of the first character in a string is 1 (according to the SPARQL W3C specs)"
);
}
let value = str.value.substring(indexValue - 1);
if (length !== undefined) {
const lengthValue = asJS(length.value, XSD_integer);
value = value.substring(0, lengthValue);
}
return shallowCloneTerm(str, value);
},
ucase: function (a: Term): Term {
return shallowCloneTerm(a, a.value.toUpperCase());
},
lcase: function (a: Term): Term {
return shallowCloneTerm(a, a.value.toLowerCase());
},
strstarts: function (term: Term, substring: Term): Term {
const a = term.value;
const b = substring.value;
return createBoolean(a.startsWith(b));
},
strends: function (term: Term, substring: Term): Term {
const a = term.value;
const b = substring.value;
return createBoolean(a.endsWith(b));
},
contains: function (term: Term, substring: Term): Term {
const a = term.value;
const b = substring.value;
return createBoolean(a.indexOf(b) >= 0);
},
strbefore: function (term: Term, token: Term): Term {
const index = term.value.indexOf(token.value);
const value = index > -1 ? term.value.substring(0, index) : "";
return shallowCloneTerm(term, value);
},
strafter: function (str: Term, token: Term): Term {
const index = str.value.indexOf(token.value);
const value =
index > -1 ? str.value.substring(index + token.value.length) : "";
return shallowCloneTerm(str, value);
},
encode_for_uri: function (a: Term): Term {
return RDF.literal(encodeURIComponent(a.value));
},
concat: function (a: Term, b: Term): Term {
if (termIsLiteral(a) && termIsLiteral(b)) {
return shallowCloneTerm(a, a.value + b.value);
}
return RDF.literal(a.value + b.value);
},
langmatches: function (langTag: Term, langRange: Term): Term {
// Implements https://tools.ietf.org/html/rfc4647#section-3.3.1
const tag = langTag.value.toLowerCase();
const range = langRange.value.toLowerCase();
const test =
tag === range ||
range === "*" ||
tag.substr(1, range.length + 1) === range + "-";
return createBoolean(test);
},
regex: function (subject: Term, pattern: Term, flags?: Term) {
const regexp =
flags === undefined
? new RegExp(pattern.value)
: new RegExp(pattern.value, flags.value);
return createBoolean(regexp.test(subject.value));
},
replace: function (
arg: Term,
pattern: Term,
replacement: Term,
flags?: Term
) {
const regexp =
flags === undefined
? new RegExp(pattern.value)
: new RegExp(pattern.value, flags.value);
const newValue = arg.value.replace(regexp, replacement.value);
if (termIsIRI(arg)) {
return RDF.namedNode(newValue);
} else if (termIsBNode(arg)) {
return RDF.blankNode(newValue);
}
return shallowCloneTerm(arg, newValue);
},
/*
Functions on Numerics https://www.w3.org/TR/sparql11-query/#func-numerics
*/
abs: function (a: Term): Term {
if (termIsLiteral(a) && literalIsNumeric(a)) {
return createInteger(Math.abs(asJS(a.value, a.datatype.value)));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the absolute value of the non-numeric term ${a}`
);
},
round: function (a: Term): Term {
if (termIsLiteral(a) && literalIsNumeric(a)) {
return createInteger(Math.round(asJS(a.value, a.datatype.value)));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the rounded value of the non-numeric term ${a}`
);
},
ceil: function (a: Term): Term {
if (termIsLiteral(a) && literalIsNumeric(a)) {
return createInteger(Math.ceil(asJS(a.value, a.datatype.value)));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute Math.ceil on the non-numeric term ${a}`
);
},
floor: function (a: Term): Term {
if (termIsLiteral(a) && literalIsNumeric(a)) {
return createInteger(Math.floor(asJS(a.value, a.datatype.value)));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute Math.floor on the non-numeric term ${a}`
);
},
/*
Functions on Dates and Times https://www.w3.org/TR/sparql11-query/#func-date-time
*/
now: function (): Term {
return createDate(new Date());
},
year: function (a: Term): Term {
if (termIsLiteral(a) && literalIsDate(a)) {
return createInteger(Number(parseISO8601(a.value).year));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the year of the RDF Term ${a}, as it is not a date`
);
},
month: function (a: Term): Term {
if (termIsLiteral(a) && literalIsDate(a)) {
const value = asJS(a.value, a.datatype.value);
return createInteger(Number(parseISO8601(a.value).month));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the month of the RDF Term ${a}, as it is not a date`
);
},
day: function (a: Term): Term {
if (termIsLiteral(a) && literalIsDate(a)) {
return createInteger(Number(parseISO8601(a.value).day));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the day of the RDF Term ${a}, as it is not a date`
);
},
hours: function (a: Term): Term {
if (termIsLiteral(a) && literalIsDate(a)) {
return createInteger(Number(parseISO8601(a.value).hour));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the hours of the RDF Term ${a}, as it is not a date`
);
},
minutes: function (a: Term): Term {
if (termIsLiteral(a) && literalIsDate(a)) {
return createInteger(Number(parseISO8601(a.value).minute));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the minutes of the RDF Term ${a}, as it is not a date`
);
},
seconds: function (a: Term): Term {
if (termIsLiteral(a) && literalIsDate(a)) {
return createDecimal(Number(parseISO8601(a.value).second));
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the seconds of the RDF Term ${a}, as it is not a date`
);
},
tz: function (a: Term): Term {
if (termIsLiteral(a) && literalIsDate(a)) {
return RDF.literal(parseISO8601(a.value).timezone);
}
throw new SyntaxError(
`SPARQL expression error: cannot compute the timezone of the RDF Term ${a}, as it is not a date`
);
},
/*
Hash Functions https://www.w3.org/TR/sparql11-query/#func-hash
*/
md5: async function (a: Term): Promise<Term> {
return RDF.literal((await import("js-md5")).md5.hex(a.value));
},
sha1: digestOperation("SHA-1"),
sha256: digestOperation("SHA-256"),
sha384: digestOperation("SHA-384"),
sha512: digestOperation("SHA-512"),
};