Reference: SRL language
Start
OverviewConceptsGuides
Run and configureREST API walkthroughCommand lineQuery groupsNotebooksRules and SRLPrefixesTestingMCP clientsPublish as toolsQuery bench in chatStatic exportETLDeployingReference
ConfigurationFeature flagsEntity modelSRL languageScriptsExplanation
ArchitectureVersioningEnvironmentsStorage and cachingSecurity modelRDF PatchInline inputsExamples
Sudoku solverSRL language reference
SRL is the surface syntax sqlib uses for SPARQL RL inference rules.
The parser, well-formedness checker, stratifier and SPARQL compiler are
in packages/srl.
This page documents what the parser accepts and what each stage reports. For writing and running rules, see rules and SRL; for a worked set, see the Sudoku solver.
The parser is built on the Traqula SPARQL 1.2 parser, so the leaves of a rule — terms, triple patterns, expressions — are SPARQL 1.2. Everything in RDF 1.2 that SPARQL 1.2 accepts is accepted here, including triple terms and reifiers.
Document shape
A document is a sequence of prologue declarations, RULE
blocks and DATA blocks, in any order. PREFIX
and BASE may appear between rules, not only at the top.
PREFIX : <http://example.org/>
DATA {
:alice :parent :bob .
:bob :parent :carol .
}
RULE :grandparent {
?x :grandparent ?z
} WHERE {
?x :parent ?y .
?y :parent ?z
}
RULE
RULE <iri>? { head } WHERE DATA? { body }
The name is optional. When present it is an IRI, expanded against the prologue, and it is the rule's identity across edits. A rule without a name is identified by a SHA-256 hash of its canonical text, so editing an unnamed rule mints a new one where editing a named rule revises it.
The head is a set of triple templates. Every variable in the head must be bound by the body.
WHERE DATA makes the whole body match the
ground graph — the base graph plus every DATA
block, as it stood before any rule ran — instead of the evaluation
graph. Such a rule reads only what was given and never what has been
inferred, and so depends on no other rule.
DATA
DATA { triples }
Facts that are inferred automatically at the start of evaluation,
before any rule runs. Variables are not allowed; a variable anywhere in
a DATA block is rejected with:
SRL syntax error: variables are not allowed in a DATA block
A document may contain several DATA blocks. A block has
no name in the language, so its identity is always a content hash: two
documents declaring the same triples reuse the same stored block.
What a rule body admits
A body is a sequence of items. Order matters — SRL evaluates a body
sequentially, unlike SPARQL, where FILTER is scoped to its
whole group. A . between items is optional.
| Construct | Form |
|---|---|
| Triple patterns | ?s :p ?o . ?o :q ?x |
FILTER |
FILTER(?age >= 18), and the rest of SPARQL's filter
syntax except EXISTS / NOT EXISTS |
| Negation | NOT { … } |
| Ground negation | NOT DATA { … } |
| Assignment | SET ( ?v := expr ) |
NOT { … } sees only the variables bound before it. A
variable it shares with the body but that is bound only after
it is free inside the negation, so
RULE { ?x :r ?y } WHERE { NOT { ?x :q ?y } ?x :p ?y }
means "if no :q triple exists anywhere, every
?x :p ?y", not "every ?x :p ?y unless
?x :q ?y". Put the NOT after the patterns that
bind its variables for the per-solution reading. FILTER and
SET have no such trap: using a variable before it is bound
makes the rule ill-formed (use-before-bind below).
NOT { … } nests. NOT DATA { … } sends only
the negated pattern to the ground graph, so a rule can ask "was this
absent from the input?" while the rest of its body still sees inferred
triples. Setting a default value is the usual case.
SET ( ?v := expr ) binds ?v to the value of
a SPARQL expression. It may not re-bind a variable the body already
binds, and its expression may only reference variables already bound at
that point.
What a rule body does not admit
These are SPARQL constructs the body grammar does not include. Each is a parse error naming the offending token.
| Construct | Reported as |
|---|---|
VALUES |
Parse error Expecting --> } <-- but found --> 'VALUES' <-- |
BIND |
Parse error Expecting --> } <-- but found --> 'BIND' <-- |
OPTIONAL |
Parse error Expecting --> } <-- but found --> 'OPTIONAL' <-- |
UNION |
Parse error Expecting --> } <-- but found --> '{' <-- |
Sub-SELECT |
Parse error Expecting --> } <-- but found --> 'SELECT' <-- |
MINUS |
Parse error Expecting --> } <-- but found --> 'MINUS' <-- |
GRAPH |
Parse error Expecting --> } <-- but found --> 'GRAPH' <-- |
BIND has SET in its place, and
FILTER EXISTS / FILTER NOT EXISTS — rejected
with
NOT EXISTS is not part of SRL — write the negation as NOT { … }
— have NOT in theirs; the SRL forms carry the
sequential-evaluation semantics the compiler relies on. The rest have no
SRL equivalent.
An earlier IF … THEN spelling and a
FOR ?v IN :Shape clause are both rejected: neither is in
the language.
One further source-level check runs before parsing. A directional
language tag's base direction must be exactly ltr or
rtl in lower case — the tag itself is case-insensitive, and
the parser normalises the direction, so an invalid uppercase direction
has to be caught in the text:
SRL syntax error: invalid base direction '--LTR' (expected '--ltr' or '--rtl')
Well-formedness
checkWellFormed runs on a document that has already
parsed. It returns a list of issues, each with a category, the rule's
index, its name where it has one, and a message. An empty list means no
issues.
| Category | Condition | Message |
|---|---|---|
unbound-head |
A head variable is bound nowhere in the body | Head variable ?z is not bound by the rule body |
set-rebinds |
SET targets a variable the body already binds, or two
SETs target the same variable |
SET (?y := …) re-binds a variable already bound in the body |
use-before-bind |
A FILTER or SET expression references a
variable not yet bound at that point |
?y is used by a FILTER before it is bound |
Variables introduced inside NOT { … } are existentially
quantified within the negated pattern and are deliberately not required
to be bound outside it. They also do not count as binding a head
variable. A FILTER or SET inside a
NOT is held to the same order rule: it may use the
variables bound before the NOT and those the negated
pattern binds ahead of it, and nothing bound later in the outer
body.
Stratification
stratify takes the rules of a document and reports how
they must be layered. A rule R depends on rule S when a body pattern of
R can match a head template of S. Each dependency carries a label:
| Label | Meaning |
|---|---|
positive |
An ordinary dependency. R may be in the same stratum as S |
negative |
The dependency occurs under a NOT. R must be in a
strictly higher stratum |
closed |
R is a run-once rule, so it must see its dependencies complete. Same
ordering demand as negative |
A document is non-stratifiable exactly when a dependency cycle contains a negative or a closed edge. The report then carries an issue naming the rules, in the order they were given:
Non-stratifiable cycle involving: http://example.org/a, http://example.org/b
It also carries each such cycle as data in cycles: the
rules on it, every dependency between them (with the triple patterns
that made it), whether it fails through negation or through a run-once
rule, and a witness:
{ kind: 'negation', rules: ['a', 'b'], edges: [ … ], witness: [{ from: 'a', to: 'b', label: 'negative', reasons: [ … ] }, … ] }
The witness is the shortest loop through a negative or
closed edge, in path order (each edge's to is
the next edge's from, and the last leads back to the
first). A cycle can hold more rules and dependencies than that; the
witness is the smallest part of it that shows why the rules cannot be
ordered.
The report also gives strata (rule IRI to layer number,
from 0), edges with the triple patterns that justified each
one, monotonicity (monotone or
negation per rule) and runOnce.
strata is empty for a non-stratifiable
document: no layering exists, and the layers the algorithm had reached
when it gave up depend only on its iteration cap and on edge order.
Stratification compares fully expanded IRIs, so
expandIris must run on the document first; two rules
spelling the same term with different prefixes are otherwise never
matched.
Run-once rules
A rule that mints a blank node in its head, or that assigns with
SET, produces a fresh answer on every firing, so iterating
it has no fixpoint. Such a rule is evaluated exactly once per execution,
after everything it reads is complete, and all of its dependencies are
promoted to closed. This makes a cycle through one of these
rules non-stratifiable, which is the correct outcome.
Blank-node detection inspects the blank-node terms in the head's
triple templates, after Turtle's shorthand is expanded. A labelled blank
node (_:b), an anonymous [ … ], a list
( … ) and a reified triple << … >>
without a named reifier all mint a fresh node on every firing, so each
marks the rule run-once.
Stratification matches the triples that shorthand stands for, in RDF
1.2 terms, in heads and bodies alike. [ … ] and
( … ) assert their triples about a blank node.
<< s p o >> does not assert s p o:
it asserts that its reifier rdf:reifies the triple term
<<( s p o )>>. An annotation (~ :r
or {| … |}) asserts the triple it follows, that the reifier
rdf:reifies it, and the annotation's own triples about the
reifier.
How a rule compiles to SPARQL
compileRule turns one rule into a standalone SPARQL
program, with the document's prologue prepended so its prefixes resolve.
The program is parsed before being returned, so a compile that succeeds
has produced valid SPARQL.
An ordinary rule becomes a SPARQL UPDATE:
PREFIX : <http://example.org/>
INSERT {
?x :grandparent ?z
} WHERE {
?x :parent ?y . ?y :parent ?z .
}
Passing { flavour: 'construct' } emits
CONSTRUCT { … } WHERE { … } from the same head and body:
one pass of the rule, returning the triples instead of writing them.
Nothing executes that form; it is there so a reader can paste one pass
into any endpoint and see what it would add. Neither form is stored —
both are computed on demand from the document.
Body items translate as follows:
| SRL | SPARQL |
|---|---|
| Triple patterns | The same triple patterns |
FILTER(E) |
FILTER(E) |
NOT { P } |
FILTER NOT EXISTS { P } |
NOT DATA { P } |
FILTER NOT EXISTS { GRAPH <urn:sqlib:srl:ground> { P } } |
WHERE DATA { B } |
The whole body wrapped in
GRAPH <urn:sqlib:srl:ground> { B } |
SET ( ?v := E ) |
BIND(E AS ?v) FILTER(BOUND(?v)), with everything up to
and including the assignment wrapped in a group |
A NOT that mentions a variable bound only after it also
closes a group: see The order of
NOT below.
urn:sqlib:srl:ground is the named graph the executor
keeps the ground data in — the base graph plus every DATA
block, as it stood before any rule ran. The compiler and the executor
agree on that IRI; it is exported as GROUND_GRAPH_IRI.
The SET translation is the specification's own: a SPARQL
expression either returns a term or raises a type error, never
"unbound", so ?v unbound after BIND is
precisely the error signal, and FILTER(BOUND(?v)) drops
that solution. The group around it is load-bearing. A bare
BIND(1/0 AS ?x) FILTER(BOUND(?x)) :s ?p ?x
lets the later triple pattern bind ?x, at which point
the filter passes and a solution the SET should have killed
comes back. Wrapping gives
{ BIND(1/0 AS ?x) FILTER(BOUND(?x)) } :s ?p ?x
which keeps the test scoped to the point it was written at while still letting the expression see the variables bound before it.
If SRL ever gains an OPTIONAL-like construct, or an
expression that can legitimately yield unbound, the BOUND
test needs revisiting.
The order of NOT
NOT needs the same treatment for the same reason. SRL
checks a negation against the bindings made before it;
FILTER NOT EXISTS sees its whole group wherever it is
written. The two agree whenever every variable the NOT
shares with the body is bound before it, and then the NOT
compiles to a bare FILTER NOT EXISTS in place. When a later
element binds one of its variables, everything up to and including the
negation is wrapped in a group:
# RULE { ?x :r ?y } WHERE { NOT { ?x :q ?y } ?x :p ?y }
{ FILTER NOT EXISTS { ?x :q ?y } } ?x :p ?y
Without the group the filter would see ?x and
?y from the triple pattern after it and check each solution
separately, which is a different rule. The same applies to a
SET after the NOT and to a NOT
nested inside another.
Importing SPARQL (sparqlToRule) runs the other way. A
FILTER or FILTER NOT EXISTS written before a
pattern, BIND or nested group that binds one of its
variables is moved to the end of its group, where SRL's in-order reading
matches SPARQL's, and the import reports a filter-moved
warning. MINUS and BIND stay where they are:
SPARQL evaluates both in place too.
packages/srl/test/w3c-proposed/ holds evaluation tests
for these cases in the W3C suite's format, each with data on which the
SRL and literal-SPARQL readings infer different triples; its README
lists both answers.
Canonical form and identity
expandIris rewrites prefixed names to full IRIs
throughout the AST, and generateRuleSet serialises back to
SRL from the AST rather than from source slices. Together these
make canonical text independent of how the author spelled their
prefixes, so a prefix-only edit is provably not a content change. The
verbatim headText, bodyText and
prologueText slices are kept beside the AST for display and
are not affected by expansion.
A rule's canonical form carries no prologue: prefixes are presentation, owned by the rule set.
Aggregation: GROUP
GROUP is the aggregation element proposed for SPARQL-RL
in w3c/data-shapes#1344,
with the grammar of w3c/data-shapes#1347.
sqlib also follows three open pull requests as if they were merged: #1375 puts a
comma between assignments, #1377 defines
the well-formedness conditions and evalAggregate, and #1376 adds the
specification's example. GROUP is gated by the
ruleAggregates feature flag
(FEATURE_RULE_AGGREGATES), which is off by
default.
With the flag off, GROUP is a syntax error:
SRL syntax error: GROUP requires the rule-aggregates extension (parse with { aggregates: true })
Syntax
GROUP is a rule body element: optional BY
variables, a pattern, and a block of assignments.
PREFIX : <http://example.org/>
RULE { ?c :employees ?n ; :salaryBill ?t }
WHERE {
?c a :Company .
GROUP BY ?c { ?x :employee ?c ; :salary ?s }
[ ?n := COUNT(DISTINCT ?x) ,
?t := SUM(?s) ]
}
| Part | Form |
|---|---|
BY |
one or more variables, no separator; may be left out |
| pattern | triple patterns, FILTER and SET |
| block | [ ?v := FUNCTION(…) , … ], assignments separated by
commas, or [] for none |
| functions | COUNT(*), COUNT(?v),
COUNT(DISTINCT ?v), COUNT(DISTINCT *),
SUM(?v), MIN(?v), MAX(?v) |
The block is required, even when empty, because its closing
] marks the end of the element: a triple pattern after a
GROUP can start with (.
These are refused, each with a message naming the fault:
AVG, SAMPLE, GROUP_CONCAT, an
expression as an argument (SUM(?a + 1)),
DISTINCT in SUM, MIN or
MAX, a NOT in the pattern, a
GROUP inside another GROUP or a
NOT, and parentheses in the block. Two assignments without
a comma between them, a leading or trailing comma, and a comma between
BY variables are parse errors.
Evaluation
GROUP follows evalRuleElements in w3c/data-shapes#1377.
It adds the results to the rows it receives and keeps every variable
they bind.
- The
GROUPreceives the rows produced by the elements before it. - The pattern's solutions are grouped by the
BYvariables, the only variables the pattern shares with those rows. ABYvariable bound before theGROUPselects that key's group; one the earlier rows do not bind takes its values from the pattern, or from aSETin it. With noBY, every solution falls in one group. - Each function runs over the solutions in a group, every pattern
variable included. Two children aged 10 are two solutions, so
SUM(?age)counts both. How many rows the earlier elements gave for a key does not change the result: a person with two names and two children counts 2. - Every row keeps what it had, and gains the
BYvariables and the results. The pattern's other variables stay inside theGROUP.
A row whose pattern has no solutions forms no group, so it is
dropped, whatever the functions are. A person with no children gets no
COUNT row rather than a count of 0. A GROUP
with no BY whose pattern has no solutions gives no row
either. An empty pattern, GROUP { }, has one solution, so
it gives one group.
An error in any assignment, such as SUM over a string,
drops the group, as an error in SET drops a row. An empty
block, [], keeps the rows whose pattern has at least one
solution.
COUNT(?v) counts the solutions in which ?v
is bound, and every pattern variable is bound in every solution, so it
equals COUNT(*). To count distinct values, write
COUNT(DISTINCT ?v). For the same reason
COUNT(DISTINCT *) equals COUNT(*).
Well-formedness
Violations are reported under the category
aggregate-scope:
- Each
BYvariable must be bound before theGROUP, by its pattern, or both. ABYvariable the pattern does not use is carried through unchanged. - A pattern variable that is not a
BYvariable belongs to theGROUP,SETtargets included, as a variable a subquery does not project belongs to the subquery. One of the same name before or after theGROUPis a different variable. To correlate it, list it afterBY. Unlike aNOT, which sees every variable bound before it, aGROUPsees only itsBYvariables. - Each result must be a new variable: not bound before, not used in the pattern, and not assigned again later in the body.
- A
SETin the pattern must not re-bind a variable. The pattern starts from theBYvariables bound before theGROUP, so aSETthere may compute aBYvariable that is not bound before it, but not one that is. - Each argument must be bound by the pattern or be a
BYvariable bound before theGROUP.
A FILTER or SET in the pattern follows the
usual order rule, with only the BY variables bound from
outside.
Patterns that follow from the evaluation
Using an earlier variable after the
GROUP. It is still bound:
?x a :Person ; :name ?name .
GROUP BY ?x { ?y :childOf ?x } [ ?n := COUNT(*) ]
A person with two names keeps two rows, each with the same count.
Counting per value the pattern supplies. Leave the
key out of the body before the GROUP. Binding it first
gives the same rows: a department with nobody in it forms no group
either way.
GROUP BY ?d { ?p :dept ?d } [ ?n := COUNT(*) ]
?d a :Dept . GROUP BY ?d { ?p :dept ?d } [ ?n := COUNT(*) ]
Two GROUPs in one rule. The second
keeps the first one's result. A row survives only if both patterns have
a solution for it:
?x a :Person .
GROUP BY ?x { ?y :childOf ?x } [ ?n := COUNT(*) ]
GROUP BY ?x { ?y :childOf ?x ; :age ?a FILTER(?a >= 18) } [ ?m := COUNT(*) ]
Rank within a partition. The keys come from before
the GROUP, and the comparison is a FILTER in
the pattern. The top score in each partition has nobody above it, so it
forms no group and gets no rank:
PREFIX : <http://example.org/>
RULE { ?p :rankInDept ?rank }
WHERE {
?p :dept ?dept ; :score ?score .
GROUP BY ?dept ?score { ?o :dept ?dept ; :score ?s2 FILTER(?s2 > ?score) }
[ ?better := COUNT(*) ]
SET (?rank := ?better + 1)
}
One value per item. When a pattern variable has
several values per item, it multiplies that item's rows. With two orders
of 100, one carrying two tags, SUM over
{ ?o :amount ?a ; :tag ?t } gives 300. Derive the items in
a rule of their own and aggregate those:
PREFIX : <http://example.org/>
RULE { ?o :taggedAmount ?a } WHERE { ?o :amount ?a ; :tag ?t }
RULE { :all :total ?total }
WHERE { GROUP { ?o :taggedAmount ?a } [ ?total := SUM(?a) ] }
The total is 200, because the helper's head holds one triple per
order. Keep the item's identity in the helper's head:
:all :amountSeen ?a would collapse the two amounts into
one.
Stratification
A rule with a GROUP is run-once, with the reason
aggregate (GROUP), and its monotonicity is reported as
aggregation. As for any run-once rule, every dependency
becomes closed, so whatever the pattern reads is complete
before the rule fires. A rule whose result feeds back into its own
pattern forms a cycle and is refused.
Compilation
A GROUP compiles to a subquery that groups the pattern
by the BY variables, joined to the elements before it. The
pattern's own variables are renamed (?_agg0_y), so the
subquery shares only the keys with the rest of the body. The join drops
a row whose key has no group. For
RULE { ?x :numChildren ?n } WHERE { ?x a :Person . GROUP BY ?x { ?y :childOf ?x } [ ?n := COUNT(*) ] },
compileRule returns:
PREFIX : <http://example.org/>
INSERT {
?x :numChildren ?n
} WHERE {
{
?x <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Person> .
{
SELECT ?x (COUNT( * ) AS ?_agg0_v0) WHERE {
?_agg0_y <http://example.org/childOf> ?x .
}
GROUP BY ?x
}
BIND(?_agg0_v0 AS ?n)
FILTER(BOUND(?n))
}
}
A failed aggregate leaves its subquery variable unbound, and
FILTER(BOUND(?n)) drops the row. A GROUP with
no BY also projects a hidden COUNT(*) and
keeps its row only when that is above 0, because SPARQL returns one row
for an aggregate over no solutions. When a BY variable
bound before the GROUP is not bound by the pattern, as
?score is in the rank example, the subquery cannot compute
it, since SPARQL evaluates a subquery without the outer bindings. The
subquery is then seeded with the distinct key values from the elements
before the GROUP, which repeats those elements inside
it.
Test cases
The W3C suite's GROUP tests are in the vendored
snapshot, packages/srl/test/w3c/, which takes them from
w3c/data-shapes#1380 and #1375 as if both were merged; its
SOURCE file says how. Both conformance harnesses run them
with GROUP on.
sqlib's own cases, in the suite's format, are in
packages/srl/test/group-cases/ and run in
packages/api/test/lib/RuleSetExecutor.groupCases.test.ts.