Function: isvisible in snapshot-isolation/mvcc_database.py

Date: 2026-05-29

Time: 09:58

isvisible(self, tx, version) — MVCC Snapshot Visibility Check

Purpose

This is the core visibility function of the snapshot isolation implementation. It answers: "Should transaction tx be able to see this particular version of a record?"

In MVCC (Multi-Version Concurrency Control), every write creates a new version rather than overwriting in place. Multiple transactions can be in flight simultaneously, each seeing a consistent snapshot of the database as it existed when they started. isvisible enforces this illusion by filtering which versions each transaction can observe.

Without this function, transactions would see uncommitted writes from other transactions (dirty reads), partially-committed state, or data that was deleted after their snapshot was taken.

Contract

Preconditions:

Postconditions:

Invariant: A version is visible if and only if:

1. Its creating transaction is visible to tx, AND

2. It has not been deleted by a transaction visible to tx

Parameters

| Parameter | Type | Description |

|-----------|------|-------------|

| tx | Transaction | The observing transaction. Its txid, activeat_start set determine what snapshot it sees. |

| version | Version | The candidate version being checked. Has createdby and deletedby fields referencing transaction IDs. |

Edge cases:

Return Value

boolTrue if the version is visible to tx, False otherwise.

The caller (read, delete, scan) uses this to filter the version list. If multiple versions of the same key are visible, the caller takes the last one (latest visible).

Algorithm

The function has two phases: creation visibility and deletion visibility.

Phase 1: Is the creating transaction visible?


created_by = version.created_by

Step 1 — Aborted check: If the creator was aborted, return False immediately. Aborted transactions' effects must never be seen by anyone.

Step 2 — Self-write check: If createdby == tx.txid, the transaction sees its own writes. This is essential — without it, a transaction couldn't read back what it just wrote.

Step 3 — Committed-and-in-snapshot check: If the creator committed, it's visible only if ALL three conditions hold:

1. createdby in self.committed — the creator actually committed

2. createdby not in tx.activeat_start — the creator was not still running when tx started (if it was active at our start, it committed *after* our snapshot was taken)

3. createdby < tx.txid — the creator's ID is lower than ours, meaning it started before us

Condition 3 catches transactions that both started and committed in the gap between tx recording its snapshot and the current moment. A transaction with a higher ID started after us, so even if it committed quickly, its writes shouldn't be in our snapshot.

Step 4: If none of these made created_visible = True, return False. The version was created by a still-active or future transaction.

Phase 2: Has a visible transaction deleted this version?

If we reach this phase, the creation is visible. Now check whether a deletion has also become visible.

Step 5 — No deletion: If deleted_by is None, the version exists and is visible. Return True.

Step 6 — Self-deletion: If deletedby == tx.txid, this transaction deleted this version. It should not see it. Return False.

Step 7 — Aborted deletion: If the deleter was aborted, the deletion never happened. The version is still visible. Return True.

Step 8 — Committed deletion: Apply the same three-way visibility test as creation: if the deleting transaction committed, was not active at our start, and has a lower ID, the deletion is in our snapshot. Return False.

Step 9 — Fallback: The deleting transaction is still in-flight or started after us. We can't see the deletion. Return True.

The symmetry is intentional: a creation is visible under the same rules as a deletion. The function applies the snapshot visibility rule twice — once to decide "does this version exist?" and once to decide "has it been removed?"

Side Effects

None. This is a pure query function. It reads from self.committed, self.aborted, tx.activeatstart, and tx.tx_id but mutates nothing.

Error Handling

No exceptions are raised or caught. The function assumes all inputs are valid. Specifically:

Usage Patterns

Called internally by three methods:

Caller obligation: the caller must have already verified the transaction is active. isvisible does not check this.

Dependencies

Assumptions Not Enforced by Types

1. Transaction IDs are monotonically increasing — the createdby < tx.txid check relies on this to mean "started before us." If IDs were reused or non-monotonic, the visibility logic would break.

2. activeatstart is an accurate snapshot — if this set is wrong, the snapshot is wrong. Nothing enforces that begin_transaction populated it correctly.

3. A transaction is in exactly one of: active, committed, aborted — the function checks committed and aborted as disjoint sets. If a tx_id appeared in both, behavior would be inconsistent (the aborted check runs first, so aborted would win).

4. created_by always refers to a valid transaction — there's no check for unknown transaction IDs; they'd silently fall through as "not visible."

Topics to Explore

Beliefs