ExDatalog.Schema (ExDatalog v0.4.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, materialize/0, 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.

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. Lowercase identifiers are logic variables, atoms starting with : 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, salary)
  gt(salary, 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 (Preview)

Aggregates are parsed but not yet executable:

rule employee_count(dept, agg(:count, emp)) do
  employee(emp, dept)
end

Attempting to materialize a program with aggregates returns {:error, %ExDatalog.UnsupportedFeature{feature: :aggregates}}.

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

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.

Lowercase identifiers in the head and body are logic variables. Atoms starting with : 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, salary)
  gt(salary, 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, wildcard())
end