Transactions#
Transactions are psycopg’s, unchanged. A Cypher statement and a SQL statement are both ordinary statements to PostgreSQL, so they share a transaction like any two statements, and a graph write commits or rolls back together with the relational writes beside it.
The default: a transaction opens by itself#
A new connection is not in autocommit. The first statement opens a transaction, and it stays open until you end it:
conn.execute_query("CREATE (:movie {title: 'Tenet', year: 2020})")
conn.execute("INSERT INTO rating VALUES ('Tenet', 'critics', 7.3)")
conn.commit() # both, or with rollback() neither
Nothing is visible to another session before commit(). Closing the connection, or leaving a
with agensgraph.connect(...) block because of an exception, rolls back.
Because selecting a graph is a statement too, a rollback undoes conn.graph()
if it ran in the same transaction. Commit after selecting it.
Transaction blocks#
conn.transaction() makes a block a transaction of its own. It commits when the block ends, and
rolls back if the block raises:
with conn.transaction():
conn.execute_query("CREATE (:movie {title: 'Oppenheimer', year: 2023})")
conn.execute_query(
"MATCH (p:person {name: 'Christopher Nolan'}), (m:movie {title: 'Oppenheimer'})"
" CREATE (p)-[:directed]->(m)"
)
A block inside another block is a savepoint. When the inner block raises, only its own work is undone, and the outer block carries on:
with conn.transaction():
conn.execute_query("CREATE (:probe {n: 1})")
try:
with conn.transaction():
conn.execute_query("CREATE (:probe {n: 2})")
raise RuntimeError("changed my mind")
except RuntimeError:
pass
>>> conn.execute_query("MATCH (n:probe) RETURN n.n").records
[(1,)]
transaction(force_rollback=True) runs a block and then undoes it whatever happens, which is
useful in tests. The driver’s own read_only_transaction()
is built on the same mechanism.
Autocommit#
With autocommit=True every statement commits as soon as it finishes:
auto = agensgraph.connect("host=localhost dbname=docs user=agens", autocommit=True)
Some statements cannot run inside a transaction at all, such as CREATE DATABASE and VACUUM;
they need autocommit. A long-lived connection that mostly reads, such as one that waits for
notifications, is simpler in autocommit too, because nothing it does
holds a transaction open.
conn.transaction() still works in autocommit and opens a real transaction for the block. That is
the way to group several statements on such a connection, and the way to give
stream() the transaction it needs.
Change the mode with conn.autocommit = True or conn.set_autocommit(True), between transactions.
Isolation#
The isolation level is psycopg’s setting, applied to each transaction the connection opens:
import psycopg
conn.commit() # settings like this one change between transactions
conn.isolation_level = psycopg.IsolationLevel.SERIALIZABLE
psycopg refuses to change it, or read_only or autocommit, while a transaction is open.
Concurrent writes to the same elements under REPEATABLE READ or SERIALIZABLE are refused with
SQLSTATE 40001, a serialization failure. That is the server’s ordinary answer to a conflict, and
it is safe to run the transaction again: Retries covers how to decide
and how long to wait.
Read-only transactions#
conn.read_only = True makes every transaction the connection opens read-only. The server then
refuses every write, whatever language it is written in:
>>> conn.read_only = True
>>> conn.execute_query("CREATE (:movie {title: 'Not today'})")
Traceback (most recent call last):
...
agensgraph.errors.ReadOnlyGraphWrite: cannot write to a graph in a read-only transaction
>>> conn.rollback()
>>> conn.read_only = False
The server reports a refused graph write with a code it shares with every other refused write and
a message that varies between releases, so the driver raises a class of its own for it,
ReadOnlyGraphWrite. It is a subclass of psycopg’s ReadOnlySqlTransaction, so code catching the
psycopg class keeps working. A refused SQL write, such as an INSERT, is psycopg’s
ReadOnlySqlTransaction itself.
To run one statement you did not write, such as a query from a language model, use
read_only_transaction(), which adds the checks a
read-only transaction alone does not make.
Transaction ids#
conn.transaction_id() returns the id of the open transaction, as an int, once it has one. A
transaction gets an id when it first writes, so a read-only one has none:
with conn.transaction():
before = conn.transaction_id()
conn.execute_query("CREATE (:probe {n: 3})")
after = conn.transaction_id()
>>> before is None, isinstance(after, int)
(True, True)
transaction_id(assign=True) gives the transaction an id at once. Keeping the id is what makes it
possible to find out, from another connection, whether a commit whose answer never arrived took
effect; see Losing a connection while committing.
Two-phase commit#
psycopg’s two-phase commit methods work unchanged, including for a graph write:
conn.graph("moviekb")
conn.commit() # start with no transaction open
conn.tpc_begin(conn.xid(0, "order-7", "graph"))
conn.execute_query("CREATE (:movie {title: 'Prepared'})")
conn.tpc_prepare() # durable, but not yet visible
conn.tpc_commit() # or tpc_rollback()
tpc_begin() must be called with no transaction open, so commit after selecting the graph.
Between tpc_prepare() and the commit, the transaction survives the connection: another
connection lists it with tpc_recover() and can finish it with tpc_commit(xid).
The server allows no prepared transactions unless max_prepared_transactions is above zero, and
the default is zero. On such a server tpc_prepare() fails:
psycopg.NotSupportedError: prepared transactions are disabled