Transactions
Source: db/kv.go
PallasDB implements snapshot-isolated, optimistic MVCC transactions. Each transaction sees a consistent snapshot of the store at the time it was opened. Concurrent transactions can proceed in parallel; conflicts are detected only at commit time.
Transaction types

Both types support nested transactions: tx.NewTX() creates a child transaction whose updates are applied to the parent on commit rather than directly to the store.
Opening a transaction
tx := kv.NewTX()
NewTX() under kv.mu:
- Takes a snapshot of
kv.snapshot(a monotonically increasing counter). - Copies the memtable reference and SSTable list (copy-on-read - the slices are shared but immutable from the transaction's perspective).
- Constructs a
MergedSortedKVview:[tx.updates, memtable_snapshot, sstable_0, ...] - Registers the transaction in
kv.ongoing.
Reading in a transaction
val, ok, err := tx.Get(key)
Delegates to tx.levels.getExact(key) which scans levels newest-first. Tombstones (deleted=true) cause ok=false. The result reflects the state at the snapshot time plus any updates staged in tx.updates.
Range scans:
iter, err := tx.Range(start, stop, descending)
Returns a RangedKVIter that stops when the key moves past stop (ascending) or before stop (descending). Tombstones are filtered by NoDeletedIter.
Writing in a transaction
tx.Set(key, val) // upsert
tx.SetEx(key, val, mode) // ModeInsert, ModeUpdate, or ModeUpsert
tx.Del(key) // delete (stages a tombstone)
All mutations go into tx.updates (a SortedArray) and are invisible to other transactions until Commit().
Committing
err := tx.Commit()
Under kv.commit (a separate mutex from kv.mu):
-
Conflict check - if
tx.updatesis non-empty, compare each updated key againstkv.history. If any key was updated by another transaction after this transaction's snapshot, returnErrTXConflict. -
WAL write - iterate
tx.updates, write oneEntryAddorEntryDelper key, then writeEntryCommitand callSync(). -
Memtable update (under
kv.mu):- Fast path: if no other transactions are open, try
appendSortedArray(O(1) if updates sort after existing keys). - Merge path: build a new merged
SortedArrayfromMergedSortedKV{tx.updates, kv.mem}.
- Fast path: if no other transactions are open, try
-
History update - increment
kv.snapshotand record updated keys inkv.history(if other transactions are open and might conflict).
Conflict detection
kv.history is a log of (snapshot, key) pairs for every key written since the oldest open transaction's snapshot:
type UpdatedKey struct {
snapshot uint64
key []byte
}
A conflict occurs if any key in tx.updates appears in kv.history with a snapshot newer than tx.snapshot. This is a write-write conflict: another transaction committed a write to the same key after this transaction read it.
kv.history is pruned when transactions close: entries older than the oldest open transaction's snapshot are removed, as they can no longer cause conflicts.
Aborting
tx.Abort()
Removes the transaction from kv.ongoing and prunes kv.history. No WAL entry is written. The staged tx.updates are discarded.
Nested transactions
tx.NewTX() creates an inner transaction whose levels include the parent's tx.updates at the front:
inner := &KVTX{target: tx}
inner.levels = slices.Concat(MergedSortedKV{&inner.updates}, tx.levels)
On inner.Commit(), the inner updates are merged into tx.updates (not written to the WAL). On inner.Abort(), the inner updates are silently dropped. This is used by the SQL layer to implement statement-level atomicity within a larger transaction.
Synchronization
| Mutex | Protects |
|---|---|
kv.mu | kv.mem, kv.main, kv.snapshot, kv.history, kv.ongoing |
kv.commit | Serializes the WAL write + memtable update sequence |
kv.commit is held for the duration of the write pipeline, ensuring WAL entries are appended in commit order. kv.mu is held only briefly to update in-memory state after the WAL write completes.
UpdateMode
SetEx and KV.SetEx accept an UpdateMode:

If the mode's precondition is not met, updated=false is returned and no WAL entry is written.