Contents
SPARQL Functions
rdf_fdw implements most of the SPARQL 1.1 built-in functions as SQL functions in the sparql schema, which avoids clashes with PostgreSQL built-ins such as round, replace or ceil. They follow SPARQL semantics for literals, language tags, datatypes and unbound values. When used in the WHERE clause of a query on a FOREIGN TABLE they are pushed down to the endpoint wherever possible.
In the examples below, the result of each statement is shown as a comment. NULL stands for an unbound value or a SPARQL error.
- Triplestores differ
- Aggregates
- Functional forms
- Functions on RDF terms
- Functions on strings
- Functions on numerics
- Functions on dates and times
- Hash functions
- Non-standard functions
Triplestores differ
While most RDF triplestores claim SPARQL 1.1 compliance, their behaviour often diverges from the standard, particularly for literals with language tags or datatypes. For example:
SELECT (REPLACE("foo"@en, "o"@de, "xx"@fr) AS ?str) {}
- Virtuoso →
"fxxxx" - Blazegraph → Unknown error: incompatible operand for REPLACE: “o”@de
- GraphDB →
"fxxxx"@en
A pushed-down function is evaluated by the endpoint, and PostgreSQL re-checks the returned rows with its own implementation. When the two disagree, rows are silently filtered out locally. To avoid surprises:
- Test how your target triplestore handles tagged or typed literals.
- Consider simpler (less performant) alternatives, such as comparing
strvalues, when working with language-tagged literals. - Compare the number of records returned by the endpoint with the number of rows you get, using EXPLAIN or the
log_sparqltable option. If the endpoint returned more records than you see, some were filtered out locally.
Aggregates
| Function | Returns | Description |
|---|---|---|
sparql.sum(rdfnode) |
rdfnode |
Sum of numeric values (§18.5.1.3) |
sparql.avg(rdfnode) |
rdfnode |
Arithmetic mean of numeric values (§18.5.1.4) |
sparql.min(rdfnode) |
rdfnode |
Smallest value (§18.5.1.5) |
sparql.max(rdfnode) |
rdfnode |
Largest value (§18.5.1.6) |
sparql.group_concat(rdfnode, separator text) |
rdfnode |
Joins the lexical forms into one simple literal (§18.5.1.7) |
sparql.sample(rdfnode) |
rdfnode |
An arbitrary value of the group, currently the first non-NULL one (§18.5.1.8) |
[!NOTE] Aggregates are always evaluated by PostgreSQL; they are not pushed down. For remote aggregation, put the aggregate in the foreign table’s
sparqloption.
Semantics
- SQL
NULLis treated as SPARQL UNDEF and skipped. - If no values remain (an empty group, or only
NULLs):sum,avg→"0"^^xsd:integer(per spec: “the sum of no bindings is 0”)group_concat→""min,max,sample→NULL
sumandavgreturnNULLif any value is not numeric.- Numeric types are promoted following XPath rules,
xsd:integer<xsd:decimal<xsd:float<xsd:double, and the result has the highest type encountered. All integer subtypes (xsd:int,xsd:long,xsd:short, …) count asxsd:integer.avgof a non-empty set is at leastxsd:decimal. minandmaxkeep the datatype of the value they return and also accept non-numeric values. Values of different kinds (e.g. numbers and strings) are ordered following SPARQL 1.1 §15.1. That ordering is only partially specified, so results for mixed sets may differ from other triplestores.group_concatstrips datatypes, language tags and the angle brackets of IRIs. Unlikestring_agg, the separator is required.
SELECT sparql.sum(v) FROM (VALUES ('"10"^^xsd:integer'::rdfnode), ('"20.5"^^xsd:decimal'), ('"30"^^xsd:integer')) t(v);
-- "60.5"^^<http://www.w3.org/2001/XMLSchema#decimal>
SELECT sparql.sum(v) FROM (VALUES ('"10.5"^^xsd:decimal'::rdfnode), ('"20.3"^^xsd:decimal'), ('"5"^^xsd:float')) t(v);
-- "35.8"^^<http://www.w3.org/2001/XMLSchema#float>
SELECT sparql.avg(v) FROM (VALUES ('"10"^^xsd:integer'::rdfnode), (NULL), ('"30"^^xsd:integer')) t(v);
-- "20.0"^^<http://www.w3.org/2001/XMLSchema#decimal>
SELECT sparql.min(v), sparql.max(v) FROM (VALUES ('"10"^^xsd:integer'::rdfnode), ('"-5"^^xsd:integer'), ('"3"^^xsd:integer')) t(v);
-- "-5"^^<http://www.w3.org/2001/XMLSchema#integer> | "10"^^<http://www.w3.org/2001/XMLSchema#integer>
SELECT sparql.group_concat(v, ' | ') FROM (VALUES ('"42"^^xsd:integer'::rdfnode), ('"hello"@en'), ('<http://example.org/r1>')) t(v);
-- "42 | hello | http://example.org/r1"
SELECT sparql.sample(v) FROM (VALUES ('"hello"@en'::rdfnode), ('"bonjour"@fr')) t(v);
-- "hello"@en
SELECT sparql.sum(v), sparql.avg(v), sparql.group_concat(v, ','), sparql.min(v) FROM (SELECT NULL::rdfnode WHERE false) t(v);
-- "0"^^<http://www.w3.org/2001/XMLSchema#integer> | "0"^^<http://www.w3.org/2001/XMLSchema#integer> | "" | NULL
Functional forms
| Function | Returns | Description |
|---|---|---|
sparql.bound(rdfnode) |
boolean |
true if the value is not NULL. NaN and INF are bound. |
sparql.coalesce(rdfnode, ...) |
rdfnode |
The first argument that is not NULL, evaluated left to right |
sparql.sameterm(rdfnode, rdfnode) |
boolean |
true if both are exactly the same term, including datatype and language tag, without any coercion |
SELECT sparql.bound(NULL), sparql.bound('"NaN"^^xsd:double'); -- f | t
SELECT sparql.coalesce(NULL, NULL, '"foo"@en'); -- "foo"@en
SELECT sparql.sameterm('"42"^^xsd:int', '"42"^^xsd:long'); -- f
SELECT '"42"^^xsd:int'::rdfnode = '"42"^^xsd:long'::rdfnode; -- t
SELECT sparql.sameterm('"foo"@en', '"foo"@en'); -- t
Use sameterm for exact RDF identity. = compares values, with numeric promotion (see Comparing, sorting and grouping).
Functions on RDF terms
| Function | Returns | Description |
|---|---|---|
sparql.isiri(rdfnode), sparql.isuri(rdfnode) |
boolean |
true for IRIs |
sparql.isblank(rdfnode) |
boolean |
true for blank nodes |
sparql.isliteral(rdfnode) |
boolean |
true for literals |
sparql.isnumeric(rdfnode) |
boolean |
true for literals of a numeric datatype (xsd:integer, xsd:int, xsd:decimal, xsd:double, …) |
sparql.str(rdfnode) |
rdfnode |
Lexical form as a simple literal: drops language tag and datatype; IRIs become their string |
sparql.lang(rdfnode) |
rdfnode |
Language tag of a literal, or an empty string. NULL for IRIs and blank nodes. |
sparql.datatype(rdfnode) |
rdfnode |
Datatype IRI: xsd:string for simple literals, rdf:langString for language-tagged ones, NULL for IRIs and blank nodes |
sparql.iri(rdfnode), sparql.uri(rdfnode) |
rdfnode |
Wraps a string as an IRI. No validation is performed. |
sparql.bnode([rdfnode]) |
rdfnode |
Blank node with the given label, or a fresh one without argument |
sparql.strdt(lexical rdfnode, datatype rdfnode) |
rdfnode |
Typed literal. Applied to a literal, it replaces the datatype. |
sparql.strlang(lexical rdfnode, lang rdfnode) |
rdfnode |
Language-tagged literal. Applied to a tagged literal, it replaces the tag. |
sparql.uuid() |
rdfnode |
Fresh urn:uuid: IRI |
sparql.struuid() |
rdfnode |
Fresh UUID as a simple literal |
SELECT sparql.isiri('<https://foo.bar/>'), sparql.isiri('_:bnode42'); -- t | f
SELECT sparql.isblank('_:bnode42'); -- t
SELECT sparql.isliteral('"foo"@es'), sparql.isliteral('<http://foo.bar>'); -- t | f
SELECT sparql.isnumeric('"42.73"^^xsd:decimal'); -- t
SELECT sparql.isnumeric('"42.73"^^xsd:string'); -- f
SELECT sparql.str('"foo"@en'); -- "foo"
SELECT sparql.str('<http://foo.bar>'); -- "http://foo.bar"
SELECT sparql.lang('"foo"@es'); -- es
SELECT sparql.datatype('"42"^^xsd:int'); -- <http://www.w3.org/2001/XMLSchema#int>
SELECT sparql.datatype('"foo"'); -- <http://www.w3.org/2001/XMLSchema#string>
SELECT sparql.datatype('"foo"@de'); -- <http://www.w3.org/1999/02/22-rdf-syntax-ns#langString>
SELECT sparql.datatype('<http://foo.bar>'); -- NULL
SELECT sparql.iri('http://foo.bar'); -- <http://foo.bar>
SELECT sparql.bnode('foo'); -- _:foo
SELECT sparql.bnode(); -- _:b844460685272636_0
SELECT sparql.strdt('"2025-01-01"^^xsd:string', 'http://www.w3.org/2001/XMLSchema#date');
-- "2025-01-01"^^<http://www.w3.org/2001/XMLSchema#date>
SELECT sparql.strlang('"foo"@pt', 'es'); -- "foo"@es
SELECT sparql.uuid(); -- <urn:uuid:1beda602-2e35-4d13-a907-071454d2fce7>
SELECT sparql.struuid(); -- "25a55e10-f789-4aab-bb7f-05f2ba495fd2"
[!NOTE] Some triplestores (like Virtuoso) return
xsd:anyURIas theDATATYPEof an IRI. This is not defined in SPARQL 1.1.
Functions on strings
Unless stated otherwise, these functions work on the lexical form and the result keeps the language tag or datatype of the first argument. Where a function takes two string arguments, they must be argument-compatible: a language-tagged second argument only matches a first argument with the same tag. Otherwise the result is NULL.
Most of these functions are only defined for string literals (simple, xsd:string or language-tagged). For other terms SPARQL raises an error, which leaves the result unbound, and rdf_fdw returns NULL, so a query over a column that mixes literals with IRIs keeps its rows. Use sparql.str() to turn other terms into strings first.
| Function | Returns | Description |
|---|---|---|
sparql.strlen(rdfnode) |
int |
Number of characters of a string literal. NULL for IRIs, blank nodes and non-string literals. |
sparql.substr(rdfnode, start int [, length int]) |
rdfnode |
Substring; start is 1-based. Without length, it runs to the end. |
sparql.ucase(rdfnode) / sparql.lcase(rdfnode) |
rdfnode |
Upper / lower case |
sparql.strstarts(rdfnode, rdfnode) |
boolean |
Starts with |
sparql.strends(rdfnode, rdfnode) |
boolean |
Ends with |
sparql.contains(rdfnode, rdfnode) |
boolean |
Contains |
sparql.strbefore(rdfnode, rdfnode) |
rdfnode |
Part before the first match, or an empty string |
sparql.strafter(rdfnode, rdfnode) |
rdfnode |
Part after the first match, or an empty string |
sparql.encode_for_uri(rdfnode) |
rdfnode |
Percent-encodes reserved characters; always returns a simple literal |
sparql.concat(rdfnode, ...) |
rdfnode |
Concatenation. The result keeps a language tag or datatype only when all arguments share it. |
sparql.langmatches(tag rdfnode, range rdfnode) |
boolean |
Language tag matches a language range (* matches any tag) |
sparql.replace(rdfnode, pattern rdfnode, replacement rdfnode [, flags rdfnode]) |
rdfnode |
Replaces all matches of a regular expression |
SELECT sparql.strlen('"🐘"@en'); -- 1
SELECT sparql.strlen(sparql.str('"42"^^xsd:int')); -- 2
SELECT sparql.substr('"foobar"', 1, 3), sparql.substr('"foobar"', 4); -- "foo" | "bar"
SELECT sparql.ucase('"foo"@en'); -- "FOO"@en
SELECT sparql.lcase('"FOO"^^xsd:string'); -- "foo"^^<http://www.w3.org/2001/XMLSchema#string>
SELECT sparql.strstarts('"foobar"@en', '"foo"^^xsd:string'); -- t
SELECT sparql.strends('"foobar"^^xsd:string', '"bar"^^xsd:string'); -- t
SELECT sparql.contains('"_foobar_"@en', '"foo"'); -- t
SELECT sparql.contains('"_foobar_"^^xsd:string', '"foo"@en'); -- NULL
SELECT sparql.strbefore('"foobar"@en', '"bar"^^xsd:string'); -- "foo"@en
SELECT sparql.strafter('"foobar"', '"foo"'); -- "bar"
SELECT sparql.encode_for_uri('"foo&bar!"'); -- "foo%26bar%21"
SELECT sparql.concat('"foo"@en', '"&"@en', '"bar"@en'); -- "foo&bar"@en
SELECT sparql.concat('"foo"', '"&"', '"bar"'); -- "foo&bar"
SELECT sparql.langmatches('en-US', 'en'), sparql.langmatches('en', 'de'); -- t | f
SELECT sparql.langmatches('en', '*'); -- t
SELECT sparql.replace('"foo bar foo"', 'foo', 'baz'); -- "baz bar baz"
SELECT sparql.replace('"abc"', '', 'Z'); -- "ZaZbZcZ"
SELECT sparql.replace('"2025-01-02"', '"([0-9]{4})-([0-9]{2})-([0-9]{2})"', '"$3/$2/$1"'); -- "02/01/2025"
REPLACE
- Supports the
i,mandsflags. - An empty pattern matches at every position, so the replacement is inserted between all characters and at both ends.
- The replacement uses the XPath syntax required by the spec:
$1to$9insert the captured groups,\$is a literal dollar and\\a literal backslash. - The regex engine follows PostgreSQL’s POSIX ERE dialect rather than the XPath dialect required by SPARQL 1.1. For most common patterns they are equivalent, but Unicode category escapes (
\p{L},\p{N}, …) are not supported.
[!WARNING] Some triplestores count bytes instead of characters in
STRLEN. GraphDB, for instance, returns 2 forSTRLEN("🐘"@en), whereas Fuseki, Virtuoso andsparql.strlenreturn 1.
Functions on numerics
All of these keep the datatype of their argument.
| Function | Returns | Description |
|---|---|---|
sparql.abs(rdfnode) |
rdfnode |
Absolute value |
sparql.round(rdfnode) |
rdfnode |
Nearest integer; halves are rounded towards positive infinity |
sparql.ceil(rdfnode) |
rdfnode |
Smallest integer not less than the value |
sparql.floor(rdfnode) |
rdfnode |
Largest integer not greater than the value |
sparql.rand() |
rdfnode |
Random xsd:double between 0 and 1. Never pushed down. |
SELECT sparql.abs('"-42"^^xsd:int'); -- "42"^^<http://www.w3.org/2001/XMLSchema#int>
SELECT sparql.round('"2.5"^^xsd:decimal'); -- "3"^^<http://www.w3.org/2001/XMLSchema#decimal>
SELECT sparql.round('"-2.5"^^xsd:float'); -- "-2"^^<http://www.w3.org/2001/XMLSchema#float>
SELECT sparql.ceil('"3.14"^^xsd:decimal'); -- "4"^^<http://www.w3.org/2001/XMLSchema#decimal>
SELECT sparql.floor('"-2.1"^^xsd:float'); -- "-3"^^<http://www.w3.org/2001/XMLSchema#float>
SELECT sparql.rand(); -- "0.14079881274421657"^^<http://www.w3.org/2001/XMLSchema#double>
Functions on dates and times
The component functions accept xsd:dateTime and xsd:date literals (hours, minutes and seconds also xsd:time).
| Function | Returns | Description |
|---|---|---|
sparql.now() |
rdfnode |
Current xsd:dateTime, constant within a transaction. Never pushed down. |
sparql.year(rdfnode) |
int |
Year |
sparql.month(rdfnode) |
int |
Month (1–12) |
sparql.day(rdfnode) |
int |
Day of the month |
sparql.hours(rdfnode) |
int |
Hour (0–23) |
sparql.minutes(rdfnode) |
int |
Minute (0–59) |
sparql.seconds(rdfnode) |
numeric |
Seconds, including fractions |
sparql.timezone(rdfnode) |
rdfnode |
Timezone offset as xsd:dayTimeDuration. A value without a timezone raises an error. |
sparql.tz(rdfnode) |
rdfnode |
Timezone offset as a simple literal ("+02:00", "Z"), or "" if there is none |
SELECT sparql.year('"2025-05-17T14:42:37Z"^^xsd:dateTime'); -- 2025
SELECT sparql.month('"2025-05-17T14:42:37Z"^^xsd:dateTime'); -- 5
SELECT sparql.day('"2025-05-17T14:42:37Z"^^xsd:dateTime'); -- 17
SELECT sparql.hours('"2025-05-17T14:42:37Z"^^xsd:dateTime'); -- 14
SELECT sparql.minutes('"2025-05-17T14:42:37Z"^^xsd:dateTime'); -- 42
SELECT sparql.seconds('"2025-05-17T14:42:37.5Z"^^xsd:dateTime'); -- 37.500000
SELECT sparql.timezone('"2025-05-17T10:00:00+02:00"^^xsd:dateTime');
-- "PT2H"^^<http://www.w3.org/2001/XMLSchema#dayTimeDuration>
SELECT sparql.tz('"2025-05-17T10:00:00+02:00"^^xsd:dateTime'); -- "+02:00"
SELECT sparql.tz('"2025-05-17T08:00:00Z"^^xsd:dateTime'); -- "Z"
SELECT sparql.tz('"2025-05-17T08:00:00"^^xsd:dateTime'); -- ""
SELECT sparql.now(); -- "2026-10-04T20:25:20.960274Z"^^<http://www.w3.org/2001/XMLSchema#dateTime>
Hash functions
| Function | Returns | Description |
|---|---|---|
sparql.md5(rdfnode) |
rdfnode |
MD5 of the lexical form, as a lowercase hexadecimal simple literal |
SELECT sparql.md5('"foo"'); -- "acbd18db4cc2f85cedef654fccc4a4d8"
Non-standard functions
| Function | Returns | Description |
|---|---|---|
sparql.lex(rdfnode) |
text |
Lexical form as plain PostgreSQL text: no quotes, language tag or datatype. Handy for casting to other types, e.g. sparql.lex(o)::geometry. |
SELECT sparql.lex('"foo"@es'); -- foo
SELECT sparql.lex('"foo"^^xsd:string'); -- foo