diff --git a/ROADMAP.md b/ROADMAP.md index 344b785d7..2653c730d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1602,6 +1602,16 @@ Push `Expr` predicate clauses (e.g. `[(> ?age 30)]`) down to filter bindings as - Migration tools for any format changes - Long-term support +### v2.0 — File Format Policy + +**v2.0 supports file format v7 only.** Support for v1–v6 is dropped. The v1–v6 → v7 auto-migration code in `persistent_facts.rs` can be removed when cutting 2.0. Any database that was opened at least once under a v1.x release will already be on v7; there are no known users on older formats. + +v2.0 scope (GitHub milestone "2.0"): +- `lag`/`lead` window functions (#182) +- Sliding row frames — `:rows N preceding` (#183) +- PreparedQuery over UniFFI (#181) +- UDF registration over UniFFI (#180) + --- ## Decision Framework @@ -1664,6 +1674,7 @@ When evaluating features, ask: - ✅ Wave 1 Performance: Complete (May 2026) — hash-join cluster + selective B+Tree lookup (#202, #203, #204, #208), 850 tests - ✅ Wave 2 Optimizer & Benchmarks: Complete (May 2026) — predicate push-down (#207, #206), cost-based not/or ordering (#205), SIMD crossover analysis (#229), 850 tests - ✅ Wave 3 Reliability: Complete (May 2026) — WAL fault injection, migration matrix, index corruption resilience, property-based testing, coverage gates, long-haul smoke, XTDB/Datomic compat (#209, #210, #212, #213, #214, #215, #216, #217, #219, #220, #221), 935 tests +- ✅ Wave 4 Deferred Features: Complete (May 2026) — #182, #183, #180, #181 tagged milestone 2.0; #187 closed (no pre-1.0 users); #201 deferred to Wave 6 - 🎯 Phase 9: Ongoing (Ecosystem — integration examples, cookbook, GraphRAG/LangChain examples) **Note**: This is a hobby project. Timeline is flexible but realistic. @@ -1699,4 +1710,4 @@ See [GitHub Issues](https://github.com/project-minigraf/minigraf/issues) for spe --- -**Last Updated**: Wave 3 Reliability Complete (May 2026) — 935 tests passing, v1.0.0 +**Last Updated**: Wave 4 Complete (May 2026) — 935 tests passing, v1.0.0; milestone 2.0 issues queued; #231 repo-split is next gate diff --git a/docs/superpowers/plans/2026-05-16-wave3-pr4-query-correctness.md b/docs/superpowers/plans/2026-05-16-wave3-pr4-query-correctness.md index 77295b41c..de3988c9b 100644 --- a/docs/superpowers/plans/2026-05-16-wave3-pr4-query-correctness.md +++ b/docs/superpowers/plans/2026-05-16-wave3-pr4-query-correctness.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Add proptest-based cross-feature property tests with a reference evaluator (#221), Datalog parser/evaluator fuzz targets with seed corpus (#213), and CI coverage gates (#212). +**Goal:** Add proptest-based cross-feature property tests with a reference evaluator (#212), Datalog parser/evaluator fuzz targets with seed corpus (#213), and CI coverage gates (#219). **Architecture:** The reference evaluator is a naive nested-loop implementation (~150 lines) intentionally independent of production code. proptest generates random small graphs and queries; both evaluators are compared after normalizing output. Fuzz targets replace the stubs from PR 1. Coverage gates use the existing `llvm-cov.yml` pattern with per-module thresholds in `coverage-thresholds.toml`. @@ -10,7 +10,7 @@ **Prerequisites:** PR 1 merged (proptest dev-dep, fuzz/ stub targets) -**Closes:** #221, #213, #212 +**Closes:** #212, #213, #219 --- @@ -18,18 +18,18 @@ | Action | Path | Purpose | |---|---|---| -| Create | `tests/property_test.rs` | proptest reference evaluator (#221) | +| Create | `tests/property_test.rs` | proptest reference evaluator (#212) | | Modify | `fuzz/fuzz_targets/datalog_parser.rs` | real fuzz target (#213) | | Modify | `fuzz/fuzz_targets/datalog_eval.rs` | real fuzz target (#213) | | Create | `fuzz/corpus/datalog_parser/` | seed corpus (#213) | | Create | `fuzz/corpus/datalog_eval/` | seed corpus (#213) | -| Create | `.github/workflows/coverage-gates.yml` | per-module coverage CI (#212) | -| Create | `coverage-thresholds.toml` | threshold values (#212) | +| Create | `.github/workflows/coverage-gates.yml` | per-module coverage CI (#219) | +| Create | `coverage-thresholds.toml` | threshold values (#219) | | Modify | `.github/workflows/fuzz.yml` | add Datalog fuzz targets if file exists, else create | --- -## Task 1: proptest reference evaluator (#221) +## Task 1: proptest reference evaluator (#212) **Files:** - Create: `tests/property_test.rs` @@ -41,7 +41,7 @@ The reference evaluator handles only the small query shapes generated by the tes Create `tests/property_test.rs`: ```rust -//! Cross-feature property tests (#221). +//! Cross-feature property tests (#212). //! //! Compares Minigraf query results against a deliberately simple reference //! evaluator for randomly-generated small graphs and queries. @@ -322,7 +322,7 @@ Expected: all 3 proptest cases pass. If any fails, the output includes the minim ```bash git add tests/property_test.rs -git commit -m "test(query): add #221 proptest reference evaluator for cross-feature property tests" +git commit -m "test(query): add #212 proptest reference evaluator for cross-feature property tests" ``` --- @@ -547,7 +547,7 @@ git commit -m "test(fuzz): add #213 Datalog parser/eval fuzz targets with seed c --- -## Task 3: Coverage gates (#212) +## Task 3: Coverage gates (#219) **Files:** - Create: `coverage-thresholds.toml` @@ -704,7 +704,7 @@ Expected: all tests pass. ```bash git add coverage-thresholds.toml .github/workflows/coverage-gates.yml -git commit -m "ci: add #212 per-module coverage gates with cargo-llvm-cov" +git commit -m "ci: add #219 per-module coverage gates with cargo-llvm-cov" ``` --- @@ -716,11 +716,11 @@ git commit -m "ci: add #212 per-module coverage gates with cargo-llvm-cov" ```bash git push -u origin HEAD gh pr create \ - --title "test(query): proptest reference evaluator, Datalog fuzz targets, coverage gates (#221, #213, #212)" \ + --title "test(query): proptest reference evaluator, Datalog fuzz targets, coverage gates (#212, #213, #219)" \ --body "$(cat <<'EOF' ## Wave 3 PR 4 — Query Correctness -Closes #221, #213, #212. +Closes #212, #213, #219. ### #221 — proptest reference evaluator (`tests/property_test.rs`) - Naive reference evaluator (independent of production code) diff --git a/docs/superpowers/plans/2026-05-16-wave3-pr6-compat.md b/docs/superpowers/plans/2026-05-16-wave3-pr6-compat.md index b4af5687e..1e78fbf46 100644 --- a/docs/superpowers/plans/2026-05-16-wave3-pr6-compat.md +++ b/docs/superpowers/plans/2026-05-16-wave3-pr6-compat.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Add XTDB and Datomic compatibility test suites (#219) by porting semantically equivalent test cases from each corpus, with license review documented inline. +**Goal:** Add XTDB and Datomic compatibility test suites (#221) by porting semantically equivalent test cases from each corpus, with license review documented inline. **Architecture:** Two new test files — `tests/xtdb_compat_test.rs` and `tests/datomic_compat_test.rs`. Each test case is a semantic port of a known test from the source corpus. XTDB (Apache 2.0) allows direct semantic porting; Datomic test material is restricted so all cases are independently rewritten from the semantic intent. Unsupported or intentionally divergent cases are listed with rationale. @@ -10,7 +10,7 @@ **Prerequisites:** PRs 2, 3, 4, 5 all merged -**Closes:** #219 +**Closes:** #221 --- @@ -58,7 +58,7 @@ If the XTDB repo is inaccessible, the tests below are independently written sema Create `tests/xtdb_compat_test.rs`: ```rust -//! XTDB compatibility tests (#219). +//! XTDB compatibility tests (#221). //! //! License: XTDB is Apache 2.0. These tests are semantic ports of query //! concepts from the XTDB documentation and test suite. Each test is @@ -338,7 +338,7 @@ Expected: all tests pass. If any fail due to syntax differences (e.g., `:valid-a ```bash git add tests/xtdb_compat_test.rs -git commit -m "test(compat): add XTDB compatibility tests (#219)" +git commit -m "test(compat): add XTDB compatibility tests (#221)" ``` --- @@ -355,7 +355,7 @@ All Datomic-inspired tests are independently written semantic ports — they tes Create `tests/datomic_compat_test.rs`: ```rust -//! Datomic-inspired compatibility tests (#219). +//! Datomic-inspired compatibility tests (#221). //! //! License notice: Datomic is a commercial product. These tests are //! INDEPENDENTLY WRITTEN semantic ports — they test concepts from Datomic's @@ -630,7 +630,7 @@ Expected: all tests pass. ```bash git add tests/datomic_compat_test.rs -git commit -m "test(compat): add Datomic-inspired compatibility tests (#219)" +git commit -m "test(compat): add Datomic-inspired compatibility tests (#221)" ``` --- @@ -642,11 +642,11 @@ git commit -m "test(compat): add Datomic-inspired compatibility tests (#219)" ```bash git push -u origin HEAD gh pr create \ - --title "test(compat): XTDB and Datomic compatibility test corpus (#219)" \ + --title "test(compat): XTDB and Datomic compatibility test corpus (#221)" \ --body "$(cat <<'EOF' ## Wave 3 PR 6 — Compat Gate -Closes #219. +Closes #221. ### XTDB (`tests/xtdb_compat_test.rs`) - License: Apache 2.0 — semantic ports permitted @@ -678,8 +678,8 @@ After this PR merges, all 11 Wave 3 issues are closed: - ✅ #215 Migration matrix fixtures - ✅ #216 Index corruption recovery - ✅ #217 Concurrency stress tests -- ✅ #219 XTDB/Datomic compat corpus +- ✅ #221 XTDB/Datomic compat corpus - ✅ #220 Long-haul smoke suite -- ✅ #221 Cross-feature property tests +- ✅ #212 Cross-feature property tests Update `CLAUDE.md` test count and `CHANGELOG.md` with Wave 3 completion after all PRs merge. diff --git a/minigraf-c/src/lib.rs b/minigraf-c/src/lib.rs index 8a41aac1e..56ac992d5 100644 --- a/minigraf-c/src/lib.rs +++ b/minigraf-c/src/lib.rs @@ -203,11 +203,11 @@ fn value_to_json(v: &Value) -> serde_json::Value { fn query_result_to_json(result: QueryResult) -> String { let val = match result { - QueryResult::Transacted { tx_id, tx_count } => { - serde_json::json!({"transacted": tx_id, "tx_count": tx_count}) + QueryResult::Transacted(tx_id) => { + serde_json::json!({"transacted": tx_id}) } - QueryResult::Retracted { tx_id, tx_count } => { - serde_json::json!({"retracted": tx_id, "tx_count": tx_count}) + QueryResult::Retracted(tx_id) => { + serde_json::json!({"retracted": tx_id}) } QueryResult::Ok => serde_json::json!({"ok": true}), QueryResult::QueryResults { vars, results } => { diff --git a/minigraf-ffi/src/lib.rs b/minigraf-ffi/src/lib.rs index d2a41b070..643810b3a 100644 --- a/minigraf-ffi/src/lib.rs +++ b/minigraf-ffi/src/lib.rs @@ -120,11 +120,11 @@ fn value_to_json(v: &Value) -> serde_json::Value { fn query_result_to_json(result: QueryResult) -> String { use serde_json::json; let val = match result { - QueryResult::Transacted { tx_id, tx_count } => { - json!({"transacted": tx_id, "tx_count": tx_count}) + QueryResult::Transacted(tx_id) => { + json!({"transacted": tx_id}) } - QueryResult::Retracted { tx_id, tx_count } => { - json!({"retracted": tx_id, "tx_count": tx_count}) + QueryResult::Retracted(tx_id) => { + json!({"retracted": tx_id}) } QueryResult::Ok => json!({"ok": true}), QueryResult::QueryResults { vars, results } => { @@ -166,13 +166,9 @@ mod tests { #[test] fn query_result_to_json_transacted() { - let json = query_result_to_json(QueryResult::Transacted { - tx_id: 12345, - tx_count: 1, - }); + let json = query_result_to_json(QueryResult::Transacted(12345)); let v: serde_json::Value = serde_json::from_str(&json).expect("valid json"); assert_eq!(v["transacted"], serde_json::json!(12345)); - assert_eq!(v["tx_count"], serde_json::json!(1)); } #[test] @@ -239,13 +235,9 @@ mod tests { #[test] fn query_result_to_json_retracted() { - let json = query_result_to_json(QueryResult::Retracted { - tx_id: 99, - tx_count: 2, - }); + let json = query_result_to_json(QueryResult::Retracted(99)); let v: serde_json::Value = serde_json::from_str(&json).expect("valid json"); assert_eq!(v["retracted"], serde_json::json!(99)); - assert_eq!(v["tx_count"], serde_json::json!(2)); } #[test] diff --git a/minigraf-node/src/lib.rs b/minigraf-node/src/lib.rs index d8406b057..7c87ff2fd 100644 --- a/minigraf-node/src/lib.rs +++ b/minigraf-node/src/lib.rs @@ -22,11 +22,11 @@ fn value_to_json(v: &Value) -> serde_json::Value { fn query_result_to_json(result: QueryResult) -> String { let val = match result { - QueryResult::Transacted { tx_id, tx_count } => { - serde_json::json!({"transacted": tx_id, "tx_count": tx_count}) + QueryResult::Transacted(tx_id) => { + serde_json::json!({"transacted": tx_id}) } - QueryResult::Retracted { tx_id, tx_count } => { - serde_json::json!({"retracted": tx_id, "tx_count": tx_count}) + QueryResult::Retracted(tx_id) => { + serde_json::json!({"retracted": tx_id}) } QueryResult::Ok => serde_json::json!({"ok": true}), QueryResult::QueryResults { vars, results } => { diff --git a/src/browser/mod.rs b/src/browser/mod.rs index 42618a618..d6e07a5b9 100644 --- a/src/browser/mod.rs +++ b/src/browser/mod.rs @@ -329,11 +329,11 @@ fn query_result_to_json(result: QueryResult) -> String { use serde_json::{Value as JVal, json}; let val: JVal = match result { - QueryResult::Transacted { tx_id, tx_count } => { - json!({"transacted": tx_id, "tx_count": tx_count}) + QueryResult::Transacted(tx_id) => { + json!({"transacted": tx_id}) } - QueryResult::Retracted { tx_id, tx_count } => { - json!({"retracted": tx_id, "tx_count": tx_count}) + QueryResult::Retracted(tx_id) => { + json!({"retracted": tx_id}) } QueryResult::Ok => json!({"ok": true}), QueryResult::QueryResults { vars, results } => { diff --git a/src/db.rs b/src/db.rs index 8f3f8d1d3..381cfcfdb 100644 --- a/src/db.rs +++ b/src/db.rs @@ -509,9 +509,9 @@ impl Minigraf { // Return the same QueryResult the executor would have returned. if is_retract { - Ok(QueryResult::Retracted { tx_id, tx_count }) + Ok(QueryResult::Retracted(tx_id)) } else { - Ok(QueryResult::Transacted { tx_id, tx_count }) + Ok(QueryResult::Transacted(tx_id)) } } else { // Read-only: no lock needed @@ -575,6 +575,18 @@ impl Minigraf { Self::do_checkpoint(&self.inner.fact_storage, &mut ctx) } + /// Returns the current monotonic transaction counter. + /// + /// This is the value that `:as-of N` compares against. After a successful + /// [`execute`](Self::execute) that returns [`QueryResult::Transacted`] or + /// [`QueryResult::Retracted`], this reflects the count of that transaction. + /// + /// Starts at 0 for a new database and increments once per `transact`/`retract` call, + /// regardless of how many facts the batch contains. + pub fn current_tx_count(&self) -> u64 { + self.inner.fact_storage.current_tx_count() + } + /// Internal checkpoint logic (operates on an already-held write-lock guard). fn do_checkpoint(_fact_storage: &FactStorage, ctx: &mut WriteContext) -> Result<()> { match ctx { diff --git a/src/query/datalog/executor.rs b/src/query/datalog/executor.rs index a8d88405f..61f9c29a9 100644 --- a/src/query/datalog/executor.rs +++ b/src/query/datalog/executor.rs @@ -71,27 +71,21 @@ fn collect_all_patterns(clauses: &[WhereClause]) -> Vec { /// println!("{}: {:?}", vars[0], row[0]); /// } /// } -/// QueryResult::Transacted { tx_id, tx_count } => println!("tx {} (count {})", tx_id, tx_count), -/// QueryResult::Retracted { tx_id, tx_count } => println!("retracted tx {} (count {})", tx_id, tx_count), +/// QueryResult::Transacted(tx_id) => println!("tx {}", tx_id), +/// QueryResult::Retracted(tx_id) => println!("retracted tx {}", tx_id), /// QueryResult::Ok => {} /// } /// ``` #[derive(Debug, Clone, PartialEq)] pub enum QueryResult { - /// Transaction completed successfully. - Transacted { - /// Unix-millisecond timestamp of the transaction. - tx_id: TxId, - /// Monotonic counter (1, 2, 3 …). This is the value used by `:as-of N`. - tx_count: u64, - }, - /// Retraction completed successfully. - Retracted { - /// Unix-millisecond timestamp of the retraction. - tx_id: TxId, - /// Monotonic counter (1, 2, 3 …). This is the value used by `:as-of N`. - tx_count: u64, - }, + /// Transaction completed successfully. The inner value is the transaction ID + /// (Unix milliseconds). Use [`crate::db::Minigraf::current_tx_count`] to retrieve + /// the monotonic counter (`:as-of N` value) after a write. + Transacted(TxId), + /// Retraction completed successfully. The inner value is the transaction ID + /// (Unix milliseconds). Use [`crate::db::Minigraf::current_tx_count`] to retrieve + /// the monotonic counter (`:as-of N` value) after a write. + Retracted(TxId), /// Query results: list of variable bindings QueryResults { /// The variable names in the order they appear in the `:find` clause. @@ -269,12 +263,12 @@ impl DatalogExecutor { fact_tuples.push((entity_id, attribute, value, per_fact_opts)); } - let (tx_id, tx_count) = self + let (tx_id, _tx_count) = self .storage .transact_batch(fact_tuples, tx_opts) .map_err(|e| anyhow!("Transaction failed: {}", e))?; - Ok(QueryResult::Transacted { tx_id, tx_count }) + Ok(QueryResult::Transacted(tx_id)) } /// Execute a retract command: retract facts from storage @@ -299,12 +293,12 @@ impl DatalogExecutor { fact_tuples.push((entity_id, attribute, value)); } - let (tx_id, tx_count) = self + let (tx_id, _tx_count) = self .storage .retract(fact_tuples) .map_err(|e| anyhow!("Retraction failed: {}", e))?; - Ok(QueryResult::Retracted { tx_id, tx_count }) + Ok(QueryResult::Retracted(tx_id)) } /// Build a filtered fact snapshot for a query's temporal constraints. @@ -2349,9 +2343,8 @@ mod tests { let result = executor.execute(cmd).unwrap(); match result { - QueryResult::Transacted { tx_id, tx_count } => { + QueryResult::Transacted(tx_id) => { assert!(tx_id > 0); - assert!(tx_count > 0); } _ => panic!("Expected Transacted result"), } @@ -2488,9 +2481,8 @@ mod tests { let result = executor.execute(cmd).unwrap(); match result { - QueryResult::Retracted { tx_id, tx_count } => { + QueryResult::Retracted(tx_id) => { assert!(tx_id > 0); - assert!(tx_count > 0); } _ => panic!("Expected Retracted result"), } @@ -2516,7 +2508,7 @@ mod tests { let result = executor.execute(cmd).unwrap(); match result { - QueryResult::Transacted { .. } => {} + QueryResult::Transacted(_) => {} _ => panic!("Expected Transacted result"), } diff --git a/src/repl.rs b/src/repl.rs index 592ddd7e9..90c7f96f5 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -186,11 +186,11 @@ impl<'a> Repl<'a> { use crate::query::datalog::QueryResult as DResult; match result { - DResult::Transacted { tx_id, tx_count } => { - println!("✓ Transacted successfully (tx: {tx_id}, count: {tx_count})"); + DResult::Transacted(tx_id) => { + println!("✓ Transacted successfully (tx: {tx_id})"); } - DResult::Retracted { tx_id, tx_count } => { - println!("✓ Retracted successfully (tx: {tx_id}, count: {tx_count})"); + DResult::Retracted(tx_id) => { + println!("✓ Retracted successfully (tx: {tx_id})"); } DResult::QueryResults { vars, results } => { if results.is_empty() { @@ -453,19 +453,13 @@ mod tests { #[test] fn print_result_transacted() { use crate::query::datalog::QueryResult as DResult; - Repl::print_result(DResult::Transacted { - tx_id: 12345678, - tx_count: 1, - }); + Repl::print_result(DResult::Transacted(12345678)); } #[test] fn print_result_retracted() { use crate::query::datalog::QueryResult as DResult; - Repl::print_result(DResult::Retracted { - tx_id: 12345678, - tx_count: 2, - }); + Repl::print_result(DResult::Retracted(12345678)); } #[test]