store.query(...).SparqlGraphDialect (src/graph/sparql) implements the samecompile* methods, the same findAll-shaped specgraph() accessor that returns a dialect bound to it:import { Op } from 'ts-prorm-orm';
import { FusekiStore } from 'ts-prorm-orm/nosql/fuseki';
const store = new FusekiStore({ endpoint: 'http://localhost:3030', dataset: 'kb' });
await store.connect();
const graph = store.graph();
await graph.createNode('User', { id: 42, name: 'Ada', age: 36 });
const users = await graph.findNodes({
label: 'User',
attributes: ['id', 'name', 'age'],
where: { age: { [Op.gte]: 30 } },
order: [['name', 'ASC']],
limit: 10,
});
await graph.relate({
fromLabel: 'User', fromWhere: { id: 42 },
edgeLabel: 'HAS_POSTS',
toLabel: 'Post', toWhere: { id: 7 },
});
const reachable = await graph.traverse({
label: 'User', where: { id: 42 },
edgeLabel: 'KNOWS', direction: 'both', maxDepth: 3,
});query / update / ask / add / drop methods on each store aregraph() calls from the top of this page and the SPARQL each one compiles to, with the default http://prorm.dev/ns/ bases. ?_p0-style placeholders are filled by bindSparql() just before the request is sent (see Injection).import { Op } from 'ts-prorm-orm';
import { FusekiStore } from 'ts-prorm-orm/nosql/fuseki';
const store = new FusekiStore({ endpoint: 'http://localhost:3030', dataset: 'kb' });
await store.connect();
const graph = store.graph();
// 1. flat projection: attributes are known, one row per node
const users = await graph.findNodes({
label: 'User',
attributes: ['id', 'name', 'age'],
where: { age: { [Op.gte]: 30 } },
order: [['name', 'ASC']],
limit: 10,
});
// 2. everyone within two KNOWS hops, either direction
const circle = await graph.traverse({ label: 'User', where: { id: 42 }, edgeLabel: 'KNOWS', direction: 'both', maxDepth: 2 });
// 3. update one property
await graph.updateNodes('User', { id: 42 }, { age: 37 });
// 4. an edge between two matched nodes
await graph.relate({ fromLabel: 'User', fromWhere: { id: 42 }, edgeLabel: 'HAS_POSTS', toLabel: 'Post', toWhere: { id: 7 } });
// 5. delete a node and every triple pointing at it
await graph.destroyNodes('User', { id: 42 });# 1. ?_p0 = 30
SELECT ?s ?v_id ?v_name ?v_age
WHERE {
?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
OPTIONAL { ?s <http://prorm.dev/ns/prop/age> ?v_age . }
OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
OPTIONAL { ?s <http://prorm.dev/ns/prop/name> ?v_name . }
FILTER(?v_age >= ?_p0)
}
ORDER BY ASC(?v_name)
LIMIT 10
# 2. ?_p0 = 42
SELECT DISTINCT ?t
WHERE {
?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
FILTER(?v_id = ?_p0)
{ ?s (<http://prorm.dev/ns/edge/KNOWS>|^<http://prorm.dev/ns/edge/KNOWS>) ?t . }
UNION { ?s (<http://prorm.dev/ns/edge/KNOWS>|^<http://prorm.dev/ns/edge/KNOWS>)/(<http://prorm.dev/ns/edge/KNOWS>|^<http://prorm.dev/ns/edge/KNOWS>) ?t . }
}
# 3. ?_p0 = 42, ?_p1 = 37
DELETE { ?s <http://prorm.dev/ns/prop/age> ?old_age . }
INSERT { ?s <http://prorm.dev/ns/prop/age> ?_p1 . }
WHERE {
?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
FILTER(?v_id = ?_p0)
OPTIONAL { ?s <http://prorm.dev/ns/prop/age> ?old_age . }
}
# 4. ?_p0 = 42, ?_p1 = 7
INSERT { ?a <http://prorm.dev/ns/edge/HAS_POSTS> ?b . }
WHERE {
?a <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
OPTIONAL { ?a <http://prorm.dev/ns/prop/id> ?v_id . }
FILTER(?v_id = ?_p0)
?b <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/Post> .
OPTIONAL { ?b <http://prorm.dev/ns/prop/id> ?v_id_1 . }
FILTER(?v_id_1 = ?_p1)
}
# 5. two operations in one request: incoming triples, then the node's own
DELETE { ?src ?inp ?s . }
WHERE {
?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
FILTER(?v_id = ?_p0)
?src ?inp ?s .
} ;
DELETE { ?s ?p ?o . }
WHERE {
?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
FILTER(?v_id = ?_p1)
?s ?p ?o .
}| ORM concept | RDF | Default IRI |
|---|---|---|
node label User | an rdfs:Class-style class IRI | <http://prorm.dev/ns/class/User> |
| the label itself | <subject> rdf:type <class IRI> | — |
node with id 42 | the subject IRI | <http://prorm.dev/ns/data/User/42> |
property name | a predicate IRI | <http://prorm.dev/ns/prop/name> |
property value 'Ada' | an object literal | "Ada" |
edge label HAS_POSTS | a predicate IRI | <http://prorm.dev/ns/edge/HAS_POSTS> |
| an edge | <…/data/User/42> <…/edge/HAS_POSTS> <…/data/Post/7> | — |
User.create({ id: 42, name: 'Ada' }) becomes:INSERT DATA {
<http://prorm.dev/ns/data/User/42> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
<http://prorm.dev/ns/data/User/42> <http://prorm.dev/ns/prop/id> "42"^^<http://www.w3.org/2001/XMLSchema#integer> .
<http://prorm.dev/ns/data/User/42> <http://prorm.dev/ns/prop/name> "Ada" .
}const graph = store.graph({
base: 'https://example.org/kb', // moves all four bases at once
// …or set them individually:
classBase: 'https://example.org/kb/class/',
propertyBase: 'http://xmlns.com/foaf/0.1/',
edgeBase: 'https://example.org/kb/rel/',
dataBase: 'https://example.org/kb/id/',
classIris: { User: 'http://xmlns.com/foaf/0.1/Person' },
propertyIris: { name: 'http://xmlns.com/foaf/0.1/name' },
edgeIris: { HAS_POSTS: 'http://schema.org/author' },
idProperty: 'uid', // which attribute is the node's identity
graph: 'urn:graph:main', // scope every pattern and write to a named graph
defaultLanguage: 'en', // tag bare string literals
});store.graph() picks them up:new FusekiStore({ dataset: 'kb', graphMapping: { base: 'https://example.org/kb' } });<…>. A prefixed name's local part has to satisfy the PN_LOCALfirst name,2fa_enabled, price$); emitting one anyway is either a parse error at theUPDATE, DELETE or link to one. Since a node's IRI is a pure function of itsid, the subject is known before a request runs, which is exactlysafeIdent-ed. The Cypher and GremlinsafeIdent, which maps everything outside[A-Za-z0-9_] to _. That is not injective, so labels Book/Chapter andBook_Chapter would collapse onto one class IRI and silently share storage.safeIdent is still used in this dialect — forimport { iri } from 'ts-prorm-orm/graph/sparql';
await graph.createNode('User', { id: 1, homepage: iri('https://ada.example') });"https://…" were auto-detected as an IRI, a homepage attribute holding anull or undefined deletes itswhere: { nickname: null } compiles!BOUND(?v_nickname).name triples on onefindNodes folds repeated values into an array. Use distinct: trueattributes to keep this predictable.| JavaScript | RDF term |
|---|---|
'Ada' | "Ada" (or "Ada"@en with defaultLanguage) |
36 | "36"^^xsd:integer |
1.5 | "1.5"^^xsd:decimal |
Infinity / NaN | "INF"^^xsd:double / "NaN"^^xsd:double |
true | "true"^^xsd:boolean |
new Date(...) | "…"^^xsd:dateTime |
iri('http://…') | <http://…> |
lang('bonjour', 'fr') | "bonjour"@fr |
typed('P1D', xsdDuration) | "P1D"^^<…#duration> |
"10" literal sortsage > 9 would exclude 10.$p0 the server substitutes for you. (RDF4J and Jena exposesetBinding in their Java APIs, not over HTTP.) So the work is split in two,compile* method{ text, params } where text contains only ?_p0-styleexpect(stmt.text).not.toContain('Ada').bindSparql(text, params) insrc/graph/sparql/terms.ts is the only place a value is serialized, throughserializeTerm, which can only emit a well-formed RDF term. The dialect'squery() calls it immediately before dispatch; nothing else concatenates."…" with \,", LF, CR, TAB, BS and FF replaced by their SPARQL escapes (backslash first,", so it cannot terminate its own quote — which<…> has no escape<, >, ", {, }, |, ^, `,\, a space or an ASCII control character throws a ValidationError. That isIRIREF excludes in the SPARQL grammar. An IRI with no schemeLANGTAG production.IN binds one placeholder per element. SPARQL's IN takes an expression?_p3 is a syntactically validrequireGraphLabel() first. A missing label throws instead of producing<…/class/undefined>, which is a perfectly good IRI that nothing is storedcompile* method | Compiles to |
|---|---|
compileMatch | SELECT with OPTIONAL property bindings + FILTER, ORDER BY, LIMIT/OFFSET, COUNT, DISTINCT, includes as OPTIONAL edge hops |
compileCreateNode | INSERT DATA |
compileUpdateNodes | DELETE … INSERT … WHERE |
compileDeleteNodes | two DELETE … WHERE operations in one request |
compileUpsertNode | DELETE … WHERE then INSERT DATA, keyed on the identity property |
compileIncrement | DELETE … INSERT … WHERE with COALESCE(?old, 0) |
compileCreateEdge | INSERT … WHERE between two matched endpoint sets |
compileTraverse | property paths — a UNION of fixed-length paths, or +/* |
compileShowLabels | SELECT DISTINCT ?label over the class base |
compileCreateConstraint | ✗ GraphCapabilityError |
compileDropConstraint | ✗ GraphCapabilityError |
compileCreateIndex | ✗ GraphCapabilityError |
compileDropIndex | ✗ GraphCapabilityError |
CREATE CONSTRAINT that silently does not constrain is worsecreateTable() still works — it registers the node model and its schema hints —RDF_OBJ_FT_RULE_ADD.startTransaction() throws. The atomic unit you actually get is one update;-separated operations — which is howsince: 2019. RDF-star quoted triples solve it, but they are not part ofrdfStar: true). Stardog, Fuseki and GraphDB default to on.maxPathExpansion) — see below.^, /, |, *, +, ? and ! — but noelt{n,m} was in the working draft and was dropped from<e>{1,3} is a parse error on a conformingUNION of fixed-lengthgraph.compileTraverse({ label: 'User', edgeLabel: 'KNOWS', maxDepth: 3 }){ ?s <…/edge/KNOWS> ?t . }
UNION { ?s <…/edge/KNOWS>/<…/edge/KNOWS> ?t . }
UNION { ?s <…/edge/KNOWS>/<…/edge/KNOWS>/<…/edge/KNOWS> ?t . }maxDepth: Infinity uses + instead (or * when minDepth is 0, or a fixed+ when minDepth > 1). direction: 'in' inverts the path^; 'both' alternates with (<e>|^<e>).compileMatch picksattributes, or from a nodedefineNode / createTable), each attribute gets itsORDER BY/LIMIT mean whatSELECT ?s ?p ?o over a subquery that has alreadyLIMIT 10 would limit triples rather than nodes and returnORDER BY repeated outside, because a subquery's ordering is notfindNodes() folds either shape back into node objects, each carrying its$iri.OPTIONALwhere field compiles to OPTIONAL { ?s <prop> ?v_field } plus aFILTER, never to a plain triple pattern. A plain pattern is a join, and that{ nickname: { [Op.ne]: 'x' } } drops every node with no nickname at all;Op.isNull can never match, because the pattern binding the variable wouldOp.or annihilates the whole solution instead of being false.OPTIONAL + FILTER reproduces the ORM's three-valued semantics.SparqlExecutor interface (query / update / ask plusgraph() works without the dialect owning any HTTP| Store | Query path | Update path | rdfStar default | Notes |
|---|---|---|---|---|
| Stardog | /<db>/query | /<db>/update | on | ICV is database-level validation, not SPARQL DDL |
| Fuseki | /<ds>/query | /<ds>/update | on | Jena implements RDF-star |
| GraphDB | /repositories/<r> | /repositories/<r>/statements | on | full-text/vector indexes are connectors, not CREATE INDEX |
| Blazegraph | /blazegraph/namespace/<ns>/sparql | same | off | its <<…>> "RDR" predates and differs from RDF-star |
| Virtuoso | /sparql | same | off | commonly anonymous read-only — see below |
| AllegroGraph | /repositories/<r> | same | off | per-triple metadata is triple attributes, not RDF-star |
PREFIX/BASE headers — without stripping # globally, since # is a/sparql is very often exposed with anonymous read-onlySPARQL Request Failed / 37000 permission fault:const store = new VirtuosoStore({
endpoint: 'https://dbpedia.org',
graphMapping: { readOnly: true },
});
await store.graph().createNode('User', { id: 1 });
// GraphCapabilityError: "SPARQL 1.1 Update (endpoint configured read-only)" is
// not supported by the virtuoso graph dialectawait store.query(`
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name (COUNT(?friend) AS ?n) WHERE {
?p foaf:name ?name ; foaf:knows ?friend .
} GROUP BY ?name HAVING (COUNT(?friend) > 5)
`);findNodes / query flatten typed literals into JavaScript values.