Skip to content

feat(engine): Phase 11.4 BEGIN CONCURRENT writes + commit-time validation (SQLR-22)#125

Merged
joaoh82 merged 1 commit into
mainfrom
worktree-phase-11-4-begin-concurrent
May 10, 2026
Merged

feat(engine): Phase 11.4 BEGIN CONCURRENT writes + commit-time validation (SQLR-22)#125
joaoh82 merged 1 commit into
mainfrom
worktree-phase-11-4-begin-concurrent

Conversation

@joaoh82
Copy link
Copy Markdown
Owner

@joaoh82 joaoh82 commented May 10, 2026

Summary

The headline slice of Phase 11 — concurrent writes via MVCC + BEGIN CONCURRENT (SQLR-22). Multiple sibling Connections can each hold their own open BEGIN CONCURRENT transaction; commits validate against MvStore and abort with SQLRiteError::Busy on row-level write-write conflict. The four plan-required tests pass:

  • two concurrent inserts on disjoint rowids → both commit
  • two concurrent updates on the same rowid → one commits, the other aborts with Busy
  • aborted transaction's writes never become visible
  • retry-after-Busy succeeds

New mvcc::transaction::ConcurrentTx (per-Connection)

  • TxHandle (RAII registry entry, drops at COMMIT/ROLLBACK)
  • tables — working state, swapped with db.tables for the duration of each statement's executor pass
  • tables_at_begin — immutable BEGIN-time clone, used at COMMIT to derive the write-set without seeing other transactions' commits as bogus DELETEs
  • schema_at_begin — sorted table-name fingerprint

Connection wiring

  • New concurrent_tx: Option<ConcurrentTx> field — per-handle so N siblings can each carry their own open tx.
  • Connection::execute pre-parses for BEGIN CONCURRENT / COMMIT / ROLLBACK before sqlparser runs (sqlparser 0.61 has no Concurrent modifier; same intercept pattern as PRAGMA).
  • begin_concurrent: pre-conditions (journal_mode = mvcc, no active tx, not read-only) → ConcurrentTx::begin → store.
  • execute_in_concurrent_tx: rejects DDL via string-prefix check; std::mem::swap(db.tables, tx.tables); parks a dummy TxnSnapshot on db.txn to suppress auto-save; runs process_command; unwinds in reverse. Executor itself unchanged.
  • commit_concurrent: schema-unchanged check; diff tx.tables_at_begin vs tx.tables; validation walks MvStore for latest_committed_begin per row; if any > begin_tsBusy with rollback semantics. On success: tick clock for commit_ts; push committed versions; per-row apply (delete_row + restore_row) to db.tables; save_database for legacy WAL persistence.
  • rollback_concurrent: just self.concurrent_tx.take().

Error variants

  • SQLRiteError::Busy(String) — write-write conflict.
  • SQLRiteError::BusySnapshot(String) — reserved for snapshot-read anomalies in the 11.5 read-path integration; not emitted yet.
  • SQLRiteError::is_retryable() covers both — the contract SDK retry helpers will rely on.

MvStore::latest_committed_begin(row_id)

Returns the largest begin_ts of any committed version on the chain, or None for empty / in-flight-only chains. Used by commit validation.

Scope honesty (carried into 11.5)

  • Reads via Statement::query bypass the swap. query takes &self, so it can't mutate Connection state to perform the swap. Reads via Connection::execute("SELECT …") do see the BEGIN-time snapshot through the swap-based dispatch. Full MvStore-routed reads land when 11.5 catches up the read side.
  • MVCC writes persist only via the legacy Database::tables mirror. A crash mid-transaction loses the in-flight write-set (correct — the transaction never committed). 11.5 introduces an MVCC log-record WAL frame so committed writes land in the WAL through MvStore itself.
  • AUTOINCREMENT inside BEGIN CONCURRENT isn't specifically rejected; concurrent INSERTs that allocate the same rowid collide at the second commit's validation pass. The plan's "reject AUTOINCREMENT under MVCC" gate is a clean follow-up.
  • Tables touched by concurrent writes shouldn't carry FTS / HNSW indexesrestore_row only maintains B-tree secondary indexes. The four plan-required tests don't exercise FTS / HNSW.

Test plan

  • cargo build --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --all-targets — clean
  • cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks634/634 pass (15 new)
  • cargo clippy --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --all-targets — no new warnings
  • cargo fmt --all -- --check — clean
  • cargo doc --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --no-deps — no new warnings on changed files
  • No file-format or WAL-format change

New tests (15 total)

  • 4 ConcurrentTx struct tests in mvcc/transaction.rs — clone-isolation, schema fingerprint, registry registration / unregistration on drop.
  • 11 connection-level tests:

Followups (next sub-phases, separate PRs)

  • 11.5 — Checkpoint integration + crash recovery. Drains MvStore into the existing bottom-up B-tree rebuild path; introduces an MVCC log-record WAL frame format so concurrent commits become durable through MvStore directly. Also catches up the read path so Statement::query consults MvStore when journal_mode == Mvcc.
  • 11.6 — Garbage collection (per-commit + background). min_active_begin_ts watermark already lives in ActiveTxRegistry.
  • 11.7 — Indexes under MVCC (deferred-by-design — separate later phase).
  • 11.8 — SDK + REPL propagation of Busy / BusySnapshot; new .spawn REPL command + concurrent-writers benchmark workload.
  • 11.9 — Docs sweep — promote the plan to docs/concurrent-writes.md.

🤖 Generated with Claude Code

…tion (SQLR-22)

The headline slice of Phase 11. Multiple sibling Connections can
each hold their own open BEGIN CONCURRENT transaction; commits
validate against MvStore and abort with SQLRiteError::Busy on
row-level write-write conflict. The four plan-required tests
pass: disjoint inserts both commit, same-row updates collide and
one wins, aborted writes never become visible, retry-after-Busy
succeeds.

New `sqlrite::mvcc::transaction::ConcurrentTx` (per-Connection):
- TxHandle (RAII registry entry, drops at COMMIT/ROLLBACK)
- tables: HashMap<String, Table> — working state, swapped with
  db.tables for the duration of each statement's executor pass
- tables_at_begin: HashMap<String, Table> — immutable BEGIN-time
  clone, used at COMMIT to derive the write-set without seeing
  other transactions' commits as bogus DELETEs
- schema_at_begin: Vec<String> — sorted table-name fingerprint

Connection wiring:
- New concurrent_tx: Option<ConcurrentTx> field — per-handle so
  N siblings can each carry their own open tx.
- Connection::execute pre-parses for BEGIN CONCURRENT / COMMIT /
  ROLLBACK before sqlparser runs (sqlparser 0.61 doesn't have a
  Concurrent modifier; same intercept pattern as PRAGMA).
- begin_concurrent: pre-conditions (journal_mode = mvcc, no
  active tx, not read-only) → ConcurrentTx::begin → store.
- execute_in_concurrent_tx: rejects DDL via string-prefix check;
  std::mem::swap(db.tables, tx.tables); parks dummy TxnSnapshot
  on db.txn to suppress auto-save; runs process_command;
  unwinds in reverse. Executor itself unchanged.
- commit_concurrent: schema_unchanged check; diff_tables_for_writes
  against tx.tables_at_begin; validation walks MvStore for
  latest_committed_begin per row; if any > begin_ts → Busy with
  rollback semantics. On success: tick clock for commit_ts; push
  committed versions; per-row apply (delete_row + restore_row) to
  db.tables; save_database for legacy WAL persistence.
- rollback_concurrent: just self.concurrent_tx.take().

New error variants:
- SQLRiteError::Busy(String) — write-write conflict.
- SQLRiteError::BusySnapshot(String) — reserved for snapshot-read
  anomalies in the 11.5 read-path integration; not emitted yet.
- SQLRiteError::is_retryable() covers both. The contract SDK
  retry helpers will rely on.

MvStore::latest_committed_begin(row_id) — returns the largest
begin_ts of any committed version on the chain, or None for
empty / in-flight-only chains. Used by commit validation.

Known limitations carried to 11.5:
- Reads via Statement::query bypass the swap (query takes &self,
  can't mutate Connection state). Reads via execute("SELECT…") DO
  see the snapshot. Full MvStore-routed reads land when the
  read-side wiring catches up.
- MVCC writes persist only via the legacy Database.tables mirror.
  An MVCC log-record WAL frame is on the 11.5 list.
- AUTOINCREMENT inside BEGIN CONCURRENT isn't specifically
  rejected; the plan flags this.
- Tables touched by concurrent writes shouldn't carry FTS / HNSW
  indexes — restore_row only maintains B-tree indexes.

Test breakdown:
- 4 ConcurrentTx struct tests in mvcc/transaction.rs.
- 11 connection-level tests covering the four plan scenarios +
  essentials (begin requires mvcc; nested rejected; legacy BEGIN
  inside concurrent rejected; DDL rejected; empty commit never
  busies; is_retryable covers Busy variants).

634/634 workspace tests pass. fmt + clippy + doc clean on
changed files. No file-format or WAL-format change.

Docs: roadmap.md marks 11.3 ✅ and 11.4 🚧 with the limitations
called out. design-decisions.md gets a 12d entry on the
deep-clone snapshot + diff-against-BEGIN trade-offs.
supported-sql.md gets a `BEGIN CONCURRENT` reference with the
retry pattern. embedding.md gets a worked transfer() example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rust-sqlite Ready Ready Preview, Comment May 10, 2026 2:32pm

Request Review

@joaoh82 joaoh82 merged commit 22a5517 into main May 10, 2026
18 of 19 checks passed
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