Query Language

This chapter introduces the basic query facilities provided by tribles. Queries are declared in a small declarative language that describes which values should match rather than how to iterate them.

The find! macro builds a Query by declaring variables and a constraint expression. A minimal invocation looks like:

#![allow(unused)]
fn main() {
let results = find!((a), a.is(1.into())).collect::<Vec<_>>();
}

find! returns an Iterator over tuples of the bound variables. Matches can be consumed lazily or collected into common collections:

#![allow(unused)]
fn main() {
for (a,) in find!((a), a.is(1.into())) {
    println!("match: {a}");
}
}

Variables can optionally specify a concrete type to convert the underlying value:

#![allow(unused)]
fn main() {
find!((x: i32, y: Value<ShortString>),
      and!(x.is(1.into()), y.is("foo".to_value())))
}

The first variable is read as an i32 and the second as a short string if the conversion succeeds. The query engine walks all possible assignments that satisfy the constraint and yields tuples of the declared variables.

Built-in constraints

find! queries combine a small set of constraint operators to form a declarative language for matching tribles:

  • and! builds an IntersectionConstraint requiring all sub-constraints to hold.
  • or! constructs a UnionConstraint accepting any satisfied alternative.
  • ignore! tells the query engine to ignore variables in a sub-query. Constraints mentioning ignored variables are evaluated without checking those positions, so their bindings neither join with surrounding constraints nor appear in the result set.
  • Collection types such as TribleSet provide a has method yielding a ContainsConstraint for membership tests.

Any data structure that can iterate its contents, test membership and report its size can implement ContainsConstraint, so queries have no inherent ordering requirements.

Ignored variables are handy when a sub-expression references fields you want to drop. The engine skips checking them entirely, effectively treating the positions as wildcards. If the underlying constraint guarantees some value, ignoring works like existential quantification; otherwise the ignored portion is simply discarded. Without ignoring, those variables would leak into the outer scope and either appear in the results or unintentionally join with other constraints.

Alternatives are expressed with or! and temporary variables can be hidden with ignore!:

#![allow(unused)]
fn main() {
find!((x), or!(x.is(1.into()), x.is(2.into())));

find!((x),
      ignore!((y), and!(x.is(1.into()), y.is(2.into()))));
}

In the second query y is ignored entirely—the engine never checks the y.is(2.into()) part—so the outer query only enforces x.is(1.into()) regardless of whether any y equals 2.

Example

#![allow(unused)]
fn main() {
use tribles::prelude::*;
use tribles::examples::{self, literature};

let dataset = examples::dataset();

for (title,) in find!((title: Value<_>),
                     and!(dataset.has(title), title.is("Dune".to_value()))) {
    println!("Found {}", title.from_value::<&str>());
}
}

This query searches the example dataset for the book titled "Dune". The variables and constraint can be adapted to express more complex joins and filters.

matches!

Sometimes you only want to check whether a constraint has any solutions. The matches! macro mirrors the find! syntax but returns a boolean:

#![allow(unused)]
fn main() {
use tribles::prelude::*;

assert!(matches!((x), x.is(1.into())));
assert!(!matches!((x), and!(x.is(1.into()), x.is(2.into()))));
}

Custom constraints

Every building block implements the Constraint trait. You can implement this trait on your own types to integrate custom data sources or query operators with the solver.

Regular path queries

The path! macro lets you search for graph paths matching a regular expression over edge attributes. It expands to a RegularPathConstraint and can be combined with other constraints. Invoke it through a namespace module (social::path!) to implicitly resolve attribute names:

#![allow(unused)]
fn main() {
use tribles::prelude::*;
NS! { namespace social {
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" as follows: GenId;
    "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" as likes: GenId;
} }
let mut kb = TribleSet::new();
let a = fucid(); let b = fucid(); let c = fucid();
kb += social::entity!(&a, { follows: &b });
kb += social::entity!(&b, { likes: &c });

let results: Vec<_> = find!((s: Value<_>, e: Value<_>),
    social::path!(&kb, s (follows | likes)+ e)).collect();
}

The middle section uses a familiar regex syntax to describe allowed edge sequences. Editors with Rust macro expansion support provide highlighting and validation of the regular expression at compile time. Paths reference attributes from a single namespace; to traverse edges across multiple namespaces, create a new namespace that re-exports the desired attributes and invoke path! through it.