feat(engine): Phase 11.4 BEGIN CONCURRENT writes + commit-time validation (SQLR-22)#125
Merged
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The headline slice of Phase 11 — concurrent writes via MVCC +
BEGIN CONCURRENT(SQLR-22). Multiple siblingConnections can each hold their own openBEGIN CONCURRENTtransaction; commits validate againstMvStoreand abort withSQLRiteError::Busyon row-level write-write conflict. The four plan-required tests pass:BusyBusysucceedsNew
mvcc::transaction::ConcurrentTx(per-Connection)TxHandle(RAII registry entry, drops at COMMIT/ROLLBACK)tables— working state, swapped withdb.tablesfor the duration of each statement's executor passtables_at_begin— immutable BEGIN-time clone, used at COMMIT to derive the write-set without seeing other transactions' commits as bogus DELETEsschema_at_begin— sorted table-name fingerprintConnection wiring
concurrent_tx: Option<ConcurrentTx>field — per-handle so N siblings can each carry their own open tx.Connection::executepre-parses forBEGIN CONCURRENT/COMMIT/ROLLBACKbefore sqlparser runs (sqlparser 0.61 has noConcurrentmodifier; 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 dummyTxnSnapshotondb.txnto suppress auto-save; runsprocess_command; unwinds in reverse. Executor itself unchanged.commit_concurrent: schema-unchanged check; difftx.tables_at_beginvstx.tables; validation walksMvStoreforlatest_committed_beginper row; if any >begin_ts→Busywith rollback semantics. On success: tick clock forcommit_ts; push committed versions; per-row apply (delete_row+restore_row) todb.tables;save_databasefor legacy WAL persistence.rollback_concurrent: justself.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_tsof any committed version on the chain, orNonefor empty / in-flight-only chains. Used by commit validation.Scope honesty (carried into 11.5)
Statement::querybypass the swap.querytakes&self, so it can't mutateConnectionstate to perform the swap. Reads viaConnection::execute("SELECT …")do see the BEGIN-time snapshot through the swap-based dispatch. FullMvStore-routed reads land when 11.5 catches up the read side.Database::tablesmirror. 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 throughMvStoreitself.AUTOINCREMENTinsideBEGIN CONCURRENTisn'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.restore_rowonly 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— cleancargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks— 634/634 pass (15 new)cargo clippy --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --all-targets— no new warningscargo fmt --all -- --check— cleancargo doc --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --no-deps— no new warnings on changed filesNew tests (15 total)
ConcurrentTxstruct tests inmvcc/transaction.rs— clone-isolation, schema fingerprint, registry registration / unregistration on drop.two_concurrent_inserts_on_disjoint_rows_both_commitcargo fmt#2two_concurrent_updates_same_row_one_aborts_with_busyaborted_transactions_writes_never_become_visibleretry_after_busy_succeedsbegin_concurrent_requires_mvcc_journal_modebegin_concurrent_then_empty_commit_round_tripsnested_begin_concurrent_is_rejectedlegacy_begin_inside_concurrent_is_rejectedddl_inside_begin_concurrent_is_rejectedempty_concurrent_commit_never_busiesis_retryable_covers_busy_variantsFollowups (next sub-phases, separate PRs)
MvStoreinto the existing bottom-up B-tree rebuild path; introduces an MVCC log-record WAL frame format so concurrent commits become durable throughMvStoredirectly. Also catches up the read path soStatement::queryconsultsMvStorewhenjournal_mode == Mvcc.min_active_begin_tswatermark already lives inActiveTxRegistry.Busy/BusySnapshot; new.spawnREPL command + concurrent-writers benchmark workload.docs/concurrent-writes.md.🤖 Generated with Claude Code