# Transactions

> SmashTable for Python

### Optimistic Concurrency

`watch` makes a transaction fail rather than overwrite a key someone else touched first.
`ConflictError` is the retry signal, and the only exception a correct program is expected to catch routinely:

```python
while True:
    try:
        with st.transaction(accounts) as (view,):
            view.watch("alice")
            view["alice"] = view["alice"] - 100
        break
    except st.ConflictError:
        continue    # nothing was applied, so retrying is safe
```

### The Two Phases

`with` is exactly `begin()`, the body, `stage()`, `commit()`.
Both phases are also available directly:

```python
group = st.transaction(by_id, by_name)
ids, names = group.begin()
ids[42] = "carol"; names["carol"] = 42
group.stage()      # validates watches and reserves; still invisible, and may raise ConflictError
group.commit()     # flips visibility
```

Separating the fallible phase from the applying phase is what lets independent transactions compose into one all-or-nothing unit.
`stage` is where a conflict or an allocation failure normally surfaces.
Between the two the participants take no more writes: `ids[43] = 'dave'` after `stage()` raises `StateError`, since staging already reserved and validated what the transaction would change.
A read still answers.
`commit` validates again and can still refuse, because a watched key may be committed over while the group sits staged.
A group of two or more containers is asked in full before any of them writes, so a refusal publishes nothing and leaves the group staged and retryable.
A lone participant commits in one call, having nothing to tear against.
Every participant keeps what it validated held until it publishes, so nothing moves between the two passes and the second cannot refuse.

### Guarantees & Limits

- __A group stages in full or not at all.__
  A body that raises resets every participant.
  A stage that fails on one participant unwinds them all.
- __A commit publishes in full or not at all.__
  Every participant is asked before any of them writes, and a refusal leaves the group staged, where `rollback()` or another `commit()` picks it up.
- __Staged writes are invisible to everyone else__, including a transaction opened after the stage.
- __A transaction reads its own writes, until it stages.__
  `view[k]` sees what `view[k] = v` put there; after `stage()` the write is in the store carrying no stamp, so it is invisible to everyone including its own transaction until `commit()`.
- __Commit is not a snapshot, unless the containers share a commit order.__
  It applies each container in turn, so another thread reading two containers while a commit runs may find one of them a step ahead.
  A reader that needs the pair to agree should take its own transaction or read after the writer's block returns.
- __Scans are not snapshots either.__
  `scan()` walks in key order and never yields a key twice or raises mid-walk, but a key inserted behind the cursor is missed.
  That is the container's own `scan`; the same call on a transaction participant records the window it read, so at `serializable` and above a key committed into that window refuses the commit rather than being silently missed.

`import smashtable`

`from smashtable import Participant, Transaction`

## ConflictError

Base of the three ways an optimistic validation turns a transaction away.

```python
class ConflictError(SmashTableError, RuntimeError)
```

Never raised on its own - catch it to retry whichever of the three arrived.

## WriteConflictError

An earlier commit already wrote a key this transaction staged.

```python
class WriteConflictError(ConflictError)
```

## ReadConflictError

A key this transaction read was committed over before it could commit.

```python
class ReadConflictError(ConflictError)
```

## StateError

A transaction was asked for something its current state does not allow.

```python
class StateError(SmashTableError, RuntimeError)
```

## Participant

One container's slice of a transaction.

```python
class Participant
```

Reads see this transaction's own writes and the state as of when the group
opened, never another transaction's staged-but-uncommitted writes. Writes stay
invisible to every other reader until the transaction commits, and vanish if it does
not. A view stops working once its transaction finishes.

## get

Value for a key inside this transaction, or default when the key is absent.

```python
def get(key: _Key, default: Any = None, /) -> Any
```

Reads this transaction's own uncommitted writes, and otherwise the state as of
when the transaction opened. It does not see writes another transaction has staged but
not committed, so a read is never dirty.

Repeating a read is not guaranteed to return the same value: a committed write
from elsewhere becomes visible immediately. Call watch(key) to make the
transaction refuse to commit if that happens.

Raises: - `TypeError`: If key is not of this store's key type, or this is a set.

- [`StateError`](#StateError): If the transaction has already finished.

## add

Insert one member into a set participant, invisibly until the transaction commits.

```python
def add(member: _Key, /) -> None
```

Idempotent, and subject to the same visibility rule as upsert.

Raises: - `TypeError`: If this participant is a map, which needs a value.

- [`StateError`](#StateError): If the transaction has already finished.

## discard

Remove a key if it is there, reporting whether it was.

```python
def discard(key: _Key, /) -> bool
```

`del participant[key]` removes it too; this is the form that answers.

The removal is invisible outside the transaction until commit, and is undone by
rollback or by the block raising.

Raises: - `TypeError`: If key is not of this store's key type.

- [`StateError`](#StateError): If the transaction has already finished.

## update

Apply every pair of a mapping to this participant.

```python
def update(
    other: Mapping[_Key, Any] | Iterable[Any],
    /,
) -> None
```

Atomic, unlike SortedMap.update: every pair lands with the rest of the transaction or
none of them does. A failure part-way leaves the transaction abandonable by
rollback with nothing applied.

Raises: - `TypeError`: If other is not a mapping, or a key is of the wrong type.

- `ValueError`: If an element of other is not a pair.

- [`StateError`](#StateError): If the transaction has already finished.

## watch

Refuse to commit if another writer touches this key first.

```python
def watch(key: _Key, /) -> None
```

This is what turns read-modify-write into a lost-update-free operation. Without
it a transaction is atomic but not serializable: two transactions can each read the
same value, each write back, and one update is silently lost.

Covers absence as well as presence, so watching a key that does not exist still
conflicts if someone inserts it. The conflict surfaces at stage(), not at the
write, and nothing is applied when it does.

Raises: - [`ConflictError`](#ConflictError): At stage() time, never here.

- `TypeError`: If key is not of this store's key type.

- [`StateError`](#StateError): If the transaction has already finished.

## scan

List of (key, value) pairs in key order over [start, stop), or of members for a set.

```python
def scan(
    start: _Key | None = None,
    stop: _Key | None = None,
    *,
    limit: int | None = None,
) -> list[Any]
```

Sees this transaction's own staged writes, and otherwise the state as of when it
opened. Materialized in one span rather than lazily, because a walk records the
window it read and nothing else may run inside that record.

Under 'serializable' or stricter, a key another transaction commits into that
window makes this transaction's commit raise PhantomConflictError, rather than
let a repeat of the walk answer differently.

Raises: - `TypeError`: If a bound is not of this store's key type, or this participant is unordered.

- `ValueError`: If limit is negative.

- [`StateError`](#StateError): If the transaction has already finished.

## Transaction

A transaction of containers staged together and then published in turn.

```python
class Transaction
```

Either every store is published or none is: a body that raises resets each
participant, a stage that one refuses unwinds the rest, and a commit asks every
store before any of them writes. Publication itself
walks the containers in turn rather than moving them at one instant, so a thread
reading two of them while a commit runs may find one a step ahead - take a
transaction of your own if the pair must agree. No transaction ever reads
another's uncommitted writes. What more is promised depends on the store: its
isolation property reports the level, and below 'snapshot' a value already
read may change underneath, so a read-modify-write needs watch() to be safe.

Durability is memory-only: this is not a database and nothing survives the
process.

Used as a context manager, the block is exactly begin(), the body, stage(),
commit(); an exception discards everything instead.

## begin

Tuple of per-container views, in the order the stores were given.

```python
def begin() -> tuple[Participant, ...]
```

Participants stage in a process-wide canonical order rather than argument order,
which is what lets two groups naming the same containers in opposite orders run
concurrently without deadlocking. The views you receive ignore that and follow
your arguments.

Raises: - [`StateError`](#StateError): If the transaction has already finished.

## stage

Validate every watch and reserve the writes.

```python
def stage() -> None
```

Nothing becomes visible.

This is the first of the two phases. After it succeeds, commit() cannot fail for
a reason this transaction could have avoided. It can still refuse over a watched
key another transaction published while this one sat staged.

A partial stage is never observable: if one participant refuses, every other is
unwound before this returns.

Raises: - [`ConflictError`](#ConflictError): If a watched key changed since the transaction opened.

- [`StateError`](#StateError): If the transaction is not open - already staged, or finished.

## commit

Publish every staged change, or none of it.

```python
def commit() -> None
```

Every container is asked before any of them writes, so a refusal publishes
nothing and leaves the group staged, to be retried or rolled back.

Not a snapshot across containers. Each is applied in turn, so a thread reading
two of them while this runs may find one a step ahead of the other; a reader that
needs the pair to agree should take its own transaction.

Raises: - [`StateError`](#StateError): If the transaction was not staged first.

- [`ConflictError`](#ConflictError): If a watched key was published over while the group sat staged.

## rollback

Pull staged changes back out of the stores, leaving them as they were.

```python
def rollback() -> None
```

Valid only after stage() and before commit(). Since nothing staged was ever
visible, no reader can have observed what this undoes.

Raises: - [`StateError`](#StateError): If the transaction was not staged.

- [`SmashTableError`](https://www.unum.cloud/docs/smashtable/python/quickstart.md#SmashTableError): If a staged version was gone before the rollback reached
it, which is a broken invariant rather than a race a retry could win.

## reset

Discard everything pending and start the transaction over.

```python
def reset() -> None
```

Unlike rollback, valid at any point before the handle closes - open, staged or
already committed. This is what a retry loop calls after catching ConflictError.

## transaction

Open one transaction spanning every given store.

```python
def transaction(
    *stores: SortedMap | SortedSet | HashMap | HashSet = (),
) -> Transaction
```

Used as a context manager, the block either makes every change visible or none
of them. Stores may mix maps and sets, ordered and unordered, and may use
different key types and isolation levels.

- `stores`: One or more SortedMap, SortedSet, HashMap or HashSet, each at
most once. A store's own transaction() is this with one argument.

Raises: - `TypeError`: If no store was given, or one is not a store.

- `ValueError`: If the same store is given twice.

