SPARQL Engine
Implementation status (2026-07-23, kelly): ✅ Implemented (code is AHEAD of this doc). SELECT/ASK/CONSTRUCT/DESCRIBE, BGP/JOIN/UNION/OPTIONAL/FILTER/BIND, DISTINCT/ORDER/LIMIT/GROUP/HAVING, aggregates (COUNT/SUM/AVG/MIN/MAX/SAMPLE/ GROUP_CONCAT), FILTER builtins, RDFS subclass inference, and bitemporal current-state filtering are all live (
src/sparql/). Note the doc UNDER-claims: it marks “Property paths: Planned,” but they are fully implemented insrc/sparql/property_path.rs(Reverse/Sequence/Alternative/ZeroOrMore/OneOrMore) — the table row below is now corrected.
Quipu includes a custom SPARQL evaluator that compiles queries directly against the SQLite fact log. No separate triple store or graph database is needed.
How It Works
- Parse: SPARQL string -> AST via spargebra
- Evaluate: Walk the AST, executing each graph pattern against SQLite
- Return: Variable bindings as
HashMap<String, Value>rows
#![allow(unused)]
fn main() {
use quipu::store::Store;
use quipu::sparql;
let result = sparql::query(&store,
"SELECT ?name WHERE { ?s <http://example.org/name> ?name }"
).unwrap();
for row in result.rows() {
println!("{:?}", row.get("name"));
}
}
Query Forms
| Form | Description | Example |
|---|---|---|
| SELECT | Return variable bindings | SELECT ?s ?p ?o WHERE { ... } |
| ASK | Boolean existence check | ASK { ?s a ex:Person } |
| CONSTRUCT | Build new triples | CONSTRUCT { ?s a ex:Result } WHERE { ... } |
| DESCRIBE | Return all facts about an entity | DESCRIBE <http://example.org/alice> |
Supported Features
Graph Patterns
| Pattern | Status | Example |
|---|---|---|
| Basic Graph Pattern (BGP) | Supported | ?s ?p ?o |
| JOIN | Supported | Multiple BGP patterns |
| UNION | Supported | { ... } UNION { ... } |
| FILTER | Supported | FILTER(?age > 30) |
| OPTIONAL (LeftJoin) | Supported | OPTIONAL { ?s ex:email ?e } |
| PROJECT | Supported | SELECT ?name |
| DISTINCT / REDUCED | Supported | SELECT DISTINCT ?type |
| LIMIT / OFFSET | Supported | LIMIT 10 OFFSET 5 |
| ORDER BY | Supported | ORDER BY DESC(?age) |
| GROUP BY | Supported | GROUP BY ?type |
| HAVING | Supported | HAVING(COUNT(?s) > 2) |
| EXTEND (BIND) | Supported | Computed variables |
| VALUES | Supported | VALUES ?x { "a" "b" }, multi-column, UNDEF (src/sparql/values.rs) |
| Property paths | Supported | Reverse ^, Sequence /, Alternative |, *, + (src/sparql/property_path.rs) |
Aggregates
| Function | Example |
|---|---|
| COUNT | SELECT (COUNT(?s) AS ?n) WHERE { ... } |
| SUM | SELECT (SUM(?age) AS ?total) ... |
| AVG | SELECT (AVG(?age) AS ?mean) ... |
| MIN / MAX | SELECT (MIN(?age) AS ?youngest) ... |
An ungrouped, non-distinct COUNT(*) or COUNT of a bound variable over
one triple pattern can use a scalar SQL count. This avoids retaining a binding
for every matching triple. Composed stores canonicalize subject, predicate and
reference-object aliases before counting, so duplicate assertions across
attached dictionaries still count once. The query’s graph and temporal scope
remain in force.
During the scalar statement, file mappings are disabled and an oversized main page-cache budget is reduced to 8 MiB. Each provider’s mapping policy and the main cache policy are restored afterward, including on errors. Smaller cache budgets are preserved. Query deadlines still apply; unsupported aggregate forms and joins retain their existing complexity limits.
FILTER Expressions
| Expression | Example |
|---|---|
| Equality | ?name = "Alice" |
| Comparison | ?age > 30, ?age <= 50 |
| AND / OR / NOT | ?age > 20 && ?age < 40 |
| BOUND | BOUND(?name) |
| Regex | regex(?name, "Ali") |
| CONTAINS | CONTAINS(STR(?s), "traefik") |
| LCASE / STR | LCASE(STR(?name)) |
| isIRI | FILTER(isIRI(?o)) |
| IN / NOT IN | FILTER(?name IN ("Alice", "Bob")) |
RDFS Inference
Quipu supports RDFS subclass inference for rdf:type queries. If you define:
ex:Engineer rdfs:subClassOf ex:Person .
ex:alice a ex:Engineer .
Then SELECT ?s WHERE { ?s a ex:Person } will return ex:alice through
transitive subclass reasoning.
Temporal Awareness
The SPARQL engine automatically filters to current state
(op = 1 AND valid_to IS NULL). Time-travel is supported via the
unravel command:
# See the world as it was at a specific transaction
quipu unravel --tx 5 --db my.db
# See the world as it was at a specific time
quipu unravel --valid-at "2026-03-15T00:00:00Z" --db my.db
Via the MCP tool:
{
"tool": "quipu_query",
"input": {
"query": "SELECT ?s ?p ?o WHERE { ?s ?p ?o }",
"valid_at": "2026-03-15T00:00:00Z"
}
}