8000 fix(query): read all of a query's lookups under one lock (#555) by adityamukho · Pull Request #557 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

fix(query): read all of a query's lookups under one lock (#555) - #557

Merged
adityamukho merged 4 commits into
v3from
fix/555-one-read-lock
Oct 11, 2026
Merged

adityamukho merged 4 commits into
v3from
fix/555-one-read-lock

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Refs #555 (data-integrity: stays open until a release ships the fix).

Stacked on #556 (#554): includes its commits until it merges; the change here is the last commit, 38c0054.

Bug

selective_fact_fetch called FactStorage::get_live_facts once per entity or attribute lookup, each under its own read lock. A transaction that committed between two lookups was seen by some patterns of the query and not others, so the query could return rows matching no state of the database.

Fix

FactStorage::get_live_facts_of_scans reads every scan under one read guard (pending facts then committed facts per scan, in the order given, as before). get_live_facts delegates to it; selective_fact_fetch builds its scan list and makes one call.

The other query paths already read storage once per query: full scans, :as-of timestamps, and rules (one filtered snapshot). or, not, not-join and prepared queries reuse that snapshot.

Reproducer correction

The #555 reproducer was only passing because of #554. execute added :pair/x before :pair/y, and scans run in sorted attribute order. So the query with :pair/x positive could only catch a partial commit, never a commit between two lookups. The test now also runs the query with :pair/y positive. Without this fix it failed in all 5 runs, usually within milliseconds; with it, it passes. Both variants (in-memory, file) are un-ignored.

Tests

  • New unit test live_facts_of_scans_concatenates_each_scan_in_order.
  • 1430 passing, 11 ignored. fmt and clippy clean.

Codex review: no findings.

🤖 Generated with Claude Code

https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h

adityamukho and others added 4 commits October 11, 2026 22:49
Ignored: they fail on v3 and main today. A concurrent query sees part of a
WriteTransaction commit, and two patterns of one query can read different
states. The checkpoint double-read case did not reproduce.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h
A commit wrote its WAL entry, then added the facts to FactStorage one
load_fact call at a time, each under its own write lock, so a query on
another thread could see part of the transaction. execute's transact and
retract path (since v0.6.0, 21b555a) did this as well as
WriteTransaction::commit (since v0.5.0).

FactStorage::load_facts appends a batch under one write lock. Commits,
WAL replay, BrowserDb's commit and the v7 in-memory load use it.

The two #554 reproducers are un-ignored, with execute variants added.

Refs #554

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h
selective_fact_fetch called FactStorage::get_live_facts once per entity
or attribute lookup, each under its own read lock, so a transaction that
committed between two lookups was seen by some patterns of the query and
not others. FactStorage::get_live_facts_of_scans reads every scan under
one read guard, and selective_fact_fetch uses it.

The #555 reproducer was passing only because of #554: execute added
:pair/x before :pair/y, and scans run in sorted attribute order, so the
query with :pair/x positive could only catch a partial commit. It now
also runs the query with :pair/y positive, which fails without this fix
(every one of 5 runs) and passes with it. Both are un-ignored.

Refs #555

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h
@codecov
codecov Bot commented Oct 11, 2026
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.29630% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/graph/storage.rs 95.55% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@adityamukho
adityamukho merged commit 7ec0094 into v3 Oct 11, 2026
24 checks passed
@adityamukho
adityamukho deleted the fix/555-one-read-lock branch October 11, 2026 18:23
q-qp-p pushed a commit to q-qp-p/minigraf that referenced this pull request Oct 19, 2026
…minigraf#554, project-minigraf#555)

Backport of the v3 fixes (PRs project-minigraf#556, project-minigraf#557) to v2.x.

project-minigraf#554: commits added a transaction's facts to FactStorage one load_fact
call at a time, each under its own write lock, so a query on another
thread could see part of the transaction. FactStorage::load_facts
appends a batch under one lock; execute, WriteTransaction::commit, WAL
replay and BrowserDb's commit use it.

project-minigraf#555: selective_fact_fetch read each entity/attribute lookup under its
own read lock, so a commit between two lookups was seen by some
patterns and not others. The three lookup bodies become functions over
&FactData and FactStorage::get_facts_for_lookups runs them all under one
read guard. The old methods remain for tests only.

The file-backed project-minigraf#555 reproducer disables checkpoints: on v2.x a query
that runs during a checkpoint can lose committed facts, a separate bug.

Refs project-minigraf#554, project-minigraf#555

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0