OpenLink Virtuoso: RDF 1.2 and SPARQL 1.2 Implementation Notes - pkleef/virtuoso-opensource GitHub Wiki
Copyright 2026 OpenLink Software [email protected]
This document describes the RDF 1.2 and SPARQL 1.2 enhancements implemented in OpenLink Virtuoso, covering triple terms, reified triples, annotation syntax, directional language-tagged strings, new built-in functions, and related parser and serialization updates.
- Triple Terms
- Reified Triples
- Annotation Syntax (Turtle / SPARQL)
- Directional Language-Tagged Strings
- New SPARQL 1.2 Built-in Functions
- SPARQL 1.2 Prolog:
VERSIONDirective - New RDF Datatypes Recognized
- RDF/XML Parser Extensions
- N-Quads / N-Triples 1.2 Extensions
- Turtle / SPARQL Lexer Refinements
- Configuration / Compiler Hooks
- Result Serialization Updates
- Notable Validation Rules Enforced
- References
A triple term is a triple used as a term in another triple. Virtuoso models a
triple term internally as a minted IRI under the namespace urn:rdf-star:triple:.
Two surface forms are supported:
| Form | Syntax | Where allowed |
|---|---|---|
| Bare triple term | <<( s p o )>> |
Object position only — never as subject |
| Asserted triple term | <<{ s p o }>> |
Object position; also asserts the inner triple |
@prefix ex: <http://example.org/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
# "Alice said that Bob knows Carol" — the inner triple is referenced but
# NOT asserted into the graph.
ex:alice prov:wasDerivedFrom <<( ex:bob ex:knows ex:carol )>> .@prefix ex: <http://example.org/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
# Both rows enter the store: the outer prov:wasDerivedFrom row AND
# the inner (ex:bob ex:knows ex:carol) triple.
ex:alice prov:wasDerivedFrom <<{ ex:bob ex:knows ex:carol }>> .PREFIX ex: <http://example.org/>
PREFIX prov: <http://www.w3.org/ns/prov#>
# Match anything whose provenance is a triple term about ex:bob.
SELECT ?who ?p ?o
WHERE {
?who prov:wasDerivedFrom <<( ex:bob ?p ?o )>> .
}A reifier names a triple so that statements can be made about that naming.
Virtuoso supports the RDF 1.2 reifier syntax. Each reifier is represented as a
urn:rdf-star:reifier: IRI in canonical form, connected via a
<reifier> rdf:reifies <triple-term-IRI> triple.
The rdf:reifies property and the rdfs:Proposition class are bootstrapped by
the RDF_12_VOCAB_INIT() SQL routine into the graph urn:rdf12:vocab.
| Surface form | Meaning |
|---|---|
<< s p o >> |
Shorthand: emit a generated blank-node reifier. |
<< s p o ~iri >> |
Explicit reifier — name the reifier with iri. |
<< s p o ~_:b >> |
Explicit reifier — use blank node _:b. |
<< s p o ~ >> |
Anonymous reifier (one-off, no name reusable elsewhere). |
@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# A bnode reifier is generated and connected via rdf:reifies to the
# inner triple term. We can then make further statements about that reifier.
<< ex:alice ex:knows ex:bob >> dct:created "2026-05-22"^^xsd:date .@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
# Reuse the same reifier IRI across multiple statements.
<< ex:alice ex:knows ex:bob ~ex:claim1 >> dct:source ex:emailThread42 .
<< ex:alice ex:knows ex:bob ~ex:claim1 >> dct:created "2026-05-22" .@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
<< ex:alice ex:knows ex:bob ~_:r1 >> dct:source ex:emailThread42 .
_:r1 dct:created "2026-05-22" .@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
# No reusable name — the reifier is a one-off fresh blank node.
<< ex:alice ex:knows ex:bob ~ >> dct:created "2026-05-22" .The bootstrap routine inserts this into urn:rdf12:vocab:
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
rdf:reifies rdf:type rdf:Property ;
rdfs:domain rdfs:Resource ;
rdfs:range rdfs:Proposition ;
rdfs:isDefinedBy rdf: ;
rdfs:label "reifies" ;
rdfs:comment "The subject reifies the object triple term." .PREFIX ex: <http://example.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dct: <http://purl.org/dc/terms/>
# Find the source attached to every reifier of (ex:alice ex:knows ex:bob).
SELECT ?reifier ?source
WHERE {
?reifier rdf:reifies <<( ex:alice ex:knows ex:bob )>> ;
dct:source ?source .
}Annotation blocks let you attach metadata to the most recently written triple
without explicitly naming a reifier. They desugar to a generated reifier plus
rdf:reifies triple plus the annotation properties.
@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice ex:knows ex:bob {| dct:created "2026-05-22"^^xsd:date ;
dct:source ex:emailThread42 |} .This is equivalent to:
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice ex:knows ex:bob .
_:r1 rdf:reifies <<( ex:alice ex:knows ex:bob )>> .
_:r1 dct:created "2026-05-22"^^xsd:date .
_:r1 dct:source ex:emailThread42 .@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
# Bind a named reifier without an annotation block.
ex:alice ex:knows ex:bob ~ex:claim1 .
# Bind a blank-node reifier.
ex:alice ex:knows ex:bob ~_:r2 .
# Bind an anonymous reifier (no name).
ex:alice ex:knows ex:bob ~ .@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice ex:knows ex:bob ~ex:claim1 {|
dct:created "2026-05-22"^^xsd:date ;
dct:source ex:emailThread42
|} .The same {| ... |} syntax can appear in SPARQL graph patterns:
PREFIX ex: <http://example.org/>
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?who ?friend ?createdOn
WHERE {
?who ex:knows ?friend {| dct:created ?createdOn |} .
}A dirLangString literal carries both a language tag and a base text
direction. Virtuoso persists the direction in the language tag itself, so the
internal representation is e.g. ar--rtl or en--ltr.
| Format | Direction |
|---|---|
Turtle "…"@lang--ltr / "…"@lang--rtl
|
Direction encoded in tag |
SPARQL "…"@lang~ltr / "…"@lang~rtl
|
Direction via ~ltr / ~rtl
|
Datatype URI: <http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString>.
@prefix ex: <http://example.org/> .
# Arabic, right-to-left
ex:doc1 ex:title "مرحبا بالعالم"@ar--rtl .
# English, left-to-right
ex:doc1 ex:title "Hello, world"@en--ltr .PREFIX ex: <http://example.org/>
# Note SPARQL uses `~ltr` / `~rtl`, NOT `--ltr` / `--rtl`.
SELECT ?doc
WHERE {
?doc ex:title "مرحبا بالعالم"@ar~rtl .
}See New SPARQL 1.2 Built-in Functions for
DIR(), LANGDIR(), hasLANGDIR(), STRLANGDIR() and STRDIR().
| Function | Notes |
|---|---|
hasLANG(literal) |
Boolean — does the literal carry a lang tag? |
hasLANGDIR(literal) |
Boolean — does it carry a direction? |
LANGDIR(literal) |
Returns "ltr" / "rtl" / unbound. |
STRLANGDIR(str, lang, dir) |
Build a dirLangString literal. |
STRDIR(str, dir) |
Build a directional plain string. |
DIR(literal) |
Direction component ("ltr" / "rtl"). |
sameValue / SAMEVALUE
|
Value-based equality (vs. term-based). |
isTRIPLE(x) / ISTRIPLE
|
Is x a triple-term IRI? |
TRIPLE(s, p, o) |
Construct a triple-term IRI. |
SUBJECT(tt) |
Extract subject of a triple term. |
PREDICATE(tt) |
Extract predicate of a triple term. |
OBJECT(tt) |
Extract object of a triple term. |
contains_token |
SPARQL 1.2 extra. |
format_number |
SPARQL 1.2 extra. |
UNNEST(expr) AS ?var |
Expand an array/list expression into rows. |
sameValue complements sameTerm by providing value-based equality.
contains_token and format_number are SPARQL 1.2 extras advertised in
service descriptions so SPARQL 1.2 clients can negotiate feature availability.
PREFIX ex: <http://example.org/>
PREFIX prov: <http://www.w3.org/ns/prov#>
SELECT ?statement ?s ?p ?o
WHERE {
?subject prov:wasDerivedFrom ?statement .
FILTER (isTRIPLE(?statement))
BIND (SUBJECT(?statement) AS ?s)
BIND (PREDICATE(?statement) AS ?p)
BIND (OBJECT(?statement) AS ?o)
}Build a triple term inline:
PREFIX ex: <http://example.org/>
SELECT (TRIPLE(ex:alice, ex:knows, ex:bob) AS ?stmt) WHERE {}PREFIX ex: <http://example.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?title ?lang ?dir
WHERE {
?doc ex:title ?title .
FILTER (hasLANGDIR(?title))
BIND (LANG(?title) AS ?lang)
BIND (LANGDIR(?title) AS ?dir) # -> "ltr" or "rtl"
}PREFIX ex: <http://example.org/>
# Mint a directional language-tagged string.
SELECT (STRLANGDIR("مرحبا", "ar", "rtl") AS ?greeting) WHERE {}
# Mint a directional string without a language tag.
SELECT (STRDIR("Hello", "ltr") AS ?greeting) WHERE {}PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
# sameTerm distinguishes by lexical form + datatype; sameValue compares values.
SELECT ?same1 ?same2
WHERE {
BIND (sameTerm("1"^^xsd:integer, "1.0"^^xsd:decimal) AS ?same1) # false
BIND (sameValue("1"^^xsd:integer, "1.0"^^xsd:decimal) AS ?same2) # true
}PREFIX ex: <http://example.org/>
SELECT ?doc
WHERE {
?doc ex:body ?body .
FILTER (contains_token(?body, "quantum"))
}PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT (format_number(12345.678, "###,##0.00") AS ?pretty) WHERE {}PREFIX ex: <http://example.org/>
# Expand an array-valued expression into rows.
SELECT ?tag
WHERE {
?doc ex:tags ?tags .
UNNEST(?tags) AS ?tag
}@VERSION "1.2" is accepted in the Turtle / SPARQL prolog. It must appear
before BASE. Long (triple-quoted) string literals are rejected after
VERSION.
@version "1.2" .
@base <http://example.org/> .
@prefix ex: <http://example.org/> .
ex:doc1 ex:title "Hello"@en--ltr .VERSION "1.2"
BASE <http://example.org/>
PREFIX ex: <http://example.org/>
PREFIX prov: <http://www.w3.org/ns/prov#>
SELECT ?s WHERE { ?s prov:wasDerivedFrom <<( ex:bob ex:knows ex:carol )>> }| Datatype | IRI |
|---|---|
rdf:HTML |
http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML |
rdf:JSON |
http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON |
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:page ex:body "<p>Hello <em>world</em></p>"^^rdf:HTML .
ex:page ex:payload "{\"a\":1,\"b\":[2,3]}"^^rdf:JSON .A Triple-typed property element takes the form:
<?xml version="1.0"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ex="http://example.org/"
xmlns:prov="http://www.w3.org/ns/prov#">
<rdf:Description rdf:about="http://example.org/alice">
<prov:wasDerivedFrom rdf:parseType="Triple">
<rdf:subject rdf:resource="http://example.org/bob"/>
<rdf:predicate rdf:resource="http://example.org/knows"/>
<rdf:object rdf:resource="http://example.org/carol"/>
</prov:wasDerivedFrom>
</rdf:Description>
</rdf:RDF>Attach a reifier to a property element:
<?xml version="1.0"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ex="http://example.org/"
xmlns:dct="http://purl.org/dc/terms/">
<rdf:Description rdf:about="http://example.org/alice">
<!-- Named reifier IRI -->
<ex:knows rdf:annotation="http://example.org/claim1"
rdf:resource="http://example.org/bob"/>
<!-- Or a blank-node reifier -->
<ex:knows rdf:annotationNodeID="r2"
rdf:resource="http://example.org/carol"/>
</rdf:Description>
<rdf:Description rdf:about="http://example.org/claim1">
<dct:created>2026-05-22</dct:created>
</rdf:Description>
</rdf:RDF>The N-Triples / N-Quads parsers support three new features:
- Triple terms in object position, recursively.
-
Direction tags on language-tagged literals (
--ltr/--rtl). -
@versiondirective recognition.
@version "1.2" .
<http://example.org/alice> <http://www.w3.org/ns/prov#wasDerivedFrom> <<( <http://example.org/bob> <http://example.org/knows> <http://example.org/carol> )>> .
<http://example.org/doc1> <http://example.org/title> "مرحبا بالعالم"@ar--rtl .
<http://example.org/doc1> <http://example.org/title> "Hello"@en--ltr .
<http://example.org/alice> <http://www.w3.org/ns/prov#wasDerivedFrom> <<( <http://example.org/bob> <http://example.org/knows> <http://example.org/carol> )>> <http://example.org/provenanceGraph> .
-
BCP-47-conformant language tag: subtag length is now capped at 1–8
characters. Tags like
"…"@aand"…"@ab-cd-efare accepted; tags exceeding 8 characters per subtag are rejected. -
Stricter IRI validation: the body of
<…>rejects embedded<. An IRI like<http://bad/<oops>now errors at parse time instead of being silently accepted.
@prefix ex: <http://example.org/> .
# OK: lang subtag length within 1-8.
ex:a ex:label "hi"@en .
ex:b ex:label "salut"@fr-CA .
# Rejected by the lexer:
# ex:c ex:label "x"@thissubtagistoolong .
# ex:d ex:p <http://bad/<oops> .Controls whether internal SPARQL 1.2 triple-term helper variables are exposed
in SELECT * result sets. Default: hidden ("no"). The value must be exactly
"yes" or "no"; any other value raises a parse error.
# Make every implicit triple-term helper var visible in SELECT *.
DEFINE sparql12:show-triple-terms "yes"
PREFIX ex: <http://example.org/>
SELECT * WHERE {
?s ex:knows ?o {| ?p ?meta |} .
}Triple-term components are serialized in XML, JSON, TSV, and CSV result formats. Virtuoso provides canonicalization for triple-term IRIs and generated blank-node reifier IRIs to produce deterministic output.
-- Step 1: Bootstrap RDF 1.2 vocabulary (rdf:reifies, rdfs:Proposition)
RDF_12_VOCAB_INIT ();
-- Step 2: Load Turtle 1.2 content with reifiers and annotations
ttlp ('
@prefix ex: <http://example.org/> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice ex:knows ex:bob {| dct:created "2026-05-22"^^xsd:date |} .
', '', 'http://example.org/g1');
-- Step 3: SPARQL query that pulls the annotation back out
SPARQL
PREFIX ex: <http://example.org/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?r ?createdOn
FROM <http://example.org/g1>
WHERE {
?r rdf:reifies <<( ex:alice ex:knows ex:bob )>> ;
dct:created ?createdOn .
};- Blank node forbidden as a triple-term predicate.
- Triple terms cannot be used as a triple subject in source documents — they appear in object position only.
-
BINDpermits only bare triple terms<<( s p o )>>; reified or asserted forms (<< s p o >>,<<{ s p o }>>) are rejected. -
VALUESrejects literal or nested-triple-term subjects, and any blank node inside triple terms. -
Duplicate variable in
VALUESheader is now rejected. - A bare triple term
<<( s p o )>>cannot stand as a top-level standalone statement — it must be the object of an outer triple.
# Rejected: triple term as subject
PREFIX ex: <http://example.org/>
SELECT * WHERE { <<( ex:a ex:b ex:c )>> ex:p ?o . }
# Rejected: reified form inside BIND
PREFIX ex: <http://example.org/>
SELECT ?x WHERE { BIND (<< ex:a ex:b ex:c >> AS ?x) }
# Rejected: blank node inside a triple term inside VALUES
PREFIX ex: <http://example.org/>
SELECT * WHERE {
VALUES ?tt { <<( _:b ex:p ex:o )>> }
?s ?p ?tt .
}
# Rejected: duplicate variable in VALUES header
PREFIX ex: <http://example.org/>
SELECT * WHERE { VALUES (?a ?a) { (ex:x ex:y) } }- RDF 1.2 Concepts: https://www.w3.org/TR/rdf12-concepts/
- SPARQL 1.2 https://www.w3.org/TR/sparql12-query/