All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
v0.4.1 (2026-06-21)
Fixed
- C1: Corrected moduledoc and
rule/2docstring to state that uppercase identifiers are logic variables (was incorrectly documented as lowercase). Updated moduledoc examples,rule/2docstring, README, and article 01 to use uppercase variables consistently. - C2: Aggregate syntax (
agg(...)) now raisesExDatalog.DSL.CompileErrorwith a clear message in both head and body positions, instead of crashing withCompileErrororFunctionClauseError. Updated CHANGELOG, article 05, and moduledoc to reflect reality. - H1: Replaced meaningless aggregate struct-literal tests with real end-to-end compile-time tests that exercise
agg(...)DSL syntax in both head and body positions. - H2: Query without a
whereclause now raisesExDatalog.DSL.CompileErrorwith a readable message instead of an opaqueProtocol.UndefinedError. - M1: Query
findvariables that are not present in thewherepattern now raiseExDatalog.DSL.CompileErrorat compile time instead of silently returning wrong results. - M2: Unrecognized expressions inside
relationandfactsblocks now raiseExDatalog.DSL.CompileErrorinstead of being silently dropped. - L1: All DSL authoring errors now raise
ExDatalog.DSL.CompileError(was a mix ofCompileErrorandArgumentError). - L2: README test count updated to 792 (was stale at 751, then 786).
- L3: Dropped unused
_programparameter fromvalidate_rules!/1. - L4: Removed duplicate
eqconstraint test fromSchemaCoverageTest. - L5: Fixed moduledoc reference from
materialize/0tomaterialize/0,1. - L6: Deleted
livebooks/examples.md(521-line unreferenced LLM-generated artifact).
Changed
- Version bumped from 0.4.0 to 0.4.1
- All DSL error messages now go through
ExDatalog.DSL.CompileErrorfor uniform catch/rescue ExDatalog.UnsupportedFeaturestruct retained for future use but no longer produced at runtime
v0.4.0 (2026-06-20)
Added
ExDatalog.Schema— Ecto-inspired DSL macro module for defining Datalog programsrelation/2macro declares typed relation schemasfact/1andfacts/2macros declare ground factsrule/2macro declares rules with uppercase logic variables,not_for negation, named constraint predicates (gt,eq,add, etc.)query/2macro declares named post-materialization queries withfind/wherewildcard/0helper for explicit wildcards in rule bodies- Generated
program/0,materialize/0,1,queries/0,query/2functions
ExDatalog.UnsupportedFeaturestruct for forward-compatible aggregate syntax parsingExDatalog.DSL.CompileErrorexception for readable DSL macro errors- 33 integration tests for the DSL (relations, facts, rules, negation, constraints, queries, backward compatibility)
- Livebook tutorial:
livebooks/ex_datalog_dsl.livemd - Educational articles in
docs/articles/
Changed
- Version bumped from 0.3.0 to 0.4.0
- DSL is the recommended authoring layer; builder API remains the stable lower-level API
- README updated with DSL quickstart
Notes
- Query DSL operates on materialized knowledge only (no query planner yet)
- Aggregate syntax is not yet supported — using
agg(...)raisesExDatalog.DSL.CompileErrorat compile time - All 718 existing tests continue to pass (now 786 total)
[0.3.0] - 2025-06-19
Added
- Tuple shorthand for rules:
Program.add_rule/3andProgram.add_rule/4accept{relation, [terms]}tuples for heads,{:polarity, {relation, [terms]}}for body literals, and{:op, args...}for constraints. Uppercase atoms (:X) become variables,:_becomes a wildcard, lowercase atoms and other values become constants. Term.from/1— converts shorthand values toTerm.t()following Prolog convention.ExDatalog.Atom.from_tuple/1— constructs an atom from{"relation", [terms]}shorthand.Constraint.from_tuple/1— constructs a constraint from operator tuples like{:neq, :A, :B},{:add, :X, :Y, :Z},{:is_integer, :V}.
Changed
ExDatalog.Resultrenamed toExDatalog.Knowledge— the struct returned bymaterialize/2now reflects that it represents a materialized knowledge base, not a query result. All references updated across source, tests, docs, and livebooks.ExDatalog.query(2-arity) renamed toExDatalog.materialize/2— the top-level API function now reflects that it runs the full fixpoint pipeline, not a single query.- Telemetry events renamed:
[:ex_datalog, :query, :start|:stop|:exception]→[:ex_datalog, :materialize, :start|:stop|:exception]. ExDatalog.validate/1andExDatalog.compile/1now pass through{:error, _}tuples from the builder pipeline instead of raisingFunctionClauseError.materialize/2also passes through{:error, _}from a failed pipeline step.- Livebook examples (
quickstart.livemd,examples.livemd,examples.exs) converted to tuple shorthand notation. README quickstart updated accordingly.
[0.2.0] - 2025-05-15
Added
- Constraint types: type predicates (
type_integer/1,type_binary/1,type_atom/1), string predicates (starts_with/2,contains/2), and membership (member/2) - Constraint evaluation dispatch via
ExDatalog.Constraint.evaluate/3with behaviour-based constraint modules ExDatalog.Constraint.Contextcarries storage backend capabilities through evaluation pipelineExDatalog.Capabilitiesstruct and merge/satisfies/from_backend APIExDatalog.Storage.ETSbackend with per-relation ETS tables, wrapped-tuple keys, and concurrent read support- Storage behaviour
init/2for passing backend options (access, write_concurrency, read_concurrency) ExDatalog.IR.from_term/1now tags list constants as{:const, {:list, [...]}}for uniform IR value representationExDatalog.IR.resolve_operand/2andExDatalog.IR.value_to_native/1shared helpers for constraint evaluation- Portability test suite (Map vs ETS) covering transitive closure, arithmetic, comparison, negation, type predicates, string predicates, and membership
- Backend conformance macro (
ExDatalog.Storage.BackendConformance) for shared storage contract tests - Engine
storage_optsoption for passing options to storage backends try/afterresource cleanup inEngine.Naivefor ETS teardown safety- ETS tombstone guard (
guard_not_tombstoned!) for clear errors on post-teardown operations Constraint.valid?/1now requires{:const, list}for:memberright operand- Validator safety tests for type, string, and membership constraint ops
- Public-struct
evaluate/3dispatch tests for comparison, arithmetic, type, string, membership - ETS teardown guard test and idempotent teardown test
- Doctests on
Constraint.Context.new/0,1,2 - Documentation:
docs/constraints.mdanddocs/storage_backends.md
Changed
Storage.ETS.member?/3now uses:ets.member/2(was:ets.match_object/3) — correct O(1) lookupStorage.ETS.init/2now acceptswrite_concurrencyandread_concurrencyoptionsStorage.ETS.upsert_index_entryusesMapSetinstead of lists for O(log n) membership checksStorage.ETStables namedex_datalog_<relation>andex_datalog_idx_<relation>for observability in:observerStorage.ETS.update_index/4raisesArgumentErroron unknown relations (was silent no-op)Storage.Map.update_index/4raisesArgumentErroron unknown relations (was silent empty-index)- Both backends now report
type_predicates: trueandstring_predicates: truein capabilities Constraint.evaluate/3public-struct clause delegates to IR clause via recursion; documented both dispatch pathsIR.from_term({:const, list})now produces{:const, {:list, [tagged_elements]}}instead of{:const, list}IR.from_constraint/1usesmaybe_from_term/1pattern matching instead of bareifConstraints.Arithmetic.apply_arithmeticuses guard clauses with integer checks- All constraint modules use shared
IR.resolve_operand/2andIR.value_to_native/1(was 5x duplicated) Engine.Naive.evaluate/2wraps evaluation intry/afterto ensure ETS teardown on exceptionsStorage.ETS.teardown/1returns:ok(was returning stale struct); post-teardown operations raiseArgumentErrorTelemetry.emit_stop/5requires explicitstorage_typeargument (removed default:map)- Constraint behaviour callback uses
Binding.t()instead ofmap()for type safety Storage behaviour
teardowncallback return type widened to:ok | {:error, term()}Constraints.Stringrenamed toConstraints.StringPredicateto avoid stdlibStringconflictConstraint.Contextthreads real storage capabilities from engine through evaluatorEngine.Naiveextractsbuild_result/8andemit_result_telemetry/5helpers fromdo_evaluate_inner- Backend conformance macro no longer injects
aliasor@schemasinto caller module - Dispatch table documented as closed (not a runtime registry)
Capabilities.from_backend/1spec widened from{module(), term()}to{atom(), term()}Engine.Evaluator.eval_rule_iteration/5acceptsConstraint.Contextand passes to constraint evaluationConstraintEval.apply/3andapply_one/3accept optionalConstraint.ContextparameterStorage.Map.size/2andStorage.ETS.size/2logLogger.debugfor unknown relations- ETS moduledoc includes rationale for choosing
:settable type @optional_callbacksforbuild_index/3,get_indexed/4,update_index/4in Storage behaviour- Dispatch consistency tests verify all 16 ops dispatch correctly and
valid?/1covers every category - "Defines exactly 12 callbacks" test removed from storage tests (was fragile)
Fixed
Storage.ETS.member?/3was using:ets.match_object/3instead of:ets.member/2— wrong and slower- ETS tests "returns same state struct" removed — they enforced a misleading identity invariant
Constraint.valid_right?(:member, ...)now requires{:const, list}— prevents silent runtime filter- Capabilities doctest for
from_backend/1now uses valid Elixir instead of...ellipsis
0.1.0 - 2025-04-18
Added
- Phase 0: Architecture and design blueprint (pure Elixir, semi-naive evaluation, storage behaviour, hash-join indexing primitives)
- Phase 1: AST, DSL, and term model
ExDatalog.Term—var/1,const/1,wildcard/0, guards, and validationExDatalog.Constraint— comparison (gt,lt,gte,lte,eq,neq) and arithmetic (add,sub,mul,div) constructorsExDatalog.Atom— relation references with termsExDatalog.Rule—%Rule{head, body, constraints}with polarity on body literalsExDatalog.Program— builder pipeline (new/0,add_relation/3,add_fact/2,add_rule/2)ExDatalog.Validator— Phase 1 structural validation (arity, relation existence, term validity)ExDatalog.Validator.Error— structured error types with kind, context, and message
- Phase 2: Semantic validation
ExDatalog.Validator.Safety— variable safety and range-restriction checks (all head variables appear in a positive body atom)ExDatalog.Validator.Stratification— Tarjan SCC-based stratification (detects unstratifiable negation cycles)- Chained validation pipeline: structural → safety → stratification
- Phase 3: IR compiler
ExDatalog.Compiler— AST-to-IR compilation producingIR.Programwith strata, rules, facts, and relation schemasExDatalog.Compiler.Stratifier— Tarjan SCC algorithm to assign strata and detect unstratifiable programsExDatalog.IR— engine-neutral IR structs:IR.Program,IR.Stratum,IR.Rule,IR.Atom,IR.Fact,IR.Constraint
- Phase 4: Semi-naive engine
ExDatalog.Engine.Naive— semi-naive fixpoint evaluation with k-position delta computationExDatalog.Engine.Evaluator— single-rule evaluation with k-position delta variantsExDatalog.Engine.Binding— binding environment operations (lookup, extend, ir_value_to_native)ExDatalog.Engine.Join— sequential-scan join (join/3), tuple matching (match_tuple/3), projection (project/2), indexed join (join_indexed/4, not yet wired into evaluator)ExDatalog.Engine.ConstraintEval— constraint evaluation (comparison filters, arithmetic extensions)ExDatalog.Storage.Map— default Map/MapSet-based storage backendExDatalog.Knowledge— knowledge base struct with relations, stats, and provenance fields- Full pipeline:
ExDatalog.materialize/2public API
- Phase 5: Negation and stratification
- Negative body atoms (
{:negative, %IR.Atom{}}) evaluated as filters against fully-materialised lower-stratum relations - Stratification validation rejects unstratifiable programs before evaluation
- Per-stratum fixpoint iteration with timeout and iteration limits (
:max_iterations,:timeout_ms)
- Negative body atoms (
- Phase 6: Provenance / explain
ExDatalog.Explain— derivation attribution whenexplain: trueoption is passedresult.provenance.fact_origins— maps each derived tuple to a rule that produced it (last-wins; not guaranteed canonical)result.provenance.rules— rule map for human-readable rule lookup- Zero-overhead when
explain: false(default): provenance tracking is entirely skipped
- Phase 7: Telemetry
ExDatalog.Telemetry—:telemetryevent emission at evaluation start, stop, and exception- Events:
[:ex_datalog, :query, :start],[:ex_datalog, :query, :stop],[:ex_datalog, :query, :exception] - Measurements:
system_time,duration,iterations - Metadata:
relation_count,stratum_count,relation_sizes,kind,reason,stacktrace
- Documentation: What is Datalog? guide covering history, concepts, industry use cases, and LLM integration patterns
Changed
Program.add_fact/3validates fact values, rejecting floats and non-ground types with descriptive error messagesProgram.add_relation/3,add_fact/3, andadd_rule/2propagate{:error, _}through pipelines instead of raisingFunctionClauseErrorValidator.validate/1no longer mutates the program struct;validate/1is now idempotent (program == elem(validate(program), 1))Compiler.compile/1normalizes facts/rules order independently (was previously done byvalidate/1)Compiler.compile/1validates IR invariants after compilation (unique rule IDs, stratum bounds, relation references, rule-in-stratum consistency)Engine.Evaluator.eval_rule_iteration/4skips variant evaluation when the delta relation is empty, avoiding wasted join workEngine.Naive.iterate/1uses incrementalmerge_new/2instead of full-storagesnapshot_facts/3per iterationIR.Constraint.serialize/1always includes the:resultkey (even whennil), making the format losslessExDatalog.Atom.variables/1now deduplicates variable names (was previously returning duplicates forr(X, X))Constraint.result_variable/1has a catchall clause instead of only matching{:var, name}andnilConstraint.div/3documented as integer division (Kernel.div/2), not float divisionValidator.check_atom/4fetches the relation schema once and passes it to arity checking, avoiding redundantMap.fetch- Storage indexing API (
build_index/3,update_index/4,get_indexed/4,Join.join_indexed/4) marked@doc falsefor v0.1.0
Fixed
Program.add_fact/3now rejects float values and non-ground term tuples with clear error messages (previously: silent acceptance, crash at compile time)Term.const/1raisesArgumentError(notFunctionClauseError) for unsupported value types including floatsValidator.validate/1returns the original program struct unchanged, fixing two invariants:validate/1is now idempotent, andvalidate → add_rule → validateno longer produces interleaved rule orderEngine.Evaluator.eval_rule_iteration/4deduplicates k=0 fact rule results against existing tuples (was returning unfiltered results)Engine.Naive.derive/5computes derivation and origins in a single pass, eliminating 2x evaluation overhead whenexplain: true