Term types — typed literals, language tags, blank nodes, RDF lists
Ingest keeps every kind of RDF term: IRIs, plain literals, typed literals, language-tagged literals, blank nodes and RDF collections.
What it does
| Term | In Turtle | How SPARQL sees it |
|---|---|---|
| IRI | <http://example.org/alice> | isIRI(?x) is true |
| Plain literal | "hello" | datatype xsd:string (RDF 1.1) |
| Typed literal | "42"^^xsd:integer | DATATYPE(?x) is xsd:integer; compares as a number |
| Language-tagged literal | "colour"@en-GB | LANG(?x) is "en-GB" |
| Blank node | _:b1 or [ … ] | isBlank(?x) is true; shows as a generated label |
| RDF collection | ( a b c ) | expands to an rdf:first / rdf:rest / rdf:nil chain |
Literals are kept distinct on their full identity: value, datatype and language together. "Berlin"@en, "Berlin"@de and "Berlin" are three different terms, as are "1"^^xsd:integer and "1".
Two presentation details:
pgrdf.sparql()returns every value as a JSON string, numbers included ({"a": "30"}). UseDATATYPE()/LANG()in the query, orconstruct(), which returns each term with its type, datatype and language.- RDF-star quoted triples are not supported and refuse at load time.
Why you'd use it
- Data scientists — numeric and language filters work on real data because types survive ingest instead of being flattened to strings.
- Ontologists — OWL, SHACL, PROV-O and similar vocabularies load with their datatype assertions intact.
- Project managers — what is stored matches the W3C RDF model, so there are no lossy surprises downstream.
Example
sql
SELECT pgrdf.add_graph('http://example.org/terms');
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:alice ex:name "Alice"@en ;
ex:age "30"^^xsd:integer ;
ex:notes [ ex:source "manual" ] ;
ex:tags ( "engineer" "rust" "rdf" ) .
', pgrdf.graph_id('http://example.org/terms'));
-- → 11 (4 statements about alice, 1 for the blank node, 6 for the 3-item list)A numeric filter over the typed literal:
sql
SELECT * FROM pgrdf.sparql('
PREFIX ex: <http://example.org/>
SELECT ?s ?a WHERE { ?s ex:age ?a FILTER(?a >= 18) }');
-- {"a": "30", "s": "http://example.org/alice"}Language and datatype:
sql
SELECT * FROM pgrdf.sparql('
PREFIX ex: <http://example.org/>
SELECT ?n (LANG(?n) AS ?l) (DATATYPE(?a) AS ?dt)
WHERE { ?s ex:name ?n ; ex:age ?a FILTER(LANG(?n) = "en") }');
-- {"l": "en", "n": "Alice", "dt": "http://www.w3.org/2001/XMLSchema#integer"}Following a blank node, using a variable for it:
sql
SELECT * FROM pgrdf.sparql('
PREFIX ex: <http://example.org/>
SELECT ?source WHERE { ex:alice ex:notes ?note . ?note ex:source ?source }');
-- {"source": "manual"}The items of an RDF list:
sql
SELECT * FROM pgrdf.sparql('
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?item WHERE { GRAPH <http://example.org/terms> { ?node rdf:first ?item } }');
-- {"item": "engineer"}
-- {"item": "rust"}
-- {"item": "rdf"}Blank nodes written inside a query pattern ([ ex:source ?s ]) and sequence paths (rdf:rest*/rdf:first) are not supported. Use a variable, as above. See property paths.
Distinct literals that share a value:
sql
SELECT pgrdf.add_graph('http://example.org/berlin');
SELECT pgrdf.parse_turtle('
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:c ex:label "Berlin"@en, "Berlin"@de, "Berlin" .
ex:c ex:n "1"^^xsd:integer, "1" .
', pgrdf.graph_id('http://example.org/berlin'));
-- → 5
SELECT * FROM pgrdf.sparql('
SELECT ?o (LANG(?o) AS ?l) (DATATYPE(?o) AS ?d)
WHERE { GRAPH <http://example.org/berlin> { ?s ?p ?o } }');
-- {"d": null, "l": "en", "o": "Berlin"}
-- {"d": null, "l": "de", "o": "Berlin"}
-- {"d": "http://www.w3.org/2001/XMLSchema#string", "l": "", "o": "Berlin"}
-- {"d": "http://www.w3.org/2001/XMLSchema#integer", "l": "", "o": "1"}
-- {"d": "http://www.w3.org/2001/XMLSchema#string", "l": "", "o": "1"}See also
- Hexastore + dictionary — how terms are stored.
- FILTER expressions.