How Turso's Transaction State Machine Handles Isolation Levels
Turso's transaction state machine enforces snapshot isolation as its sole MVCC isolation level by assigning immutable begin timestamps to transactions, validating reads against multiversion chains, and detecting write-write conflicts eagerly before commit.
Turso is an edge-hosted distributed database that extends SQLite with a multiversion concurrency-control (MVCC) storage engine. While traditional SQL databases offer configurable isolation levels like READ COMMITTED or SERIALIZABLE, Turso's architecture simplifies concurrency control by implementing snapshot isolation exclusively for all MVCC-enabled transactions. The transaction state machine—comprising the Transaction and CommitStateMachine types—orchestrates this behavior through deterministic state transitions in core/mvcc/database/mod.rs.
Snapshot Isolation: The Only MVCC Level
Turso does not expose READ COMMITTED, REPEATABLE READ, or SERIALIZABLE as user-selectable isolation modes. Instead, every transaction initiated with BEGIN CONCURRENT operates under snapshot isolation semantics, backed by logical timestamp ordering rather than locking.
The TransactionState Enum
At the core of the state machine is the TransactionState enum. This tracks the transaction lifecycle through discrete phases:
enum TransactionState {
Active, // Initial state with assigned begin_ts
Preparing(u64), // Commit in progress, end_ts allocated
Committed(u64), // Final timestamp, visible globally
Aborted,
Terminated,
}
When a client executes BEGIN CONCURRENT, the system creates a Transaction instance in the Active state with a begin_ts drawn from the MvccClock defined in core/mvcc/mod.rs.
Enforcing Snapshot Isolation Properties
The state machine guarantees snapshot isolation through three coordinated mechanisms: immutable read snapshots, version visibility control, and eager conflict detection.
Consistent Snapshot Reads
When a transaction starts, it captures a begin timestamp (begin_ts) from the logical clock. All subsequent reads via MvStore::read inspect the version chain and return a row only if the transaction's begin_ts falls between the version's begin and end timestamps. This snapshot remains immutable for the transaction's lifetime.
In core/mvcc/database/mod.rs, the visibility check uses the TxTimestampOrID type to distinguish between committed timestamps and active transaction IDs:
// Simplified visibility logic from MvStore::read
if version.begin_ts <= tx.begin_ts && version.end_ts > tx.begin_ts {
return Ok(Some(version)); // Visible in snapshot
}
Preventing Dirty Writes
Write operations remain invisible to other transactions until commit completes. When a transaction modifies a row, MvStore::upsert, insert, or delete assigns the writer's TxID (rather than a timestamp) to the new version's begin field. The read path automatically filters out any version whose begin value is a TxID, effectively blocking dirty reads while allowing the writer to see its own uncommitted changes.
Eager Write-Write Conflict Detection
Turso detects conflicts immediately rather than at commit time. Before writing, the system checks if the target row has an uncommitted version via row_has_uncommitted_version_for_tx. If another active transaction has modified the row, the operation returns LimboError::WriteWriteConflict:
// From core/mvcc/database/mod.rs
if self.row_has_uncommitted_version_for_tx(&key, tx_id) {
return Err(LimboError::WriteWriteConflict);
}
This prevents serialization anomalies (G0 cycles) and eliminates the need for blocking locks during the commit phase.
The Commit State Machine
When COMMIT executes, the transaction transitions from Active to Preparing(end_ts), where end_ts is allocated from the MvccClock. The CommitStateMachine in core/mvcc/database/mod.rs then executes a deterministic sequence: Initial → Commit → WaitForDependencies → BuildLogRecord → RewriteLiveVersions → FinalizeCommit.
During the RewriteLiveVersions phase, temporary TxID markers in the version chain are atomically replaced with the final Timestamp, making the changes visible to transactions with higher begin_ts values.
Practical Transaction Lifecycle
The following example demonstrates how snapshot isolation behaves across concurrent connections:
// Transaction 1: Start snapshot-isolated transaction
let conn1 = db.connect();
conn1.execute("BEGIN CONCURRENT").unwrap(); // State: Active, begin_ts = 42
let rows = conn1.query("SELECT value FROM t WHERE id = 1", ()).unwrap();
assert_eq!(rows[0].get_int(0).unwrap(), 10); // Sees snapshot value
// Transaction 2: Attempt concurrent write to same row
let conn2 = db.connect();
conn2.execute("BEGIN CONCURRENT").unwrap();
let err = conn2.execute("UPDATE t SET value = 20 WHERE id = 1");
assert!(matches!(err, Err(LimboError::WriteWriteConflict))); // Eager conflict
// Transaction 1: Commit through state machine
conn1.execute("COMMIT").unwrap(); // Active → Preparing(43) → Committed(43)
// Transaction 3: Sees committed value with new snapshot
let conn3 = db.connect();
conn3.execute("BEGIN CONCURRENT").unwrap(); // begin_ts = 44
let rows = conn3.query("SELECT value FROM t WHERE id = 1", ()).unwrap();
assert_eq!(rows[0].get_int(0).unwrap(), 20); // Sees timestamped version
Fallback to SQLite Locking
Transactions started with plain BEGIN (without the CONCURRENT keyword) bypass the MVCC state machine entirely. These sessions use SQLite's native page-level locking, which behaves similarly to read committed isolation. Only transactions using BEGIN CONCURRENT participate in the snapshot isolation system.
Key Source Files
The implementation spans these critical paths in the tursodatabase/turso repository:
core/mvcc/database/mod.rs: DefinesTransaction,TransactionState,CommitStateMachine, and the MVCC read/write paths that enforce snapshot isolation.core/mvcc/database/hermitage_tests.rs: Comprehensive test suite validating snapshot isolation behavior including write skew and phantom prevention.core/connection.rs: Entry point forBEGIN CONCURRENTcommand parsing and transaction initialization.core/mvcc/mod.rs: ImplementsMvccClock,PackedTs, andTxTimestampOrIDauxiliary types.
Summary
- Turso's transaction state machine supports snapshot isolation exclusively for MVCC transactions; no other isolation levels are selectable.
- The
TransactionStateenum tracks atomic progress fromActivethroughPreparingtoCommitted. - Begin timestamps (
begin_ts) create immutable read snapshots for the transaction duration using theMvccClock. - Write-write conflicts are detected eagerly via
MvStoremethods that returnLimboError::WriteWriteConflictimmediately. - The
CommitStateMachinecoordinates commit ordering through deterministic state transitions ending withRewriteLiveVersions. - Non-concurrent transactions fall back to SQLite's page-level locking without MVCC semantics.
Frequently Asked Questions
Does Turso support the SERIALIZABLE isolation level?
No, Turso intentionally does not implement SERIALIZABLE, READ COMMITTED, or REPEATABLE READ as distinct isolation levels. The MVCC subsystem in core/mvcc/ provides snapshot isolation exclusively, which prevents write skew and phantom reads without the performance overhead of strict serializable schedules.
What happens when two transactions try to modify the same row?
The second transaction receives an immediate LimboError::WriteWriteConflict error when executing the write statement. This eager conflict detection occurs inside MvStore::upsert and related methods, preventing lost updates without requiring row locking or blocking waits.
How does Turso handle read-only transactions?
Read-only transactions remain in the Active state for their entire session. Since they never modify data, they do not invoke the CommitStateMachine, but they still retain their initial begin_ts snapshot to guarantee repeatable reads against the version chain stored in the MVCC engine.
What is the difference between BEGIN and BEGIN CONCURRENT?
BEGIN uses SQLite's traditional page-level locking and behaves like read committed isolation, while BEGIN CONCURRENT initiates an MVCC transaction with snapshot isolation. Only CONCURRENT transactions participate in the version chain system, timestamp-based visibility checks, and the TransactionState machine.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →