The previous post parsed a graph and read it by hand, triple by triple. That's fine for five triples; it stops working the moment a graph has five thousand. SPARQL is the query language that scales past "read every line" — this post runs it live, against the same F*-extracted evaluator the W3C SPARQL 1.1 test suite scores 631 pass, 0 fail (of 631) against.
The examples below use real Wikidata entity and property IRIs — wd:
for entities, wdt: for "direct" (truthy) property statements — so
the query syntax matches what you'd actually run against
query.wikidata.org. The dataset itself is a small, hand-authored,
easily-verified excerpt (not a live fetch — no network dependency in a
docs page or its pinned test):
@prefix wd: <http://www.wikidata.org/entity/> .
@prefix wdt: <http://www.wikidata.org/prop/direct/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
wd:Q42 rdfs:label "Douglas Adams" ;
wdt:P31 wd:Q5 ; # instance of: human
wdt:P106 wd:Q36180 . # occupation: writer
wd:Q5 rdfs:label "human" .
wd:Q36180 rdfs:label "writer" .
Five triples: one entity (Douglas Adams, wd:Q42) with a label, an
instance-of edge, an occupation edge, and labels for the two things it
points at. Every cell below parses that same Turtle text and queries
it live, using the fn typed API (see README.md for
the cell-authoring contract).
Both the dataset and its parse are named once, below, and every query
cell in this post references ttl/dataset by name instead of
repeating them — the same declare-once, use-everywhere pattern
post 26 covers
in full:
ttl = `
@prefix wd: <http://www.wikidata.org/entity/> .
@prefix wdt: <http://www.wikidata.org/prop/direct/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
wd:Q42 rdfs:label "Douglas Adams" ;
wdt:P31 wd:Q5 ;
wdt:P106 wd:Q36180 .
wd:Q5 rdfs:label "human" .
wd:Q36180 rdfs:label "writer" .
`
dataset = fn.parse(ttl)
const rows = await fn.query(dataset, `
# Every value Douglas Adams (Q42) points at, with an rdfs:label
# where one exists.
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?label WHERE {
<http://www.wikidata.org/entity/Q42> ?p ?o .
# ?o may or may not have a label; OPTIONAL keeps the row either way,
# leaving ?label unbound when there is none.
OPTIONAL { ?o rdfs:label ?label }
}
`);
return pretty(rows); // one row per outgoing edge from Q42: label, instance-of, occupation
query() returns an array of Map<string, Term> — one map per
solution row, keyed by variable name; pretty() (see
README.md) renders that shape as a table, one column
per variable, with a "N rows" caption.
A yes/no question — does this fact exist:
return await fn.query(dataset, `
# Is Douglas Adams' occupation (P106) writer (Q36180)?
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
ASK { <http://www.wikidata.org/entity/Q42> wdt:P106 <http://www.wikidata.org/entity/Q36180> }
`);
wdt:P31|wdt:P106 is a property path: "either instance-of or
occupation." Alternation (|), sequence (/), and transitive closure
(+, *) all compose the same way the property-path suite (33 pass,
0 fail of 33) tests them:
const types = await fn.query(dataset, `
# Everything Douglas Adams is either instance-of (P31) or has as
# occupation (P106); the | alternation tries both properties.
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
SELECT ?type WHERE { wd:Q42 (wdt:P31|wdt:P106) ?type }
`);
return types.map((r) => r.get("type").value);
Both the instance-of target (Q5, human) and the occupation target
(Q36180, writer) come back from one path expression. On live
Wikidata, the same pattern extended to wdt:P31/wdt:P279* (instance
of, then subclass-of zero-or-more times) is the standard way to ask
"is this a member of some broad class" — walking the transitive
closure of P279 in one query instead of writing a recursive client.
This dataset only has the direct P31 edge, so the * part matches
zero additional hops here; the syntax is identical either way.
The compact music corpus behind the original query gallery is also available here. This two-hop path walks from an album to its band and then to each musician, without exposing the intermediate band variable.
musicTurtle = await fetch("../../../fstar-extracted/samples/music.ttl").then(async (response) => {
if (!response.ok) throw new Error(`music.ttl: HTTP ${response.status}`);
return response.text();
})
musicDataset = fn.parse(musicTurtle)
const performers = await fn.query(musicDataset, `
# Each album's title paired with the title of every musician in the
# band that made it, without naming the band itself.
PREFIX ex: <http://example.org/music/>
PREFIX dc: <http://purl.org/dc/terms/>
PREFIX mo: <http://purl.org/ontology/mo/>
SELECT ?album ?musician WHERE {
# ex:by/ex:member is a two-hop path: album to its band, band to
# each member; ?a's band is never bound to a variable of its own.
?a a mo:Album ; dc:title ?album ; ex:by/ex:member ?m .
?m dc:title ?musician .
}
ORDER BY ?album ?musician
`);
return pretty(performers);
SELECT returns bindings; CONSTRUCT returns a new graph:
const derived = await fn.query(dataset, `
# Build a new triple linking each person straight to their
# occupation's label, skipping the intermediate occupation node.
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
CONSTRUCT { ?person <http://example.org/hasOccupationLabel> ?label }
WHERE { ?person wdt:P106 ?occ . ?occ rdfs:label ?label }
`);
return { size: derived.size, nquads: derived.toNQuads() };
WHERE joins the occupation edge to the occupation's label; CONSTRUCT
builds one new triple per match. derived is an ordinary Dataset,
the same type parse() returns — it can be queried again, unioned
with another graph, or serialized.
RDFS and OWL 2 RL show what
happens when the graph itself, not the query, is the thing doing the
implying — types nobody asserted, derived from a subclass axiom or an
owl:equivalentClass mapping.
Every live cell above is pinned in
tests/hub/post02_test.mjs —
the exact same source, executed against the real npm/factoidal
typed API instead of the in-browser fn adapter.