Skip to content

Latest commit

 

History

History
747 lines (558 loc) · 51.5 KB

CHANGELOG.md

File metadata and controls

747 lines (558 loc) · 51.5 KB

Changelog

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.

v1.2.2 — 2026-08-10

Drop-in replacement for v1.2.1. No file-format changes, no public API changes.

Bug fixes

  • Two handles on one file in the same process (#304): FileLock::acquire treated a lock file whose holder PID equalled our own as stale, deleted it, and opened anyway — on the theory that it could only be a leaked handle. The far more likely cause is a handle that is open and in use right now elsewhere in the process, so the "self-heal" silently produced two live FileBackends on one file. Each caches its own header.page_count, allocates new pages from that count, and bounds-checks read_page against it, so the two page tables diverge. That — not an allocation off-by-one — is the source of the intermittent Page N out of bounds (total pages: M). It also corrupted the lock itself: whichever handle dropped first ran FileLock::drop and removed the lock file that by then belonged to the survivor, leaving a live handle unlocked and admitting other processes too. A lock held by our own PID is now refused with an error naming the same-process case; a lock held by a different, dead process is still reclaimed as before. Behaviour change: callers that relied on reopening a path they already hold open now get an error instead of silent corruption, and should reuse the existing handle (Minigraf is cheap to clone and all clones share one database).

  • String comparison in predicates: [(< ?a ?b)], >, <= and >= now order two strings lexicographically instead of failing. Previously every ordering predicate routed through to_float_pair, which errors on non-numeric operands, so a string comparison silently matched no rows — and because both directions returned nothing, a range filter dropped the entire relation rather than partitioning it. This most often bit ISO-8601 timestamps stored as strings. The predicate path now agrees with value_lt / value_cmp, which min, max and :order-by already use. Mixed-type comparison (string vs number) remains an error, and NaN comparisons remain false.

v1.2.1 — 2026-06-28

Drop-in replacement for v1.2.0. No file-format changes, no public API changes.

Bug fixes

  • Magic sets fb adornment: queries with a literal keyword in value position (e.g. (reports-to ?emp :alice)) now return correct results instead of empty results (#298). Root cause: seed fact used an ephemeral random UUID as entity; the 1-arg guard bound the variable to that UUID rather than the keyword value. Fixed with a deterministic sentinel entity (Uuid::new_v5) and a 2-arg guard that binds from value position.
  • Magic sets recursive rules: (reach :a ?y) with intermediate variable ?mid and keyword entity binding now evaluates correctly (#297).

v1.2.0 — 2026-06-26

Drop-in replacement for v1.1.x. No file-format changes, no public API breaking changes. Upgrading requires no code changes.

Features

  • Magic Sets rewriting — recursive Datalog queries with bound arguments are now automatically rewritten top-down via magic sets, propagating bound values into recursive rules to avoid full-relation scans (#289)
    • Adornment classification, seed fact generation, magic guard injection, SCC-aware propagation rule generation, full rewrite() wired into execute_query_with_rules
    • Limitation: mutual recursion through negation is not rewritten (documented in ROADMAP §9.6)
  • Per-query complexity limits — :max-derived-facts and :max-results clauses added to query; global default raised to 1,000,000 derived facts (#288, #290)

Bug fixes

  • selective_fact_fetch: include asserted flag in dedup key — previously retracted facts could shadow live facts under certain access patterns (#285, #286)
  • selective_fact_fetch: restore per-pattern entity priority lost in a prior refactor (#283)
  • v5→v6 migration: fix hang caused by using header.page_count as the B-tree start page instead of the correct offset (#272)

Performance

  • Eliminate backend mutex hold on cache hits in CommittedFactLoaderImpl::resolve — read path no longer acquires the write lock when the page is cached (#279)
  • Pre-build MutexStorageBackend in CommittedFactLoaderImpl to eliminate one Arc::clone per resolve() call (#280, #281)

Infrastructure

  • Split Java, Android, Swift, and C bindings into independent repos under project-minigraf org — completes #231 repo split (#231 phase 2)
  • minigraf-ffi yanked from crates.io — UniFFI layer inlined into each binding repo's private shim; use the per-language packages instead
  • cascade.yml updated to dispatch core-release to all 7 binding repos on every version tag
  • All binding repos migrated to OIDC trusted publishing — NPM_TOKEN and CARGO_REGISTRY_TOKEN secrets removed; crates.io uses rust-lang/crates-io-auth-action@v1, npm uses setup-node@v6 OIDC
  • Python, Node, and WASM binding repos now include a prepare job that pins the minigraf dependency version in Cargo.toml and commits before building — consistent with Java, Android, Swift, and C

Documentation

  • Add docs/ERROR_REFERENCE.md: full inventory of user-facing errors (PRS/QRY/STG/WAL/API categories, 113 entries) with cause, resolution steps, and bad-input examples; docs-only reference codes PRS-001…API-009 (#192)

v1.1.1 — 2026-05-17

Patch release. Fixes cargo-dist Windows build failure that prevented REPL binaries and crates.io publish from completing for v1.1.0. No code changes to the library itself.

Build

  • Exclude fuzz crate from workspace so cargo-dist can build on Windows (MSVC linker incompatible with #![no_main] libFuzzer targets) — fixes #263

v1.1.0 — 2026-05-17

Drop-in replacement for v1.0.0. No file-format changes, no public API changes, no query surface changes. Upgrading requires no code changes.

Performance

  • Hash-join replaces nested-loop join for multi-clause queries — O(N) instead of O(N²) for large fact sets (#202, #203, #204)
  • Selective B+Tree fact fetch: queries with bound entity/attribute skip full-scan and read only relevant index pages (#208)
  • Predicate push-down into join ordering (#207)
  • Cost-based clause ordering for not/or rules (#206, #205)
  • SIMD crossover analysis and benchmarking infrastructure (#229)

Bug fixes

  • Fixed critical correctness and durability bugs found during deep audit (#225): fact visibility edge cases, WAL entry ordering, checkpoint atomicity
  • Fixed read-only handle Drop triggering unnecessary checkpoint, modifying the file on close (#226)
  • QueryResult::Transacted and Retracted reverted to tuple variants — struct-variant form introduced post-1.0 was a breaking change (#261)
  • Added Minigraf::current_tx_count() -> u64 as additive API to expose the :as-of monotonic counter without breaking existing pattern matches

Reliability & testing

  • WAL fault injection harness (FaultInjectingBackend) with 9 crash-recovery tests (#209, #210, #214)
  • Storage/migration resilience: migration matrix, index corruption recovery, concurrency stress tests (#215, #216, #217)
  • Property-based query correctness tests against a reference evaluator (proptest, #212)
  • Datalog parser/evaluator fuzz targets with seed corpus (#213)
  • Per-module branch coverage gates in CI (#219)
  • Long-haul smoke suite: 500 entities × 10 write/read/checkpoint cycles (#220)
  • XTDB and Datomic semantic compatibility tests (#221)

Internal

  • Workspace-wide clippy lint enforcement — 336 violations fixed (#232)
  • Grammar conformance test harness: pest shadow grammar + EDN corpus (#233)
  • CI: codecov-action v3→v5, actions-rs→dtolnay migration

Reliability Hardening — 2026-05-17

Summary

Six PRs hardening the v1.0.0 codebase: WAL fault injection, storage/migration resilience, query correctness (property-based + coverage gates), long-haul smoke testing, and XTDB/Datomic semantic compatibility. No API changes, no file-format changes, no new runtime dependencies.

PR #254 — #209 + #210 + #214: WAL Fault Injection

  • FaultInjectingBackend (#[cfg(test)]-only wrapper around StorageBackend) — configurable write-fail, flush-fail, read-fault injection
  • 9 new WAL fault tests: write-fail propagation, flush-fail without data corruption, read-fault on WAL replay, CRC corruption discard, checkpoint atomicity under backend failure, partial checkpoint recovery via WAL replay, multi-writer serialisation, concurrent write+checkpoint, backend error propagation as Err not panic

PR #257 — #215 + #216 + #217: Storage & Migration Resilience

  • tests/migration_matrix_test.rs — 5 migration tests: v7 round-trip, v3 empty migrate, corrupt magic returns Err, unsupported version returns Err, WAL replay idempotent
  • tests/index_corruption_test.rs — 5 corruption-resilience tests: checksum mismatch triggers index rebuild, btree leaf/internal corrupt pages return Err without panic, root pointer mismatch handled, non-critical corruption still serves queries on good data
  • 5 new concurrency stress tests in tests/concurrency_test.rs: stress readers during writer, failed write then success, rollback after partial work, open/write/checkpoint/query loop per thread, nightly stress loop (#[ignore])

PR #256 — #212 + #213 + #219: Property-Based Testing & Coverage Gates

  • tests/property_test.rs (proptest, cfg(not(wasm32))): 3 property tests — EAV model correctness vs naive reference evaluator, bi-temporal monotonicity, retract visibility invariant
  • .github/workflows/coverage-gates.yml — per-module branch coverage thresholds; CI fails if coverage drops below gate

PR #258 — #220: Long-Haul Smoke Suite

  • tests/smoke_test.rs (#[ignore] nightly): smoke_large_graph_10_cycles — 500 entities × 10 attributes × 10 update cycles; 7 invariants: active count (333), retracted count, fact count bounds, temporal snapshot integrity, prepared query consistency, recursive rule transitive closure, WAL checkpoint round-trip
  • .github/workflows/smoke.yml — nightly 5am UTC, 15-min timeout, runs --include-ignored

PR #259 — #221: XTDB & Datomic Compatibility Corpus

  • tests/xtdb_compat_test.rs — 10 semantic ports (Apache 2.0): EAV, tx-time :as-of, valid-time :valid-at, retraction (current + historical), Datalog join, negation, recursive rules, parameterised queries, combined bi-temporal
  • tests/datomic_compat_test.rs — 9 independently written semantic ports: datom model, multi-entity attribute, tx-time :as-of, retract-entity, multi-variable :find, ground-value binding, parameterised query (prepared), named reusable rules, predicate expression filter

Tests

935 tests passing (943 total, 8 ignored: 6 or+neg-cycle stratification doc tests, 1 nightly concurrency stress, 1 nightly smoke).


Optimizer & Benchmarks — 2026-05-16

Summary

Three optimisation PRs extending the v1.0.0 performance work. No API changes, no file-format changes, no new runtime dependencies. Test count unchanged at 850.

PR #249 — #207 + #206: Predicate Push-Down & Mixed Rule Optimization

  • optimizer::plan() extended to accept Expr (predicate/arithmetic) clauses; they are interleaved at the earliest position where all their variables are bound, minimising intermediate binding sets
  • StratifiedEvaluator gains mixed-rule path: when a rule stratum contains both positive-only rules and rules with not/not-join, the evaluator now evaluates all positive rules first and applies negation filters in a second pass within the same stratum — eliminates a class of ordering-dependent bugs in cross-stratum negation

PR #251 — #205: Cost-Based not/or Ordering

  • optimizer::plan() assigns selectivity estimates to not/not-join and or/or-join clauses; they are sorted after their ground-variable producers but before unconstrained pattern scans
  • not/not-join blocks placed after the patterns that produce their join variables (was: end of plan regardless); or/or-join blocks placed by estimated output cardinality

PR #253 — #229: SIMD Benchmarking & Crossover Analysis

  • benches/simd_helpers.rs: valid_time_filter_simd, as_of_filter_simd, sum_simd_i64 — portable SIMD kernels using wide::i64x4 / u64x4
  • Criterion benchmark groups: simd_temporal, simd_as_of, simd_aggregate — scalar vs. SIMD crossover analysis at 1K–1M facts
  • Analysis result: SIMD crossover occurs at ~8K–16K facts per query; scalar path preferred below that threshold; SIMD integration deferred to post-1.0 backlog pending real-workload profiling

Tests

850 tests passing (844 passing + 6 ignored: confirmed or+neg-cycle stratification bug, deferred to post-1.0 backlog). All optimizer PRs are optimisations; no new integration tests added.


Performance — 2026-05-15

Summary

Four O(N²) query-engine bottlenecks eliminated. No API changes, no file-format changes, no new dependencies.

PR #246 — #208: B+Tree Selective Lookup

  • get_facts_by_entity, get_facts_by_attribute, get_facts_by_entity_attribute promoted from #[cfg(test)] to production in src/graph/storage.rs
  • New selective_fact_fetch helper in executor.rs: inspects query patterns for bound entity literals and bound attribute strings; calls index-driven fetches instead of get_all_facts() when ≤4 distinct lookups detected
  • as_of queries continue to use full scan (required for correctness)
  • New benchmark groups: btree_lookup/entity_point, btree_lookup/attribute_scan

PR #247 — #202 + #203 + #204: Hash-Join Cluster

  • #202 (executor.rs): not/not-join bodies pre-computed once into HashSet<Vec<(String, Value)>> keyed on join variables; O(1) probe per outer binding replaces O(N) re-scan. normalize_value handles keyword→Ref representation asymmetry in value position.
  • #203 (executor.rs): or/or-join branches now evaluated from a single empty seed; branch results hash-joined back onto incoming bindings on shared user-visible variables (__-prefixed metadata keys excluded). or-join projects to join_vars before joining.
  • #204 (matcher.rs): join_with_pattern detects join variable (entity position first, value position second), builds HashMap<Value, Vec<Bindings>> once, probes per existing binding O(1). normalize_join_value handles keyword→Ref. Falls back to nested-loop when no join variable found.

Tests

850 tests passing (844 passing + 6 ignored: confirmed or+neg-cycle stratification bug, deferred to post-1.0 backlog).


v1.0.0 — Phase 8 Complete (2026-05-01)

Milestone

This is the v1.0.0 release. The public Rust API and the .graph file format are now stable and committed to semantic versioning. File format stability is guaranteed from this release.

Phase 8 summary

All Phase 8 cross-platform targets have shipped:

  • 8.1a — Browser WASM (BrowserDb, IndexedDbBackend, @minigraf/browser on npm) — v0.20.0
  • 8.1b — Server-side WASM (wasm32-wasip1 / WASI, Wasmtime/Wasmer CI) — v0.20.0
  • 8.2 — Mobile bindings (Android .aar on GitHub Packages, iOS .xcframework via SPM, UniFFI) — v0.21.0
  • 8.3a — Python (minigraf on PyPI, pre-built wheels) — v0.22.0
  • 8.3b — Java/JVM (io.github.adityamukho:minigraf-jvm on Maven Central, fat JAR) — v0.23.0
  • 8.3c — C FFI (minigraf.h + platform tarballs on GitHub Releases) — v0.24.0
  • 8.3d — Node.js (minigraf on npm, pre-built .node binaries) — v0.25.0

Also in this release

  • pkg/ renamed to minigraf-wasm/, swift/ renamed to minigraf-swift/ — consistent top-level naming across all workspace packages (issue #179)
  • @minigraf/browser now published to npm on every tagged release (issue #179)
  • @minigraf/wasi published to npm on every tagged release (issue #178) — WASI binary packaged for Node.js WASI consumers
  • Per-platform READMEs added: minigraf-wasm/, minigraf-node/, minigraf-ffi/python/, minigraf-c/, minigraf-ffi/java/

Tests

795 tests passing (788 passing + 7 ignored: confirmed or+neg-cycle stratification bug, deferred to post-1.0 backlog).

v0.25.0 — 2026-04-26

Added

  • Phase 8.3d: Node.js bindings published to npm as minigraf. Install with npm install minigraf. No build step required — prebuilt .node binaries for Linux x86_64/aarch64, macOS universal2, Windows x86_64. API: new MiniGrafDb(path), MiniGrafDb.inMemory(), .execute(datalog), .checkpoint(). Full TypeScript definitions included.

v0.24.0 — Phase 8.3c: C Bindings (2026-04-26)

Added

  • Phase 8.3c: C bindings distributed as GitHub Releases tarballs. Download minigraf-c-v0.24.0-<platform>.tar.gz (Linux/macOS) or .zip (Windows) from the release page. Each archive contains the prebuilt shared library plus minigraf.h. API: minigraf_open, minigraf_open_in_memory, minigraf_execute, minigraf_string_free, minigraf_checkpoint, minigraf_close, minigraf_last_error. Memory contract mirrors SQLite: minigraf_execute returns a heap-allocated JSON string; call minigraf_string_free to release it.
  • minigraf-c/: new workspace crate (cdylib + staticlib) — Cargo.toml, src/lib.rs
  • minigraf-c/cbindgen.toml: cbindgen 0.29.2 configuration
  • minigraf-c/include/minigraf.h: committed stable header (cbindgen-generated)
  • .github/workflows/c-ci.yml: PR test matrix on 4 platforms + header drift check
  • .github/workflows/c-release.yml: release workflow — builds + packages platform tarballs, uploads to GitHub Releases

795 tests.

v0.23.0 — Phase 8.3b: Java Desktop JVM Bindings (2026-04-25)

Added

  • Phase 8.3b: Java desktop JVM bindings published to Maven Central as io.github.adityamukho:minigraf-jvm:0.23.0. Add to Gradle: implementation("io.github.adityamukho:minigraf-jvm:0.23.0"). Fat JAR with embedded natives for Linux x86_64/aarch64, macOS universal2, Windows x86_64. API: MiniGrafDb.open(path), MiniGrafDb.openInMemory(), .execute(datalog), .checkpoint().
  • minigraf-ffi/java/: Gradle 8.11 project — build.gradle.kts, settings.gradle.kts, NativeLoader.kt (runtime native extraction from JAR resources), and Gradle wrapper
  • minigraf-ffi/java/src/test/kotlin/.../BasicTest.kt: JUnit 5 suite (in-memory, transact/query, error handling, file-backed persistence)
  • .github/workflows/java-ci.yml: PR test matrix on 4 platforms (Linux x86_64, Linux aarch64, macOS universal2, Windows x86_64)
  • .github/workflows/java-release.yml: release workflow — cross-compiles natives on 4 platforms, assembles fat JAR, publishes to Maven Central via Sonatype OSSRH

795 tests.

v0.22.0 — Phase 8.3a: Python Bindings (2026-04-25)

Added

  • Phase 8.3a: Python bindings published to PyPI as minigraf. Install with pip install minigraf. API: MiniGrafDb.open(path), MiniGrafDb.open_in_memory(), .execute(datalog), .checkpoint(). Pre-built wheels for Linux x86_64/aarch64, macOS universal2, Windows x86_64.

v0.21.1 — Patch: mobile/WASM docs (2026-04-19)

Changed

  • src/lib.rs: added Feature Flags section and WebAssembly targets subsection to crate-level docs — browser feature, wasm32-unknown-unknown target switcher note, and WASI build command
  • README.md: updated "For Mobile Apps" section — replaced Phase 8 placeholder with current state, added Kotlin/Swift quick-start snippets and link to wiki integration guide
  • Wiki Use-Cases.md: replaced Integration placeholder with full Android (Gradle setup, Kotlin API, error handling, threading) and iOS (SPM setup, Swift API, error handling, async) integration guides

795 tests.

v0.21.0 — Phase 8.2: Android/iOS Mobile Bindings (2026-04-19)

Added

  • minigraf-ffi crate: UniFFI 0.31 bindings exposing MiniGrafDb (open, openInMemory, execute, checkpoint) and MiniGrafError (Parse, Query, Storage, Other) to Kotlin and Swift
  • Android .aar release artifact, published to GitHub Packages (io.github.adityamukho:minigraf-android)
  • iOS .xcframework release artifact, distributed via Swift Package Manager (Package.swift at repo root)
  • mobile.yml CI workflow: cross-compiles Android targets with cargo-ndk, generates Kotlin/Swift UniFFI bindings, assembles AAR with Gradle, assembles xcframework with xcodebuild, and publishes both on every tag
  • docs-check CI job in rust.yml and release.yml — gates releases on cargo doc --all-features passing cleanly

Fixed

  • release.yml: added docs-check to host job's needs and if condition
  • wasm-release.yml / mobile.yml: retry loops extended from 20 to 40 attempts; inputs.tag || github.ref_name ordering corrected
  • minigraf-ffi/android/gradlew: removed inner double-quotes from DEFAULT_JVM_OPTS and replaced xargs/sed eval block with direct exec — fixes "Could not find main class" and garbled usage output
  • minigraf-ffi/android/build.gradle.kts: added android { publishing { singleVariant("release") } } — fixes AGP 8.x "SoftwareComponent 'release' not found"
  • mobile.yml Package.swift commit: pushes to unprotected swift-releases branch and moves tag via gh api -F force=true — avoids branch-protection blocks and string/boolean type mismatch

795 tests.

v0.20.1 — Patch: docs.rs browser module visibility (2026-04-19)

Fixed

  • browser module now appears on docs.rs: added docsrs to the cfg gate and doc(cfg(...)) badge annotation (src/lib.rs)

v0.20.0 — Phase 8.1: WebAssembly Support (2026-04-18)

Added

  • Phase 8.1a — Browser WASM (wasm32-unknown-unknown + wasm-bindgen):
    • BrowserDb public API: open_in_memory(), execute(), checkpoint(), export_graph(), import_graph()
    • BrowserBufferBackend — in-memory StorageBackend over a flat page buffer, identical byte layout to the native .graph format
    • IndexedDbBackend — page-granular IndexedDB storage (one 4 KB entry per page); only dirty pages written on checkpoint
    • wasm-pack build workflow (wasm32-unknown-unknown --features browser) generating minigraf-wasm/ with JS glue and TypeScript definitions
    • wasm-bindgen-test browser integration tests (Chrome + Firefox via wasm-pack test)
  • Phase 8.1b — Server-side WASM (wasm32-wasip1 / WASI):
    • FileBackend verified under WASI's capability-based filesystem (no backend changes needed)
    • CI workflow (wasm-wasi.yml) builds, unit-tests, and smoke-tests under Wasmtime and Wasmer on every push/PR
    • Thread-dependent tests gated with #[cfg(not(target_os = "wasi"))]
  • Cross-platform compatibility tests (issue #150):
    • tests/cross_platform_compat_test.rs — native round-trip (raw page byte copy) and fixture-readability tests
    • tests/fixtures/compat.graph — committed v7 binary fixture containing :alice :name "Alice" and :alice :age 30
    • examples/generate_compat_fixture.rs — reproducible fixture generator (native only; no-op on wasm32)
    • native_fixture_readable_by_browser_db wasm-bindgen-test — loads native fixture via BrowserDb::import_graph, verifies both facts
  • Release workflow: WASM artifacts (WASI binary + browser tarball) built and attached on every tag; cargo publish to crates.io on release

795 tests.

v0.19.0 — Phase 7.9: Publish Prep (2026-04-08)

Changed (breaking — internal visibility only)

  • Minigraf::repl() factory method replaces direct Repl::new(FactStorage) constructor — users call db.repl().run() instead
  • All internal types narrowed to pub(crate): FactStorage, PersistentFactStorage, FileHeader, StorageBackend, DatalogExecutor, PatternMatcher, Fact, TxId, VALID_TIME_FOREVER, Wal, and all related internals
  • Minigraf::inner_fact_storage() removed (was unused)

Added

  • Minigraf::repl(&self) -> Repl<'_> — constructs an interactive REPL session; Repl now borrows &Minigraf for lifetime safety
  • Full rustdoc on all public API items with # Examples doctests
  • [package.metadata.docs.rs] in Cargo.toml — docs.rs builds with all-features = true
  • #![warn(missing_docs)] — enforces documentation coverage going forward
  • crates.io and docs.rs badges in README.md
  • Installation section in README.md (cargo add minigraf / [dependencies] block)
  • macOS and Windows added to CI test matrix (rust.yml)
  • Strict cargo clippy -- -D warnings step in rust-clippy.yml

Fixed

  • Bare .unwrap() in library code replaced with .expect("lock poisoned") (RwLock operations in cache.rs, evaluator.rs) and .expect("WAL not initialized") (db.rs)
  • FileHeader::to_bytes now takes self by value (clippy wrong_self_convention)
  • Broken intra-doc link [Repl::run] in db.rs fixed to [crate::repl::Repl::run]

788 tests.

v0.18.0 — Phase 7.8: Prepared Statements (2026-04-04)

Added

  • Minigraf::prepare(query_str) -> Result<PreparedQuery> — parse and plan a query once, returning a PreparedQuery that can be executed many times with different bind values
  • PreparedQuery::execute(bindings: &[(&str, BindValue)]) -> Result<QueryResult> — substitute named $slot tokens and run against the current fact store state; plan is reused on each call
  • BindValue enum — Entity(Uuid), Val(Value), TxCount(u64), Timestamp(i64), AnyValidTime; each variant is permitted only in the appropriate bind-slot position
  • $identifier bind slot tokens in parser — accepted in entity position, value position, :as-of, and :valid-at; attribute position is intentionally rejected at prepare time
  • EdnValue::BindSlot(String), AsOf::Slot(String), ValidAt::Slot(String), Expr::Slot(String) AST variants (parse-only; panic at runtime if unsubstituted)
  • BindValue and PreparedQuery re-exported from lib.rs (public API surface)
  • tests/prepared_statements_test.rs — 17 integration tests covering all slot positions, combined temporal + entity parameterisation, plan reuse, and all error paths

Internal

  • src/query/datalog/prepared.rs — new module: prepare_query(), substitution logic, 19 unit tests; manual Debug impl for PreparedQuery (avoids FactStorage: Debug bound)
  • Panic guards (no slot-name interpolation) in executor.rs (4 sites) and storage.rs (1 site) for unsubstituted slot variants; CodeQL-safe (no user-controlled string in panic message)

Unchanged

  • db.execute(str) string API — no breaking change
  • Executor, optimizer, matcher — no changes required

v0.17.0 — Phase 7.7b: User-Defined Functions (2026-04-02)

Added

  • Minigraf::register_aggregate(name, init, step, finalise) — register a custom aggregate function usable in both :find grouping and :over (window) clauses
  • Minigraf::register_predicate(name, f) — register a single-argument filter predicate usable in [(name? ?var)] :where clauses
  • FunctionRegistry::register_aggregate_desc / register_predicate_desc (internal API)
  • WindowFunc::Udf(String) and UnaryOp::Udf(String) AST variants for runtime-resolved functions
  • UdfOps, AggImpl, PredicateDesc types in functions.rs

Changed

  • AggregateDesc now uses AggImpl discriminator instead of window_compatible+window_ops
  • apply_expr_clauses now returns Result<Vec<Binding>> and accepts &FunctionRegistry
  • eval_expr accepts Option<&FunctionRegistry> for UDF predicate resolution
  • WindowSpec::func_name() now returns String instead of &'static str
  • Parser emits Udf variants for unknown names instead of erroring (runtime validation)

Test count: 727 tests

v0.16.0 — Phase 7.7a: Window Functions (2026-04-02)

Added

  • Window functions in Datalog :find clause: (sum ?v :over (...)), (count ?v :over (...)), (min ?v :over (...)), (max ?v :over (...)), (avg ?v :over (...)), (rank :over (...)), (row-number :over (...)) with unbounded-preceding (cumulative from partition start to current row) frame
  • :partition-by ?var optional clause: absent means whole result set is one partition
  • :order-by ?var required in every :over clause; :desc optional (default ascending)
  • FunctionRegistry (src/query/datalog/functions.rs): string-keyed registry of aggregate descriptors; all built-in aggregates migrated into it; window_ops (init/step/finalise) on window-compatible entries; is_builtin flag separates built-ins from future UDFs
  • Mixed queries: regular aggregates and window functions may coexist in the same :find clause; aggregates collapse rows first, windows annotate over collapsed rows
  • AggregateDesc, AggState, WindowOps types in functions.rs
  • WindowFunc, Order, WindowSpec, FindSpec::Window types in types.rs
  • tests/window_functions_test.rs: 12 integration tests (cumulative sum, running count/min/avg, rank with ties, row-number, partition-by, desc ordering, mixed aggregate+window, single-row and empty-result edge cases, lag/lead parse rejection)

Changed

  • FindSpec::Aggregate { func }: type of func changed from AggFunc enum to String; dispatch goes through FunctionRegistry — internal change, no public API impact
  • AggFunc enum removed from types.rs; all aggregate dispatch centralised in functions.rs
  • apply_aggregation and apply_agg_func removed from executor.rs; replaced by apply_post_processing + helpers

Total

707 tests (unit + integration + doc)

v0.15.0 — Phase 7.6: Temporal Metadata Bindings (2026-04-01)

Added

  • Temporal pseudo-attributes: :db/valid-from, :db/valid-to, :db/tx-count, :db/tx-id, and :db/valid-at are now first-class bindable values in Datalog :where patterns
  • PseudoAttr enum and AttributeSpec wrapper type in types.rs — clean type-safe representation for real vs. pseudo attributes in Pattern
  • parse_query_pattern in parser.rs — detects :db/* keywords in the attribute position; rejects them in entity/value positions (parse error)
  • PatternMatcher::from_slice_with_valid_at constructor — passes query-level valid_at into the matcher
  • Hard-error guard in executor: per-fact pseudo-attrs (:db/valid-from, :db/valid-to, :db/tx-count, :db/tx-id) require :any-valid-time; error message tells user exactly what to add
  • :db/valid-at binds the effective query timestamp: explicit :valid-at <ts> → Value::Integer(ts), no :valid-at → Value::Integer(now), :any-valid-time → Value::Null
  • :any-valid-time now accepted as a standalone top-level query keyword (previously required :valid-at :any-valid-time form)
  • tests/temporal_metadata_test.rs: 16 new integration tests covering time-interval range queries, time-point lookups, tx-time correlation, :db/valid-at semantics, and all parse/runtime error guards

Total

647 tests (438 unit + 209 integration)

v0.14.0 — Phase 7.5: Tests + Error Coverage (2026-03-31)

Added

  • tests/production_patterns_test.rs: 8 cross-feature integration tests combining not+as-of, not-join+count, count+not, count+valid-at, recursion+not, or+count, or+sum, count+as-of-sequence
  • tests/error_handling_test.rs: 8 integration-level error-path tests covering runtime type errors (sum/string, sum/mixed, max/boolean), stratification errors (negative cycles), and parse safety errors (not-join unbound join var, or mismatched vars, aggregate unbound var)
  • Stream 3: ~109 unit tests for parser-unreachable branches and aggregation/arithmetic edge cases in executor.rs and evaluator.rs
  • cargo-llvm-cov branch coverage command documented in CONTRIBUTING.md
  • CI coverage enforcement: cargo-tarpaulin --fail-under 75 gates every PR; Codecov 75% threshold with 2% drop tolerance; fail_ci_if_error: true
  • Nightly cargo-llvm-cov --branch workflow: uploads LCOV to Codecov (branch-coverage flag) and attaches HTML artifact (30-day retention); also triggerable via workflow_dispatch
  • Codecov badge added to README.md

Coverage

  • Branch coverage: executor.rs ~85.71% (from ~75%), evaluator.rs ~89.29% (from ~73%)
  • Remaining uncovered branches: NaN-check defensive code not reachable via public API
  • Total: 617 tests (424 unit + 187 integration + 6 doc)

Known Issues

  • or-with-negative-cycle: stratification does not currently detect negative cycles inside or branches. Tracked via #[ignore] in tests/error_handling_test.rs::or_negative_cycle_rejected.

[0.13.1] — 2026-03-27

Performance

  • filter_facts_for_query snapshot fix — function now returns Arc<[Fact]> instead of a throwaway FactStorage, eliminating the O(N) four-BTreeMap index rebuild that occurred on every non-rules query call. execute_query path constructs zero FactStorage objects. execute_query_with_rules still converts Arc<[Fact]> back to FactStorage for StratifiedEvaluator (deferred).
  • ~62–65% speedup on non-rules queries at 10K facts: query/point_entity/10k 22 ms → 8.6 ms; aggregation/count_scale/10k 28 ms → 9.7 ms.
  • Evaluator loop: accumulated_facts computed once per iteration (was 4 separate get_asserted_facts() calls).

Added

  • PatternMatcher::from_slice(Arc<[Fact]>) constructor — creates a matcher from an immutable fact snapshot without index reconstruction.

Technical

  • apply_or_clauses and evaluate_not_join signatures updated to accept Arc<[Fact]> instead of &FactStorage.
  • 6 new tests: 4 in matcher.rs (unit), 2 in executor.rs (unit).

Tests

  • Total: 568 tests passing (390 unit + 172 integration + 6 doc)

[0.13.0] — 2026-03-26

Added

  • Disjunction (or / or-join): queries and rule bodies can now use (or branch1 branch2 ...) and (or-join [?v...] branch1 branch2 ...) where-clauses. Branches support all other clause types including not, not-join, Expr, and nested or/or-join. (and ...) groups multiple clauses into a single branch.
  • match_patterns_seeded on PatternMatcher for seeded branch evaluation.
  • evaluate_branch and apply_or_clauses as pub(crate) helpers in executor.rs.

Technical

  • WhereClause enum gains Or(Vec<Vec<WhereClause>>) and OrJoin { join_vars, branches } variants.
  • DependencyGraph::from_rules refactored with recursive collect_clause_deps helper; Or/OrJoin branches contribute positive dependency edges.
  • Rules with or/or-join in their bodies route to the mixed_rules path in StratifiedEvaluator.

[0.12.0] - 2026-03-25

Added

  • BinOp enum (14 variants: Lt, Gt, Lte, Gte, Eq, Neq, Add, Sub, Mul, Div, StartsWith, EndsWith, Contains, Matches) in types.rs
  • UnaryOp enum (5 variants: StringQ, IntegerQ, FloatQ, BooleanQ, NilQ) in types.rs
  • Expr enum (Var, Lit, BinOp, UnaryOp) — composable expression AST in types.rs
  • WhereClause::Expr { expr: Expr, binding: Option<String> } variant — None = filter, Some(var) = arithmetic binding
  • parse_expr_arg / parse_expr / parse_expr_clause in parser.rs; dispatch at all 4 clause sites (query :where, rule body, not body, not-join body)
  • Parse-time regex validation for matches? patterns via regex-lite; invalid patterns are rejected with a clear error
  • check_expr_safety + check_expr_safety_with_bound in parser.rs — forward-pass safety check; recurses into not/not-join bodies; unbound Expr::Var references are rejected at parse time
  • outer_vars_from_clause updated for WhereClause::Expr — binding variable contributes to scope for subsequent clauses
  • eval_expr, eval_binop, is_truthy, apply_expr_clauses in executor.rs — evaluate expression trees against a binding; type mismatches and div/0 silently drop the row
  • apply_expr_clauses_in_evaluator in evaluator.rs — sibling helper for rule body and not-join evaluation paths
  • not_body_matches in executor.rs updated to seed with outer binding for expr-only not bodies
  • tests/predicate_expr_test.rs — 28 integration tests covering all operators, silent-drop semantics, integer division, NaN, int/float promotion, string predicates, regex, expr in not body, expr in rule body, bi-temporal + expr, arithmetic into aggregate

Semantics

  • Comparison operators (<, >, <=, >=) require both operands to be numeric (Integer or Float); type mismatch → row dropped
  • = / != use structural equality on Value — type mismatch returns false/true, not an error
  • Integer + Float promotes to Float; integer division truncates; division by zero → row dropped; NaN result → row dropped
  • is_truthy: Boolean(true) → true; non-zero Integer or Float → true; everything else (including Keyword, Ref, Null, zero, empty string, Boolean(false), -0.0) → false
  • matches? pattern compiled at eval time via regex-lite; pattern must be a string literal validated at parse time

Tests

  • Added tests/predicate_expr_test.rs (28 integration tests)
  • Total: 527 tests passing (365 unit + 156 integration + 6 doc)

[0.11.0] - 2026-03-25

Added

  • Aggregation in :find clause: count, count-distinct, sum, sum-distinct, min, max
  • :with grouping clause — variables that participate in grouping but are excluded from output rows
  • AggFunc enum and FindSpec enum in src/query/datalog/types.rs; DatalogQuery.find migrated from Vec<String> to Vec<FindSpec>; DatalogQuery.with_vars: Vec<String> field added
  • apply_aggregation post-processing step in executor.rs — runs after binding collection when any aggregate is present
  • extract_variables helper in executor.rs — non-aggregate extraction path (replaces inline loops)
  • apply_agg_func and value_type_name helpers in executor.rs
  • parse_aggregate helper in parser.rs; :find arm extended to accept EdnValue::List (aggregate expressions); :with keyword arm added
  • Parse-time validation: aggregate variables must be bound in :where; :with without any aggregate is rejected
  • tests/aggregation_test.rs — 24 integration tests covering all aggregates, :with, rules, negation, temporal filters

Semantics

  • count/count-distinct with no grouping vars on zero bindings → [[0]] (SQL behavior)
  • All other aggregates on zero bindings → empty result set
  • All aggregates skip Value::Null silently (SQL behavior)
  • Type mismatches (e.g. sum on String) fail fast with a runtime error
  • min/max on mixed Integer/Float is a runtime error
  • :with ?v adds ?v to the grouping key without adding it to output columns

Tests

  • Added tests/aggregation_test.rs (24 integration tests)
  • Total: 461 tests passing (327 unit + 128 integration + 6 doc)

[0.10.0] - 2026-03-24

Added

  • src/query/datalog/stratification.rs — DependencyGraph and stratify(): analyse rule dependency graphs at registration time; programs with negative cycles are rejected with a clear error
  • WhereClause::Not(Vec<WhereClause>) and WhereClause::NotJoin { join_vars, clauses } variants in types.rs; all exhaustive matches updated
  • (not clause…) in :where and rule bodies — stratified negation where all body variables must be pre-bound by outer clauses
  • (not-join [?v…] clause…) — existentially-quantified negation with explicit join-variable declaration; body variables not in join_vars are fresh/unbound
  • Safety check at parse time: every not body variable must be bound by an outer clause; every join_vars variable in not-join must be bound by an outer clause
  • Nesting constraint: not-join cannot appear inside not or another not-join — rejected at parse time
  • StratifiedEvaluator in evaluator.rs: stratifies rules, runs positive rules first, then applies not/not-join filters per binding for mixed rules
  • evaluate_not_join free function in evaluator.rs: builds partial binding from join_vars, converts Pattern and RuleInvocation body clauses to patterns, runs PatternMatcher; returns true if body is satisfiable (reject outer binding)
  • rule_invocation_to_pattern extracted as pub(super) free function from RecursiveEvaluator
  • Two not-post-filter sites in executor.rs now handle both Not and NotJoin via evaluate_not_join
  • tests/negation_test.rs — 10 integration tests for not (Phase 7.1a): basic absence, multi-clause, rule body, time-travel, negative cycle rejection
  • tests/not_join_test.rs — 14 integration tests for not-join (Phase 7.1b): basic exclusion, multiple join vars, multi-clause body, rule body, :as-of, :valid-at, negative cycle at registration, not+not-join coexistence, RuleInvocation in body end-to-end

Changed

  • Rule.body changed from Vec<EdnValue> to Vec<WhereClause> to support negation clauses alongside patterns
  • executor.rs execute_query_with_rules now delegates to StratifiedEvaluator instead of RecursiveEvaluator directly
  • rules.rs register_rule runs stratify() after each registration; returns Err on negative cycle (rules are not registered on error)

[0.9.0] - 2026-03-23

Added

  • src/storage/btree_v6.rs — proper on-disk B+tree for all four covering indexes (EAVT, AEVT, AVET, VAET); each B+tree node is one 4KB page (internal + leaf), with build_btree for bulk-load and range_scan for leaf-chain traversal
  • OnDiskIndexReader struct + CommittedIndexReader trait — page-cache-backed index lookup replacing the full in-memory BTreeMap; index memory usage is now O(cache_pages), not O(facts)
  • MutexStorageBackend<B> adapter — holds backend mutex only for the duration of a single read_page call on a cache miss; cache-warm pages require no lock, enabling concurrent range scans to proceed in parallel
  • tests/btree_v6_test.rs — 8 integration tests covering B+tree insert/range-scan, multi-page leaf chains, concurrent scan correctness with Barrier-synchronised threads, and v5→v6 migration roundtrip
  • test_concurrent_range_scans_correctness unit test in btree_v6.rs — verifies all 8 concurrent threads return identical non-empty scan results
  • bench_concurrent_btree_scan Criterion benchmark — measures wall-clock latency at 2/4/8 concurrent EAVT range scans; results updated in BENCHMARKS.md
  • FileHeader v6 (80 bytes): adds fact_page_count u64 field at bytes 72–80; automatic v5→v6 migration on first checkpoint

Changed

  • FORMAT_VERSION bumped 5→6; v5 databases auto-migrated on first save
  • BENCHMARKS.md updated with v6 open/memory improvements, concurrent B+tree scan results, heaptrack v6 numbers, and a "How to read these numbers" methodology section
  • README.md and BENCHMARKS.md: performance table updated to reflect v6 open-time reduction (~2.4×) and peak-heap reduction (~21%)

Fixed

  • Concurrent B+tree range scans no longer serialise on cache-warm pages — 4→8 thread scaling ratio improved from ~2.2× to ~1.9×

[0.8.0] - 2026-03-22

Added

  • BENCHMARKS.md — full Criterion benchmark results at 1K/10K/100K/1M facts with machine spec, HTML report references, and heaptrack memory profiles
  • examples/memory_profile.rs — heaptrack profiling binary; accepts fact count as positional arg
  • Cargo.toml metadata: repository, keywords, categories, readme, documentation fields
  • Memory profile table in README.md "Performance" section

Changed

  • README.md Performance section now links to BENCHMARKS.md for full benchmark details
  • Phase badge and status text updated to reflect Phase 6.4b completion
  • crates.io publish deferred to Phase 7.8 (API cleanup + publish prep; file format v6 now complete)

Removed

  • Dead clap dependency from [dependencies] — clap was listed but never imported in library or binary code

[0.7.1] - 2026-03-22

Fixed

  • Retraction semantics in Datalog queries: filter_facts_for_query Step 2 now computes the net view per (entity, attribute, value) triple via net_asserted_facts(). Previously, retracted facts continued to appear in query results because the original assertion record remained in the append-only log. Now, for each EAV triple in the tx window, only the record with the highest tx_count is considered — if it is a retraction, the triple is excluded from results.
  • Oversized facts are now rejected early in db.rs (check_fact_sizes) before any WAL write, using the MAX_FACT_BYTES constant (4 080 bytes) exported from packed_pages.rs. Previously, oversized facts could cause a panic deep in the page-packing path.

Added

  • net_asserted_facts(facts: Vec<Fact>) -> Vec<Fact> helper in src/graph/storage.rs: groups facts by EAV triple, keeps the record with the highest tx_count, and discards the triple if that record is a retraction. Used by both executor.rs and storage.rs.
  • check_fact_sizes(facts: &[Fact]) in src/db.rs: validates all facts against MAX_FACT_BYTES and returns a descriptive Err before writing to the WAL.
  • MAX_FACT_BYTES: usize constant in src/storage/packed_pages.rs: PAGE_SIZE - PACKED_HEADER_SIZE - 4 = 4 080 bytes.
  • tests/retraction_test.rs — 7 integration tests covering: assert/retract with no :as-of, as-of snapshot before/after retraction boundary, re-assert after retract, :any-valid-time with retraction, recursive rule retraction visibility at and before the retraction boundary.
  • tests/edge_cases_test.rs — 4 integration tests covering: oversized-fact file-backed error path, MAX_FACT_BYTES exact boundary (accepted), MAX_FACT_BYTES + 1 (rejected), in-memory database has no size limit.

[0.7.0] - 2026-03-22

Added

  • Packed fact pages (page_type = 0x02): ~25 facts per 4KB page, ~25× disk space reduction vs v4
  • LRU page cache (src/storage/cache.rs): configurable capacity (default 256 pages = 1MB)
  • OpenOptions::page_cache_size(usize) — tune page cache capacity
  • CommittedFactReader trait: index-driven fact resolution via page cache (no startup load-all)
  • File format v5: fact_page_format header field; auto-migration from v4 on first open
  • Page-based CRC32 checksum (v5): streams raw committed pages instead of all facts

Changed

  • PersistentFactStorage::new() takes page_cache_capacity: usize as second argument
  • Committed facts no longer loaded into Vec<Fact> at startup; only pending facts held in memory
  • FactStorage::get_facts_by_entity, get_facts_by_attribute use EAVT/AEVT index range scans

Fixed

  • v4 databases auto-migrated to v5 packed format on first open (no data loss)

[0.6.0] - 2026-03-21

Added

  • Four Datomic-style covering indexes (EAVT, AEVT, AVET, VAET) with bi-temporal keys (valid_from, valid_to in all key tuples)
  • FactRef { page_id: u64, slot_index: u16 } — forward-compatible disk location pointer (slot_index=0 in 6.1)
  • Canonical value encoding (encode_value) with sort-order-preserving byte representation
  • B+tree page serialization for index persistence (src/storage/btree.rs)
  • FileHeader v4 (72 bytes): adds eavt_root_page, aevt_root_page, avet_root_page, vaet_root_page (4×8=32 bytes), index_checksum (u32), replacing the reserved field
  • CRC32 sync check on open: index mismatch triggers automatic rebuild
  • FactStorage::replace_indexes() and index_counts() for index lifecycle management
  • Query optimizer (src/query/datalog/optimizer.rs): IndexHint enum, select_index(), plan() with selectivity-based join reordering
  • Join reordering skipped under wasm feature flag
  • Cargo.toml [features] section with default = [] and wasm = []
  • 6 integration tests in tests/index_test.rs for save/reload, bi-temporal, recursive rules regression

Changed

  • FactStorage internal structure: FactData { facts, indexes } under single Arc<RwLock<FactData>> for consistent snapshots
  • PersistentFactStorage::save() writes index B+tree pages and updates header checksum
  • PersistentFactStorage::load() performs sync check and fast-path index load
  • executor::execute_query() now calls optimizer::plan() before pattern matching
  • File format version bumped 3→4; automatic v1/v2/v3→v4 migration on first save
  • FORMAT_VERSION constant updated to 4

Fixed

  • NaN values in Value::Float now canonicalize to a single bit pattern in index encoding (deterministic sort order)

[0.5.0] - 2026-03-21

Added

  • Write-ahead log (WAL): fact-level sidecar <db>.wal with CRC32-protected binary entries
  • WriteTransaction API: begin_write() / commit() / rollback() for explicit ACID transactions
  • Crash recovery: WAL entries replayed on open; corrupt/partial entries discarded at first bad CRC32
  • Checkpoint: checkpoint() flushes WAL facts to .graph and deletes the WAL; auto-checkpoint on configurable threshold
  • FileHeader v3: last_checkpointed_tx_count field (repurposes unused edge_count slot)
  • FactStorage helpers: get_all_facts(), restore_tx_counter(), allocate_tx_count()
  • OpenOptions builder: OpenOptions::new().path("db.graph").open() or Minigraf::in_memory()
  • --file <path> CLI flag for the REPL binary
  • 41 new tests covering WAL, crash recovery, transactions, and checkpoint

Changed

  • src/minigraf.rs replaced by src/db.rs — Minigraf, OpenOptions, WriteTransaction public API
  • File format version bumped 2→3; automatic v1/v2→v3 migration on first checkpoint
  • REPL version string now tracks CARGO_PKG_VERSION automatically

Fixed

  • WAL-before-apply ordering: facts are now applied to in-memory state only after the WAL entry is fsynced, ensuring crash safety for both implicit (execute()) and explicit (WriteTransaction) write paths

[0.4.0] - 2026-03-21

Added

  • Bi-temporal support: every fact now carries transaction time (tx_id, tx_count) and valid time (valid_from, valid_to)
  • :as-of N query modifier for transaction time travel (counter or ISO 8601 timestamp)
  • :valid-at "date" query modifier for valid time point-in-time queries
  • :valid-at :any-valid-time to disable valid time filtering
  • (transact {:valid-from ... :valid-to ...} [...]) syntax for specifying valid time
  • Per-fact valid time override in transact (4-element fact vectors with metadata map)
  • File format version 2 with automatic migration from version 1

Changed

  • Breaking behaviour: queries without :valid-at now return only currently valid facts (valid_from <= now < valid_to). Existing Phase 3 databases are unaffected because all migrated facts have valid_to = MAX.
  • FactStorage::transact() now accepts an optional TransactOptions parameter

Fixed

  • PersistentFactStorage::load() previously discarded original tx_id when loading facts from disk, making time-travel queries on persisted databases incorrect

[0.3.0] - 2026-03-10

Added

  • Datalog core implementation with recursive rules
  • Entity-Attribute-Value (EAV) data model
  • Pattern matching with variable unification
  • Semi-naive evaluation for recursive rules
  • Transitive closure support with cycle handling
  • Rule registry for rule management
  • Persistent storage with postcard serialization
  • REPL with multi-line command support and comments
  • 123 comprehensive tests (94 unit + 26 integration + 3 doc)

Changed

  • Replaced GQL-inspired syntax with Datalog EDN syntax
  • Data model changed from property graph to EAV triples
  • Query executor rewritten for Datalog pattern matching

[0.2.0] - 2026-02-01

Added

  • Persistent storage backend with .graph file format
  • StorageBackend trait for platform abstraction
  • FileBackend implementation (4KB pages, cross-platform)
  • MemoryBackend for testing
  • PersistentGraphStorage layer for serialization
  • Embedded API (Minigraf::open(), Minigraf::execute())
  • Auto-save on drop

Changed

  • Graph storage now supports persistence

[0.1.0] - 2026-01-15

Added

  • Initial release
  • In-memory property graph implementation
  • Basic graph operations (nodes, edges, properties)
  • Interactive REPL
  • Thread-safe storage with Arc<RwLock<>>