Stateless join primitives for semi-naive Datalog evaluation.
This module provides the core matching and joining functions used by the
Engine.Evaluator during rule evaluation:
match_tuple/3— matches a single storage tuple against an atom's IR terms, extending the binding if variables are unbound or verifying them if already bound.join/3— joins a list of binding environments with a collection of tuples for a single body atom. This is the sequential-scan version used byEngine.Naivein v0.1.0.project/2— projects a binding environment onto a head atom's variables, producing a result tuple.
The indexed variant join_indexed/4 is implemented but not used by the
default engine. It is exposed for alternative engine implementations and
will be wired into the evaluator in a future release.
All functions are pure and stateless; storage and index management is handled
by ExDatalog.Storage and Engine.Naive respectively.
Summary
Functions
Joins a list of binding environments with a collection of tuples using a sequential scan.
Matches a storage tuple against a list of IR terms, extending the binding.
Projects a binding environment onto a head atom's IR terms, producing a result tuple.
Types
@type binding() :: ExDatalog.Engine.Binding.t()
@type ir_term() :: ExDatalog.IR.ir_term()
Functions
Joins a list of binding environments with a collection of tuples using a sequential scan.
For each (binding, tuple) pair, calls match_tuple/3. Collects all
successful matches. This is O(bindings × tuples) and suitable for small
relations or when no index is available.
Examples
iex> alias ExDatalog.Engine.Join
iex> terms = [{:var, "X"}, {:var, "Y"}]
iex> tuples = [{:alice, :bob}, {:carol, :dave}]
iex> Join.join([%{}], terms, tuples)
[%{"X" => :alice, "Y" => :bob}, %{"X" => :carol, "Y" => :dave}]
iex> terms = [{:var, "X"}, {:var, "Y"}]
iex> tuples = [{:alice, :bob}, {:carol, :dave}]
iex> Join.join([%{"X" => :alice}], terms, tuples)
[%{"X" => :alice, "Y" => :bob}]
Matches a storage tuple against a list of IR terms, extending the binding.
For each position i in the terms list:
{:var, name}— ifnameis bound, the tuple value at positionimust equal the bound value. If unbound,nameis bound to the tuple value.{:const, ir_value}— the tuple value at positionimust equal the native value ofir_value.:wildcard— any value matches; no binding change.
Returns {:ok, extended_binding} if all positions match, or :no_match.
The terms list and the tuple must have the same length.
Examples
iex> alias ExDatalog.Engine.Join
iex> terms = [{:var, "X"}, {:var, "Y"}]
iex> Join.match_tuple(terms, {:alice, :bob}, %{})
{:ok, %{"X" => :alice, "Y" => :bob}}
iex> terms = [{:var, "X"}, {:const, {:atom, :bob}}]
iex> Join.match_tuple(terms, {:alice, :bob}, %{})
{:ok, %{"X" => :alice}}
iex> terms = [{:var, "X"}, {:const, {:atom, :carol}}]
iex> Join.match_tuple(terms, {:alice, :bob}, %{})
:no_match
iex> terms = [{:var, "X"}, {:var, "X"}]
iex> Join.match_tuple(terms, {:alice, :alice}, %{})
{:ok, %{"X" => :alice}}
iex> terms = [{:var, "X"}, {:var, "X"}]
iex> Join.match_tuple(terms, {:alice, :bob}, %{})
:no_match
@spec project(ExDatalog.IR.Atom.t(), binding()) :: tuple()
Projects a binding environment onto a head atom's IR terms, producing a result tuple.
For each term in the head atom:
{:var, name}— look upnamein the binding and use its value.{:const, ir_value}— use the native value ofir_value.:wildcard— not valid in rule heads (caught by the validator).
Returns an Elixir tuple suitable for insertion into a storage relation.
Examples
iex> alias ExDatalog.Engine.Join
iex> ir_atom = %ExDatalog.IR.Atom{relation: "ancestor", terms: [{:var, "X"}, {:var, "Y"}]}
iex> Join.project(ir_atom, %{"X" => :alice, "Y" => :bob})
{:alice, :bob}
iex> ir_atom = %ExDatalog.IR.Atom{relation: "result", terms: [{:var, "X"}, {:const, {:atom, :ok}}]}
iex> Join.project(ir_atom, %{"X" => 42})
{42, :ok}