Builder for constructing Datalog programs.
A program holds:
- Relations — named schemas with arity and type information.
- Facts — ground tuples asserted as true for a given relation.
- Rules — inference rules that derive new facts from existing ones.
Relation names are string keys stored in a map. They do not collide with
internal atoms like :positive, :negative, :wildcard, or :constraint
because those atoms appear in tuple positions ({:positive, atom}),
not as map keys.
Programs are built using a pipeline of builder functions. Structural
validation (arity checking, relation existence) is done at build time by
add_relation/3, add_fact/3, and add_rule/2. On failure these return
{:error, String.t()} with a human-readable message.
Error propagation in pipelines
Because builder functions return t() | {:error, String.t()}, a failed
step will short-circuit the rest of the pipeline: the {:error, _} tuple
passes through unchanged. This means you can pipe freely and check for
errors at the end:
{:ok, result} =
Program.new()
|> Program.add_relation("edge", [:atom, :atom])
|> Program.add_relation("path", [:atom, :atom])
|> Program.add_fact("edge", [:a, :b])
|> Program.add_rule(...)
|> ExDatalog.query()If add_relation/3 fails, the {:error, msg} tuple flows through
add_fact/3 and add_rule/2 without raising, and ExDatalog.query/1
will detect the error struct and return {:error, [msg]}.
Semantic validation (variable safety, stratification, constraint binding)
is done separately by ExDatalog.Validator.validate/1, which returns
{:error, [ExDatalog.Validator.Error.t()]} with structured error structs.
Note: builder methods perform a subset of the same checks as the validator (relation existence, arity). This is intentional: the builder provides early feedback for interactive construction, while the validator is the canonical source of truth and catches issues the builder cannot (e.g., programs assembled by directly modifying the struct, which bypasses builder validation).
Example
iex> alias ExDatalog.{Program, Atom, Rule, Term}
iex> program =
...> Program.new()
...> |> Program.add_relation("parent", [:atom, :atom])
...> |> Program.add_relation("ancestor", [:atom, :atom])
...> |> Program.add_fact("parent", [:alice, :bob])
...> |> Program.add_fact("parent", [:bob, :carol])
...> |> Program.add_rule(
...> Rule.new(
...> Atom.new("ancestor", [Term.var("X"), Term.var("Y")]),
...> [{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])}]
...> )
...> )
iex> length(program.facts) == 2
true
iex> length(program.rules) == 1
true
Summary
Functions
Adds a ground fact to the program.
Adds a relation schema to the program.
Adds a rule to the program.
Returns true if the relation is defined in the program.
Creates a new, empty Datalog program.
Returns the schema for a relation, or nil if not defined.
Types
@type fact_values() :: [ExDatalog.Term.value()]
@type ir_type() :: :integer | :string | :atom | :any
@type relation_name() :: String.t()
@type relation_schema() :: %{arity: non_neg_integer(), types: [ir_type()]}
@type t() :: %ExDatalog.Program{ facts: [{relation_name(), fact_values()}], relations: %{required(relation_name()) => relation_schema()}, rules: [ExDatalog.Rule.t()] }
Functions
@spec add_fact(t(), relation_name(), fact_values()) :: t() | {:error, String.t()}
Adds a ground fact to the program.
The relation must be declared via add_relation/3 and the number of
values must match the relation's arity.
Returns {:error, reason} if:
- The relation is not defined.
- The arity of
valuesdoes not match the relation schema. - A value in
valuesis not an integer, string, or atom (floats are not supported).
Examples
iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> Program.add_fact(program, "parent", [:alice, :bob])
%ExDatalog.Program{
relations: %{"parent" => %{arity: 2, types: [:atom, :atom]}},
facts: [{"parent", [:alice, :bob]}],
rules: []
}
iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> {:error, _} = Program.add_fact(program, "unknown", [:alice])
{:error, "relation \"unknown\" is not defined"}
@spec add_relation(t(), relation_name(), [ir_type()]) :: t() | {:error, String.t()}
Adds a relation schema to the program.
types is a list of type atoms (:integer, :string, :atom, :any)
with length equal to the arity of the relation.
Returns {:error, reason} if:
nameis empty.typesis empty.- The relation already exists.
Examples
iex> ExDatalog.Program.add_relation(ExDatalog.Program.new(), "parent", [:atom, :atom])
%ExDatalog.Program{
relations: %{"parent" => %{arity: 2, types: [:atom, :atom]}},
facts: [],
rules: []
}
iex> {:error, _} = ExDatalog.Program.add_relation(ExDatalog.Program.new(), "", [:atom])
{:error, "relation name must be a non-empty string"}
@spec add_rule(t(), ExDatalog.Rule.t()) :: t() | {:error, String.t()}
Adds a rule to the program.
Performs structural validation:
- The head relation must be declared.
- The head arity must match the relation schema.
- All body atoms must reference declared relations with matching arities.
Semantic validation (variable safety, stratification) is deferred to
ExDatalog.Validator.
Returns {:error, reason} if any structural check fails.
Examples
iex> alias ExDatalog.{Program, Rule, Atom, Term}
iex> program =
...> Program.new()
...> |> Program.add_relation("parent", [:atom, :atom])
...> |> Program.add_relation("ancestor", [:atom, :atom])
iex> rule = Rule.new(
...> Atom.new("ancestor", [Term.var("X"), Term.var("Y")]),
...> [{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])}]
...> )
iex> result = Program.add_rule(program, rule)
iex> length(result.rules) == 1
true
@spec has_relation?(t(), relation_name()) :: boolean()
Returns true if the relation is defined in the program.
Examples
iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> Program.has_relation?(program, "parent")
true
iex> alias ExDatalog.Program
iex> Program.has_relation?(Program.new(), "unknown")
false
@spec new() :: t()
Creates a new, empty Datalog program.
Examples
iex> ExDatalog.Program.new()
%ExDatalog.Program{relations: %{}, facts: [], rules: []}
@spec relation(t(), relation_name()) :: relation_schema() | nil
Returns the schema for a relation, or nil if not defined.
Examples
iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> Program.relation(program, "parent")
%{arity: 2, types: [:atom, :atom]}
iex> alias ExDatalog.Program
iex> Program.relation(Program.new(), "unknown")
nil