Feature
Explicit multi-statement transactions in the Python SDK, so that a group of
writes commits or rolls back as one.
Server support already shipped
The gate this issue originally named, Bolt BEGIN / COMMIT / ROLLBACK, is
the wrong one. The SDK speaks gRPC, and the gRPC API has carried interactive
transactions since server v0.5.5, which is the version this repository already
pins by digest in CI and in the demo stack:
BeginTransaction / CommitTransaction / RollbackTransaction on CypherService
ExecuteCypherRequest.transaction_id: a non-zero value runs that statement
inside the transaction, reading at the snapshot pinned at begin, with writes
buffered until commit
The pinned proto submodule already carries these and the generated stubs already
expose them, so nothing has to move on the server or the schema for this work.
Bolt (port 7082) is a stub with no listener and is unrelated.
API design
Context manager (preferred):
with client.transaction() as tx:
tx.cypher("CREATE (n:Person {name: $name})", params={"name": "Alice"})
tx.cypher("CREATE (n:Person {name: $name})", params={"name": "Bob"})
# commit on clean exit, rollback on exception
Explicit:
tx = client.begin_transaction()
try:
tx.cypher("MERGE (n:Entity {name: $name})", params={"name": "Alice"})
tx.commit()
except Exception:
tx.rollback()
raise
Both the sync and the async client get the same surface, since the async client
is the one the sync client wraps.
Semantics to honour
Read out of the server implementation rather than assumed:
- Snapshot pinned at begin. Every statement reads the snapshot taken when
the transaction began, so the transaction sees a stable view plus its own
buffered writes. Conflicts surface at commit, which can fail because another
transaction wrote the same data in the meantime.
- A failed statement aborts the whole transaction. The server drops the
buffered state on any statement error and consumes the handle, so a later
commit or rollback on that handle answers "unknown transaction id". The SDK
has to treat a server-side statement error as terminal and must not bury the
original exception under a second one from its own cleanup.
- Per-statement consistency options do not apply. The in-transaction path
ignores read concern, write concern, read preference and causal index,
because the snapshot was fixed at begin. Transaction.cypher() should decline
those arguments rather than accept and silently drop them.
- The handle is node-local. It lives in the memory of the process that
served the begin. Every statement and the commit must reach that same node, so
against a load balancer or a multi-replica deployment a transaction has to
hold one connection for its lifetime.
- Idle transactions are reaped. The default idle timeout is 30 seconds, and
reaping runs when some other transaction begins rather than on a timer, so a
long pause can lose the handle without a wall-clock guarantee of when.
Buffered writes are capped as well (256 MiB by default) and a breach aborts
the transaction.
Acceptance criteria
Feature
Explicit multi-statement transactions in the Python SDK, so that a group of
writes commits or rolls back as one.
Server support already shipped
The gate this issue originally named, Bolt
BEGIN/COMMIT/ROLLBACK, isthe wrong one. The SDK speaks gRPC, and the gRPC API has carried interactive
transactions since server v0.5.5, which is the version this repository already
pins by digest in CI and in the demo stack:
BeginTransaction/CommitTransaction/RollbackTransactiononCypherServiceExecuteCypherRequest.transaction_id: a non-zero value runs that statementinside the transaction, reading at the snapshot pinned at begin, with writes
buffered until commit
The pinned proto submodule already carries these and the generated stubs already
expose them, so nothing has to move on the server or the schema for this work.
Bolt (port 7082) is a stub with no listener and is unrelated.
API design
Context manager (preferred):
Explicit:
Both the sync and the async client get the same surface, since the async client
is the one the sync client wraps.
Semantics to honour
Read out of the server implementation rather than assumed:
the transaction began, so the transaction sees a stable view plus its own
buffered writes. Conflicts surface at commit, which can fail because another
transaction wrote the same data in the meantime.
buffered state on any statement error and consumes the handle, so a later
commit or rollback on that handle answers "unknown transaction id". The SDK
has to treat a server-side statement error as terminal and must not bury the
original exception under a second one from its own cleanup.
ignores read concern, write concern, read preference and causal index,
because the snapshot was fixed at begin.
Transaction.cypher()should declinethose arguments rather than accept and silently drop them.
served the begin. Every statement and the commit must reach that same node, so
against a load balancer or a multi-replica deployment a transaction has to
hold one connection for its lifetime.
reaping runs when some other transaction begins rather than on a timer, so a
long pause can lose the handle without a wall-clock guarantee of when.
Buffered writes are capped as well (256 MiB by default) and a breach aborts
the transaction.
Acceptance criteria
client.transaction()context manager commits on clean exitTransaction.cypher()runs inside the transaction rather than auto-committingclient.begin_transaction()/tx.commit()/tx.rollback()explicit APIAsyncCoordinodeClient