Transactions¶
with SnakeSession(driver, dialect) as session:
session.add(order)
session.add(line)
# commit on exit; rollback if anything raises
Or by hand, when you control the cycle yourself:
Every method the session offers is in Sessions.
Savepoints¶
To undo part of a transaction without losing the rest:
with SnakeSession(driver, dialect) as session:
session.add(order)
try:
with session.savepoint():
session.add(doubtful_line) # if this blows up...
except SnakeError:
pass # ...the order stays alive
session.commit()
They nest, and the session generates the names (sp1, sp2...).
Isolation levels¶
READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE. With
for_update() they are the two halves of concurrency control.
Two conditions the engines impose: call it before reading or writing (SET TRANSACTION is only
valid as the first statement), and not on SQLite, which has no isolation levels. There the call
is refused with a SnakeUnsupportedFeature that says why: SQLite has no
SET TRANSACTION ISOLATION LEVEL, one writer at a time makes its transactions serialisable already,
and its only knob —PRAGMA read_uncommitted— LOWERS the isolation instead of raising it.
Retrying a serialization conflict¶
With SERIALIZABLE, the engine aborts what it can't serialize. The correct response is to redo the
entire unit of work:
attempts=3 by default.
Why it takes a function, not a statement
When the engine aborts a transaction, the whole thing becomes unusable (current transaction
is aborted). Retrying the statement fixes nothing: you have to go back to the start with its
rollback in between. That's why with_retry takes the complete unit of work.
It recognises the transient conflict on all three engines. Anything else is raised straight away — repeating a constraint violation repeats the failure, and could duplicate side effects.
Writes that report what happened¶
user, created = session.get_or_create(
SnakeQuery(User).filter(User.email == "ana@x.com"),
lambda: User(email="ana@x.com", nickname="ana"),
)
if created:
send_welcome(user)
upsert writes, but doesn't tell you whether it created or the row already existed:
Reloading from the database¶
After a trigger or a server default that changed the row underneath you:
In production: wrapping the driver¶
from snakeorm import LoggingDriver, PostgresDialect, PsycopgDriver, TimeoutDriver
dialect = PostgresDialect()
driver = PsycopgDriver.connect(dsn)
driver = LoggingDriver(driver, write=print) # write(line: str)
driver = TimeoutDriver(driver, dialect, statement_timeout_ms=5000)
The order matters: the logger goes first so it also records what the wrappers above it do.
TimeoutDriver takes the dialect because the timeout statement is the engine's — and on an engine
that has none (SQLite) it refuses the wrap instead of pretending to cap.
The values do not go in the log¶
LoggingDriver writes the statement and the NUMBER of parameters, never the parameters themselves:
write=print sends that to the process stdout, which in a container is the log aggregator. The
statement is safe by construction — the ORM never interpolates, so nothing of the user's is in it —
and the values are the only thing that could be. To see one, name its position (0-based):
There is no environment variable for this, and the omission is the decision: an environment
variable is precisely the switch somebody flips in production by accident. It is the same policy,
spelled the same way, as the otel exporter's parameter_keys.
For pooling, see multiple connections.
Next: signals and triggers.