ExDatalog.Schema (ExDatalog v0.5.0)

Copy Markdown View Source

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
end

Supported 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
end

Rule 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)
end

Negation uses not_:

rule bachelor(p) do
  male(p)
  not_ married(p, _)
end

Constraints use named predicates:

rule high_earner(P) do
  income(P, S)
  gt(S, 100_000)
end

Supported 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)
end

Queries 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)
end

Aggregate 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)
end

Value-returning callbacks bind a result variable:

predicate :double, Math, :double, [:integer], :value

Callbacks 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

fact(rel_call)

(macro)

Declares a ground fact.

fact parent(:alice, :bob)

The relation must be declared before the fact.

facts(rel_name, list)

(macro)

Declares multiple facts for the same relation.

facts :parent do
  row :alice, :bob
  row :bob, :carol
end

predicate(name, module, function, arg_types, return_type)

(macro)

Declares a BEAM callback predicate usable in rule bodies.

predicate :adult?, MyPredicates, :adult?, [:integer], :boolean

rule adult(Name) do
  person(Name, Age)
  adult?(Age)
end

Arguments:

  • 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.

query(name, list)

(macro)

Declares a named post-materialization query.

query :descendants_of_alice do
  find y
  where ancestor(:alice, y)
end

The find clause specifies which variables to extract. The where clause specifies the relation and pattern to match.

relation(name, list)

(macro)

Declares a relation with typed fields.

relation :parent do
  field :parent, :atom
  field :child, :atom
end

Supported types: :atom, :integer, :string, :any.

rule(head, list)

(macro)

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)
end

Negation uses not_:

rule bachelor(P) do
  male(P)
  not_ married(P, _)
end

Constraints use named predicates:

rule high_earner(P) do
  income(P, S)
  gt(S, 100_000)
end

wildcard()

@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