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.
[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.Result— result struct with relations, stats, and provenance fields- Full pipeline:
ExDatalog.query/1andExDatalog.query/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