An Ecto-inspired DSL for defining Datalog programs.
use ExDatalog.Schema in a module to declare relations, facts, rules,
and queries. The module then exposes program/0, new/0,
materialize/0,1, and query/2 functions.
Example
defmodule FamilyRules do
use ExDatalog.Schema
relation :parent do
field :parent, :atom
field :child, :atom
end
relation :ancestor do
field :ancestor, :atom
field :descendant, :atom
end
fact parent(:alice, :bob)
fact parent(:bob, :carol)
rule ancestor(X, Y) do
parent(X, Y)
end
rule ancestor(X, Z) do
parent(X, Y)
ancestor(Y, Z)
end
query :descendants_of_alice do
find Y
where ancestor(:alice, Y)
end
end
{:ok, knowledge} = FamilyRules.materialize()
FamilyRules.query(:descendants_of_alice, knowledge)
#=> [:bob, :carol]Relation DSL
Relations declare named schemas with typed fields:
relation :parent do
field :parent, :atom
field :child, :atom
endSupported field types: :atom, :integer, :string, :any. A relation
must declare at least one field; zero-arity relations are not supported and
raise ExDatalog.DSL.CompileError at compile time.
Fact DSL
Facts assert ground tuples:
fact parent(:alice, :bob)Bulk facts:
facts :parent do
row :alice, :bob
row :bob, :carol
endRule DSL
Rules derive new facts. Uppercase identifiers are logic variables,
lowercase atoms and :atoms are constants, and _ is a wildcard:
rule ancestor(X, Y) do
parent(X, Y)
endNegation uses not_:
rule bachelor(p) do
male(p)
not_ married(p, _)
endConstraints use named predicates:
rule high_earner(P) do
income(P, S)
gt(S, 100_000)
endSupported constraints: eq, neq, gt, gte, lt, lte,
add, sub, mul, div, is_integer, is_binary, is_atom,
starts_with, contains, member.
Query DSL
Queries define named post-materialization lookups:
query :all_ancestors do
find X, Y
where ancestor(X, Y)
endQueries operate on materialized knowledge and use Knowledge.match/3
internally.
Aggregate Syntax
Aggregates (count, sum, min, max) are used in rule bodies as
named predicates. The result variable must appear in the rule head:
rule dept_count(D, N) do
emp(E, D)
count(E, N)
endAggregate inputs are integer-only. A rule may contain at most one aggregate, and aggregates may not appear in recursive rules.
BEAM Callback Predicates
Call deterministic Elixir functions from rule bodies:
predicate :adult?, AgeChecker, :adult?, [:integer], :boolean
rule active_user(U) do
user(U, Age)
adult?(Age)
endValue-returning callbacks bind a result variable:
predicate :double, Math, :double, [:integer], :valueCallbacks run in isolated, monitored processes with configurable
timeout (:callback_timeout_ms, default 100ms). Timeouts and exceptions
filter the binding.
Backward Compatibility
The DSL compiles into the existing Program builder API. All existing
builder APIs (Program.add_relation/3, Program.add_fact/3,
Program.add_rule/2,3,4, ExDatalog.materialize/2) continue to work.
Summary
Functions
Declares a ground fact.
Declares multiple facts for the same relation.
Declares a BEAM callback predicate usable in rule bodies.
Declares a named post-materialization query.
Declares a relation with typed fields.
Declares a Datalog rule.
Explicit wildcard for use in rule bodies and queries.
Functions
Declares a ground fact.
fact parent(:alice, :bob)The relation must be declared before the fact.
Declares multiple facts for the same relation.
facts :parent do
row :alice, :bob
row :bob, :carol
end
Declares a BEAM callback predicate usable in rule bodies.
predicate :adult?, MyPredicates, :adult?, [:integer], :boolean
rule adult(Name) do
person(Name, Age)
adult?(Age)
endArguments:
name— the predicate name used in rule bodies.module/function— the Elixir function to call.arg_types— declared argument types (currently informational; their length sets the callback arity).return_type—:boolean(filter) or:value(binds the last argument position as the result variable).
For :value predicates, the last argument in the rule-body call is the
result variable; the remaining arguments are passed to the function.
The referenced module.function is called at evaluation time in an
isolated, monitored process. A :callback_timeout_ms option
(default 100ms) applies to each call; timeouts and exceptions filter
the binding.
Declares a named post-materialization query.
query :descendants_of_alice do
find y
where ancestor(:alice, y)
endThe find clause specifies which variables to extract.
The where clause specifies the relation and pattern to match.
Declares a relation with typed fields.
relation :parent do
field :parent, :atom
field :child, :atom
endSupported types: :atom, :integer, :string, :any.
Declares a Datalog rule.
Uppercase identifiers in the head and body are logic variables.
Lowercase atoms and :atoms are constants. _ is a wildcard.
rule ancestor(X, Y) do
parent(X, Y)
endNegation uses not_:
rule bachelor(P) do
male(P)
not_ married(P, _)
endConstraints use named predicates:
rule high_earner(P) do
income(P, S)
gt(S, 100_000)
end
@spec wildcard() :: :wildcard
Explicit wildcard for use in rule bodies and queries.
Inside DSL rule and query bodies, _ is treated as a wildcard.
If Elixir's treatment of _ as a special form causes issues,
use wildcard() as an explicit alternative.
Examples
iex> ExDatalog.Schema.wildcard()
:wildcard
rule bachelor(P) do
male(P)
not_ married(P, _)
end