Optimistic locking

Optimistic locking prevents lost updates in concurrent systems without holding a database lock for the duration of a read-modify-write cycle. Each row carries a version counter; when you write back, alchemiq checks that the counter still matches what you read. If another process already incremented it, you receive a conflict exception instead of silently overwriting their change.


Enabling versioning

Set versioned = True in the model’s inner Meta class:

from alchemiq import Model
from alchemiq.types import PK

class Order(Model):
    id: PK[int]
    status: str
    total: int

    class Meta:
        versioned = True

Alchemiq injects a _version column (BIGINT, non-nullable, server_default 1 so the counter starts at 1) automatically, backed by SQLAlchemy’s native version_id_col mechanism. The single-underscore name avoids collisions with a business-level version field you might declare yourself.


Reading the current version

Use version_of() to read the _version value from a fetched instance:

from alchemiq import Repository, version_of
from myapp.models import Order

orders = Repository(Order)
order  = await orders.get(id=1)
ver    = version_of(order)   # e.g. 2

version_of() raises ConfigError if the model was not declared with Meta.versioned = True.


Conditional update and delete

Pass the version you observed as expected_version (keyword-only) to update() or delete(). Alchemiq checks it before the flush:

# Update - fails if the row was modified since order was fetched
await orders.update(1, expected_version=ver, status="paid")

# Delete - same guard applies
await orders.delete(1, expected_version=ver)

If the check fails, ConcurrentModificationError is raised and nothing is written. Map it to an HTTP 409 response:

from alchemiq import ConcurrentModificationError
from fastapi import HTTPException

try:
    await orders.update(1, expected_version=ver, status="paid")
except ConcurrentModificationError:
    raise HTTPException(status_code=409, detail="Order was modified by another request.")

Even without an explicit expected_version, SQLAlchemy’s native version_id_col increments _version on every flush and raises StaleDataError if the counter was already bumped by a concurrent transaction. Alchemiq translates that into ConcurrentModificationError as well.


Bulk operations bypass the version check

bulk_create(), filter().update(), and filter().delete() operate at the SQL level and do not enforce the version counter. Use single-row update() / delete() whenever optimistic concurrency matters.