OpenLink Virtuoso: RDF 1.2 and SPARQL 1.2 Implementation Notes - pkleef/virtuoso-opensource GitHub Wiki

OpenLink Virtuoso: RDF 1.2 (RDF-star) and SPARQL 1.2 Implementation Notes

Copyright 2026 OpenLink Software [email protected]

Implementation Notes

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.


Table of Contents


Triple Terms (core RDF 1.2 / RDF-star feature)

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

Inserting a triple with a bare triple-term object

@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 )>> .

Inserting with an asserted triple-term object

@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 }>> .

Querying triple terms with SPARQL

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 )>> .
}

Reified Triples

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).

Shorthand reified triple

@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 .

Explicit reifier (IRI)

@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" .

Explicit reifier (blank node)

@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" .

Anonymous reifier

@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 rdf:reifies vocabulary

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." .

Querying a reifier

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 Syntax (Turtle / SPARQL)

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.

Annotation block — {| ... |}

@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 .

Reifier suffix on the object — ~iri, ~_:b, ~

@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 ~ .

Combining a reifier suffix and an annotation block

@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
|} .

Annotations in SPARQL queries

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 |} .
}

Directional Language-Tagged Strings (rdf:dirLangString)

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>.

Insert with Turtle

@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 .

Query with SPARQL

PREFIX ex:  <http://example.org/>

# Note SPARQL uses `~ltr` / `~rtl`, NOT `--ltr` / `--rtl`.
SELECT ?doc
WHERE {
  ?doc ex:title "مرحبا بالعالم"@ar~rtl .
}

Inspecting direction with built-in functions

See New SPARQL 1.2 Built-in Functions for DIR(), LANGDIR(), hasLANGDIR(), STRLANGDIR() and STRDIR().


New SPARQL 1.2 Built-in Functions

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.

isTRIPLE / TRIPLE / SUBJECT / PREDICATE / OBJECT

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 {}

hasLANG / hasLANGDIR / LANGDIR / DIR

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"
}

STRLANGDIR / STRDIR

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 {}

sameValue vs. sameTerm

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
}

contains_token / format_number

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 {}

UNNEST

PREFIX ex: <http://example.org/>

# Expand an array-valued expression into rows.
SELECT ?tag
WHERE {
  ?doc ex:tags ?tags .
  UNNEST(?tags) AS ?tag
}

SPARQL 1.2 Prolog: VERSION Directive

@VERSION "1.2" is accepted in the Turtle / SPARQL prolog. It must appear before BASE. Long (triple-quoted) string literals are rejected after VERSION.

Turtle

@version "1.2" .
@base    <http://example.org/> .
@prefix  ex:  <http://example.org/> .

ex:doc1 ex:title "Hello"@en--ltr .

SPARQL

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 )>> }

New RDF Datatypes Recognized

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

Example

@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 .

RDF/XML Parser Extensions

rdf:parseType="Triple"

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>

rdf:annotation / rdf:annotationNodeID

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>

N-Quads / N-Triples 1.2 Extensions

The N-Triples / N-Quads parsers support three new features:

  1. Triple terms in object position, recursively.
  2. Direction tags on language-tagged literals (--ltr / --rtl).
  3. @version directive recognition.

N-Triples 1.2 — triple-term object

@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> )>> .

N-Triples 1.2 — directional language tag

<http://example.org/doc1> <http://example.org/title> "مرحبا بالعالم"@ar--rtl .
<http://example.org/doc1> <http://example.org/title> "Hello"@en--ltr .

N-Quads 1.2 — graph + triple term

<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> .

Turtle / SPARQL Lexer Refinements

  • BCP-47-conformant language tag: subtag length is now capped at 1–8 characters. Tags like "…"@a and "…"@ab-cd-ef are 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> .

Configuration

sparql12:show-triple-terms

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 |} .
}

Result Serialization

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.

Loading and querying — end-to-end

-- 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 .
};

Notable Validation Rules Enforced

  • 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.
  • BIND permits only bare triple terms <<( s p o )>>; reified or asserted forms (<< s p o >>, <<{ s p o }>>) are rejected.
  • VALUES rejects literal or nested-triple-term subjects, and any blank node inside triple terms.
  • Duplicate variable in VALUES header 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.

Examples of rejected input

# 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) } }

References

⚠️ **GitHub.com Fallback** ⚠️