8000 Merge main into v3: bench issue branch names (#566), Bencher window (#568), v2.0.5, v2.x fixes by adityamukho · Pull Request #569 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ jobs:
continue-on-error: true
run: |
set -o pipefail
# The t-test compares a run with the last 30 runs of its branch, not
# all history. Without a window, a correct step change (#445's
# predicate_pushdown, #476's checkpoint and wal_replay fixtures) kept
# alerting for weeks; with 30, a lasting change alerts on about five
# runs (enough for the two-in-a-row issue below), then becomes the
# baseline.
# A branch other than main starts from main's history and thresholds.
start_point=()
if [ "$BENCH_REF" != main ]; then
Expand All @@ -258,6 +264,7 @@ jobs:
--testbed ubuntu-latest \
--threshold-measure latency \
--threshold-test t_test \
--threshold-max-sample-size 30 \
--threshold-upper-boundary ${{ matrix.threshold }} \
--err \
--adapter rust_criterion \
Expand All @@ -266,10 +273,11 @@ jobs:

# Shared CI runners are noisy, so one alert on its own is usually a slow
# runner, not a regression. Report only benchmarks that also alerted in
# this suite on the previous run. Each suite has its own issue: if one is
# open, add a comment to it instead of skipping. Every outcome, including
# an alert that is not reported, goes in the job summary, because the
# Bencher step is continue-on-error and the run stays green (#393).
# this suite on the previous run. Each suite and branch has its own issue,
# titled with the branch like the nightly fuzz issue: if one is open, add
# a comment to it instead of skipping. Every outcome, including an alert
# that is not reported, goes in the job summary, because the Bencher step
# is continue-on-error and the run stays green (#393).
- name: Report regression
if: steps.bencher.outcome == 'failure'
uses: actions/github-script@v9
Expand All @@ -282,8 +290,7 @@ jobs:
const suite = process.env.SUITE;
const ref = process.env.BENCH_REF;
const jobName = `bench / ${suite}`;
// main keeps its existing issue titles; other branches get their own.
const label = ref === 'main' ? suite : `${suite}, ${ref}`;
const label = `${suite}, ${ref}`;

// Bencher lists each alert as "- <benchmark> (Latency): <.../alerts/<id>>".
// The "View results" lines use /perf? URLs, so they do not match.
Expand All @@ -300,6 +307,7 @@ jobs:
body = [
`Bencher failed in the nightly benchmark run (${label}) but listed no alerts. This is likely an API or upload error.`,
'',
`**Branch:** \`${ref}\``,
`**Run:** ${runUrl}`,
].join('\n');
} else {
Expand Down Expand Up @@ -358,6 +366,7 @@ jobs:
'',
...repeated.map(b => `- \`${b}\``),
'',
`**Branch:** \`${ref}\``,
`**Run:** ${runUrl}`,
`**Previous run:** ${previousUrl}`,
'',
Expand All @@ -371,12 +380,20 @@ jobs:
state: 'open',
per_page: 100,
});
const title = ref === 'main'
? `Benchmark regression: ${suite}`
: `Benchmark regression: ${suite} (${ref})`;
const existing = openIssues.find(i => !i.pull_request && i.title === title);
const title = `Benchmark regression: ${suite} (${ref})`;
// Issues for main used to be titled without the branch. Keep
// commenting on such an open issue, and give it the new title.
const legacyTitle = ref === 'main' ? `Benchmark regression: ${suite}` : null;
const existing =
openIssues.find(i => !i.pull_request && i.title === title) ||
openIssues.find(i => !i.pull_request && legacyTitle && i.title === legacyTitle);
let reported;
if (existing) {
if (existing.title !== title) {
await github.rest.issues.update({
owner, repo, issue_number: existing.number, title,
});
}
await github.rest.issues.createComment({
owner, repo, issue_number: existing.number, body,
});
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Weekly soak test at scale (#392).** `tests/soak_test.rs` grows a file-backed database to 10M live facts in batches of 1–2,000 facts (multi-valued tags and shard values in one transaction, strings over 64 bytes, refs), then churns it for the rest of a 5-hour budget at 50 transactions a second: retracts and re-asserts of random facts, and long retract/assert histories on 32 hot entities. It checkpoints every 20–200 transactions and reopens every 2–10 minutes, half from a checkpointed file and half replaying the WAL. Every 10 minutes, after every reopen, and between churn writes it checks samples against a reference that derives each entity's facts from its number and keeps only what was retracted: EAVT per entity, entity and attribute on the tags, AVET by unique name, AEVT over a sparse attribute, `:as-of` on hot entities, and the transaction counter; a full scan runs in a child process at 2M facts (so its memory does not hide a leak), and `verify()` at the end. It fails if RSS after a reopen grows past 1.25× the baseline plus 256 MiB. Metrics (file size, open and checkpoint time, RSS and peak RSS, p50/p99 per query path) go to a JSON-lines file that `.github/workflows/soak.yml` uploads weekly, alternating `main` and `v3`. `soak_short` runs the same driver at 20K facts in the per-PR suite. Found #518: attribute-and-value patterns scan the whole attribute.
- **Coverage gates that gate (#396).** The per-module gate in `coverage-gates.yml` had never checked anything: the instrumented test run failed (the lock tests could not find `pid_ns_helper`), the report it grepped was empty, and every module was skipped with a warning, so the job passed. It also read `cargo llvm-cov --text`, which has no per-file percentages, and named `btree_v6`, which v3 no longer has. The job now runs the whole suite with branch coverage on a pinned nightly, fails if any test fails, and checks each file's line and branch percentage against `coverage-thresholds.toml` with `scripts/coverage_gates.py`; a file missing from the report, or without branch data, fails. The thresholds file covers the WAL, `db.rs`, the fact log and log writer, every storage file, and the evaluator, executor and stratification, with floors at the measured values less one point. It also lists the paths left uncovered on purpose (invariant checks, browser-only and Windows-only paths). New tests cover the WAL's fact-size and fact-count limits (the old fact-size test changed bytes inside the fact and never reached the limit), a WAL opened in a missing directory, a failed cut of a torn WAL tail, short v7 pages and slots past the count, backup meta pages of another generation or length, a torn first meta with data in page 1, `save` with nothing pending, `into_backend` with pending facts, `rebuild_indexes` with nothing checkpointed, unsorted or repeated B+tree input, a stored key inserted with another value, a tree pointer to a page of another type, the read-only `MutexStorageBackend`, and two threads missing the same page in the page cache (before, only a lucky race reached that path). Branch coverage of `wal.rs` went from 83% to 95%, `packed_pages.rs` from 86% to 100%, `persistent_facts.rs` from 82% to 88%, and `btree.rs` from 85% to 85.5%.
- **Performance envelope at 100K–10M facts (#394).** `examples/perf_envelope.rs` builds and caches fixtures of 100K, 1M and 10M facts (six attributes per entity, a ref tree, two valid-time windows, an entity with 1,000 versions). It reports open and checkpoint times, RSS, and p50/p99 latency for twelve query shapes, each shape in its own process. It builds with a new `envelope` Cargo profile (`release` at opt-level 3, as an application compiles the library; `release` itself is opt-level "z"). `docs/BENCHMARKS.md` and the README publish the results: point lookups and single-entity time travel stay flat (12–14 µs at 10M facts). Scans, aggregations and joins grow linearly with the 8000 attributes they touch. Rule queries are supported to 100K facts only. A Python FFI round trip (`bench/envelope.py` in minigraf-python) adds about 70 µs per query. `.github/workflows/envelope.yml` runs 100K and 1M on demand, and a 10K smoke pass on PRs that change the harness. The nightly Criterion `query/*` groups gain 100K and 1M scales. Profiling at 1M facts found #528, #529, #530, #531 and #532.
- `tests/snapshot_atomicity_test.rs` races a writer against a reader: a query must see each transaction whole, whether written with `execute` or a `WriteTransaction` (#554), and must read one state for all its patterns (#555), in memory and on file. It also runs the v2.x checkpoint race from #558, which never failed on v3: copy-on-write checkpoints do not overwrite pages a published reader uses.

### Documentation

Expand All @@ -76,6 +77,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Same-transaction multi-valued facts can read back as one value (#371, #287), a fact retracted and re-asserted in one `WriteTransaction` is lost at commit (#477), a later assertion does not replace an earlier valid-time window (#435), and `save()` is not crash-atomic (#374); all are fixed in v3.0.0. All v2.x known issues, with affected versions and workarounds, are listed in the pinned issue #421.

## v2.0.5 — 2026-10-11

Patch release on the v2.x line under the [support policy](PHILOSOPHY.md#support-policy): two data-integrity fixes (#506, #513) and three bug fixes backported from v3.0.0 (#528, #457, #476), plus a weekly soak test and working coverage gates. File format is unchanged (v7). Two new error codes, `STG-045` and `WAL-007`, numbered as in v3.0.0; no other API changes. Upgrading is recommended for every v2.x user.

### Fixed

- **A file shorter than one page is refused at open with `STG-045` unless it is a torn first write (#506).** Open treated any file under 4 KiB as a new database and wrote a new header over it, so a database cut below one page by an interrupted copy, or an unrelated file at the path, was silently replaced. Such a file now opens as a new database only when its bytes are zeros or a prefix of the header a new database writes (what a crash during creation can leave); anything else is `STG-045` and the file is not modified. A zero-length file is still a new database. New code `STG-045`, numbered as in v3.0.0, which also fixes this.
- **A commit made after a torn WAL entry is no longer lost on recovery (#513).** Recovery replays the WAL up to its first partial or damaged entry, but the writer appended after the end of the file. After a crash during an append, the next session's commits went in after the partial entry, so a second crash before any checkpoint lost them, although each had been fsynced and reported as committed. A failed append (short write, full disk, I/O error) did the same to the next commit through that handle. Opening a database for writing now cuts the WAL back to the end of its last valid entry, and syncs it, before anything is appended. After a failed append, every later write through that handle fails with the new `WAL-007` until the database is reopened or a `checkpoint()` succeeds. The transaction whose append failed is not applied; after a reopen it is there in full or not at all. Affected every release since v0.5.0. Also fixed in v3.0.0.
- **Rule queries no longer fail with `INT-020` on databases with 1,000,000 or more live facts (#528).** The recursive evaluator counted the database's own facts against `max_results`, so once a database held that many live facts, every rule query failed at its first derived fact, whatever the size of its answer. `max_results` now counts only the facts the rules derive, so raising it with `OpenOptions::max_results` or `:max-results` is no longer needed. Affected every release since v0.18.0. Also fixed in v3.0.0.
- `checkpoint()` removes a WAL whose entries are all already in the file (#457). When it had no facts to flush, it returned early and left the WAL until a later write was checkpointed. That WAL is left by a crash between a checkpoint's commit and its WAL delete, or by a failed delete (`WAL-006`). Its contents were always skipped on replay, so no data was affected; the sidecar file just stayed. Also fixed in v3.0.0.
- Closing a handle opened with `wal_checkpoint_threshold(usize::MAX)` no longer writes its pending facts to the `.graph` file (#476). The sentinel was documented to suppress the close-time checkpoint, and `Minigraf`'s close did skip it, but the storage layer still saved on drop, leaving a WAL whose entries were all checkpointed. The file now stays as the last explicit `checkpoint()` left it, and the WAL keeps every pending fact for the next open to replay, so bulk loaders and benchmarks see the pending WAL they ask for. No data is lost either way. Also fixed in v3.0.0.

### Tests

- **Weekly soak test at scale (#392).** `tests/soak_test.rs` grows a file-backed database to 10M live facts in batches of 1–2,000 facts, then churns it for the rest of a 5-hour budget at 50 transactions a second (retracts, re-asserts, long histories on 32 hot entities), with a checkpoint every 20–200 transactions and a reopen every 2–10 minutes. Samples are checked against a reference through EAVT, entity and attribute, AVET, AEVT, `:as-of` and the transaction counter, with a full scan in a child process at 2M facts. RSS after a reopen is bounded relative to the fact versions held. Metrics go to a JSON-lines file that `.github/workflows/soak.yml` uploads weekly, alternating `main` (Saturday) and `v3` (Sunday). On v2.x no transaction writes two values of one entity and attribute, because of the known issue #371; the v3 workload fails here. It also checkpoints every 100–1,000 transactions and samples `:as-of` less, since both cost time in the database size on v2.x. `soak_short` runs the driver at 20K facts in the normal suite.
- **Coverage gates that gate (#396).** The per-module gate in `coverage-gates.yml` had never checked anything: the instrumented test run failed (the lock tests could not find `pid_ns_helper`), the report it grepped was empty, and every module was skipped with a warning, so the job passed. It also read `cargo llvm-cov --text`, which has no per-file percentages. The job now runs the whole suite with branch coverage on a pinned nightly, fails if any test fails, and checks each file's line and branch percentage against `coverage-thresholds.toml` with `scripts/coverage_gates.py`; a file missing from the report, or without branch data, fails. The floors are the measured values less one point, for the WAL, `db.rs`, `graph/storage.rs`, the storage files (except the v5 B+tree kept for migration) and the evaluator, executor and stratification. New tests check that WAL replay stops at an entry whose first fact is over 10 MB (the old test changed bytes inside the fact and never reached the limit) or that claims more than 1M facts, that a WAL writer cannot be opened in a missing directory, and that two threads missing the same page in the page cache get the one cached copy (before, only a lucky race reached that path).

### Known issues

- Same-transaction multi-valued facts can read back as one value (#371, #287), a fact retracted and re-asserted in one `WriteTransaction` is lost at commit (#477), a later assertion does not replace an earlier valid-time window (#435), `save()` is not crash-atomic (#374), and a `.wal` from a later checkpoint is replayed onto an older `.graph` (#535); all are fixed in v3.0.0

## v2.0.4 — 2026-10-08

Patch release on the v2.x line under the [support policy](PHILOSOPHY.md#support-policy): one `BrowserDb` durability hardening fix, plus new compatibility and crash tests. File format is unchanged (v7). No API changes. Native and WASI builds behave exactly as in v2.0.3.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Auto-migrates v7 → v8 on open (spec §9, with a backup meta page). v1–v6 are

## Test Coverage

**1441 tests** (1430 passing, 11 ignored; unit + integration + doc).
**1442 tests** (1431 passing, 11 ignored; unit + integration + doc).
See `docs/TEST_COVERAGE.md` for the full per-file breakdown.

**Testing conventions** — see the Testing Conventions section below before writing any tests.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ resolver = "2"

[package]
name = "minigraf"
version = "2.0.4"
version = "2.0.5"
edition = "2024"
rust-version = "1.89"
description = "Zero-config, single-file, embedded graph database with bi-temporal Datalog queries"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ let r2 = pq.execute(&[("tx", BindValue::TxCount(2)), ("entity", BindValue::Entit

```bash
cargo run # interactive Datalog REPL
cargo test # run 1441 tests
cargo test # run 1442 tests
cargo run < demos/demo_recursive.txt # recursive rules demo
```

Expand Down
15 changes: 14 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ Completed releases and their implementation details live in the [CHANGELOG](CHAN

---

## v2.0.5 — Data-Integrity Patch (released 2026-10-11)

File format stays v7. Two new error codes (`STG-045`, `WAL-007`); no other API changes. Details are in the [CHANGELOG](CHANGELOG.md).

Shipped:

- A file shorter than one page is refused with `STG-045` unless it is a torn first write (#506)
- A commit made after a torn WAL entry is no longer lost on recovery; a failed append makes later writes `WAL-007` until reopen or checkpoint (#513)
- Backported from v3.0.0: rule queries count only derived facts against `max_results` (#528), `checkpoint()` removes an all-checkpointed WAL (#457), `usize::MAX` checkpoint threshold skips the close-time save (#476)
- Weekly 10M-fact soak test (#392) and per-file coverage gates that actually gate (#396)

---

## v2.0.4 — Browser Durability Patch (released 2026-10-08)

File format stays v7 and the public API does not change. Details are in the [CHANGELOG](CHANGELOG.md).
Expand Down Expand Up @@ -206,4 +219,4 @@ See [GitHub Issues](https://github.com/project-minigraf/minigraf/issues) for spe

---

**Last updated**: October 2026 — v2.0.4, a browser durability patch on v2.x, is the current release; v3.0.0 work, including the file-format v8 fix for #287/#371, is developed on the long-lived `v3` branch and merges into `main` at the v3.0.0 cut.
**Last updated**: October 2026 — v2.0.5, a data-integrity patch on v2.x, is the current release; v3.0.0 work, including the file-format v8 fix for #287/#371, is developed on the long-lived `v3` branch and merges into `main` at the v3.0.0 cut.
6 changes: 3 additions & 3 deletions docs/TEST_COVERAGE.md

Large diffs are not rendered by default.

Loading
Loading
0