5. Pattern matching
Patterns are how EigenQL binds variables to resources. Every MATCH clause contains one or more patterns, and each pattern unifies with candidate resources in the layer (or in a DEFINE-derived relation, or in the FIBER overlay) to extend the current set of bindings.
The core unification function is try_match_resource in evaluate.rs, driven by apply_pattern / apply_negated_pattern.
5.1. Pattern anatomy
pub struct Pattern { pub subject: Variable, // ?x pub class: Option<Name>, // optional: Dog pub properties: Vec<PropertyPattern>, pub negated: bool, // NOT ?x { ... }}
pub struct PropertyPattern { pub property: Name, // prop name or IRI pub object: ValueOrVariable, // literal or ?var}Surface syntax:
ClassName( ?subject ) { prop1: ?var, prop2: "literal", ... }or untyped:
?subject { prop: ?v }The three parts:
- Subject — always a variable. The resource being described.
- Class predicate — optional; if present, the resource’s
is_amust include the named class (subclass chain walked viais_subclass_instance). - Property patterns — pairs of
property: targetwhere the target is either a literal (the resource’s property value must equal it) or a variable (bound to the value).
5.2. Subject variable semantics
The subject is always a Variable. It plays one of two roles depending on whether it already has a binding in scope:
- Free: no prior binding exists. The pattern scans the layer for resources and binds the subject variable to each one that matches.
- Bound: the variable is already bound (from an earlier pattern or FIBER clause). The pattern restricts to the specific resource — if that resource matches the class and property patterns, the binding survives; otherwise it’s dropped.
This is how patterns compose. Given:
MATCH Dog(?d) { breed: ?breed }MATCH Animal(?d) { diet: ?diet }the second pattern restricts ?d (already bound to a specific dog) to those dogs that also happen to be Animal instances with a diet property. The binding set narrows.
Conflict detection is implemented in try_match_resource: when a variable is already bound and the candidate resource disagrees (via values_equal), the match fails and that candidate is discarded.
5.3. Class predicates
Class predicates use the Name enum:
pub enum Name { ShortName(String), // Dog FullIri(Iri), // "urn:eigenius:example:Dog"}ShortNames are resolved against classes imported by USING (chapter 4 §4.2). FullIris are used directly.
Subclass matching is enabled: a pattern Animal(?x) matches any resource whose is_a chain leads to Animal through the subclass_of property. See is_subclass_instance.
Without a class predicate (?x { ... }), the pattern matches any resource regardless of class; only the property patterns constrain it.
5.4. Property patterns
A property pattern is:
property: targetwhere property is a Name and target is a literal value or a variable.
Property name resolution
At the point a property pattern is evaluated, the target resource’s properties are a BTreeMap<Iri, Value>. The property Name is resolved in one of two ways:
FullIri(iri)— used directly: the resource must have that exact IRI key.ShortName(s)— the evaluator scans the resource’s property keys and finds the IRI whose local name equalss. Seefind_property_by_shortname. If multiple keys could match, the first wins (the map isBTreeMap-ordered, i.e. sorted by full IRI).
A property pattern that references a property the resource doesn’t carry causes the match to fail — the pattern drops that resource from the candidate set.
Literal targets
MATCH Dog(?d) { breed: "German Shepherd" }The property pattern matches only resources whose breed value equals the literal, using values_equal. values_equal handles cross-type comparisons (integer vs float, IRI-string vs resource-reference) sensibly.
Variable targets
MATCH Dog(?d) { breed: ?breed }Variables may be free or bound:
- Free: the variable is assigned to the property’s value. In the result binding,
?breed→ value of thebreedproperty. - Bound: the property’s value is compared via
values_equalagainst the existing binding. Disagreement → the pattern fails.
5.5. Multiple patterns in a single MATCH
MATCH Dog(?d) { owner: ?o }, Person(?o) { country: "DE" }Comma-separated patterns in a MATCH clause are evaluated in order. Each subsequent pattern extends the current binding set. Bindings that fail any pattern are dropped. Effectively this is an equi-join on shared variables.
Order matters for efficiency but not for result semantics — the final binding set is the same regardless of pattern order, as long as you’re matching against the same fixed layer.
5.6. Negation: NOT { ... }
A negated pattern drops bindings where a candidate resource matches.
MATCH ?x {}MATCH NOT ?x { retired: true }Reads as “for every resource ?x, require that no property retired: true exists on it”. Equivalent to: ?x is not retired.
Implementation: apply_negated_pattern. For each existing binding, the evaluator checks whether any candidate resource matches the inner pattern (against current bindings). If none match, the binding survives; if at least one matches, the binding is dropped.
The NOT EXISTS expression (distinct from pattern negation)
NOT EXISTS(?var) is an expression (for use in WHERE), not a pattern. It tests whether ?var is currently bound — returning true if the variable is not in the binding, false if it is. See chapter 7 §7.5. This overlaps conceptually with negated patterns but is evaluated differently.
Interaction with stratification
If a DEFINE rule body contains a negated pattern on a derived relation, the stratifier ensures no negative cycles are introduced. See chapter 10.
5.7. Where patterns can reference
A pattern’s subject is scanned over candidate resources drawn from three sources in combination:
- The layer chain — the canonical resources via
Layer::all_resources(). - Derived relations (
DEFINErules) — a pattern whose class is a relation name (e.g.Ancestor(?y)) scans that relation’s current binding set. - The FIBER overlay — transient resources produced by
FIBERclauses in the current query. Visible only within this query, not persisted.
The three are scanned in source order; a resource appearing in more than one source is considered once.
5.8. Empty property lists
MATCH ?x {} is valid — it matches every resource in scope, binding ?x to each. Use it when you want to iterate all resources and let subsequent clauses or WHERE narrow the set.
MATCH ?x {}WHERE ?x = "urn:eigenius:test:alice"5.9. Class-only matching
MATCH Dog(?d) {} binds ?d to every Dog in the layer without examining its properties.
5.10. A concrete example
Given the test layer (core ontology + animals.json):
USING "urn:eigenius:example:Dog", "urn:eigenius:example:Person"
MATCH Dog(?d) { name: ?dog_name, owner: ?o}MATCH Person(?o) { name: ?owner_name, country: ?country}WHERE ?country IN ["DE", "FR"]What happens:
- First
MATCH: scan all Dog resources, bind?d/?dog_name/?ofor each. - Second
MATCH: for each binding so far, restrict to rows where?o(already bound) resolves to a Person with anameandcountry. Bind?owner_nameand?country. WHERE: keep only bindings where?countryis in the given array.
After this chain, the surviving bindings have all five variables populated and can be shaped by RETURN.