Skip to content

Commit 22f4c94

Browse files
authored
Update README.md (#2414)
docs: Clarify transaction/commit semantics with non-autocommit clients Adds a "Using AGE with Non-Autocommit Clients" section explaining PostgreSQL transaction visibility rules as they apply to AGE DDL-like functions (create_graph, create_vlabel, etc.), with broken/fixed psycopg v3 examples and a JDBC note. Refs #2195
1 parent 9f9d0f3 commit 22f4c94

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,105 @@ LOAD 'age';
215215
SET search_path = ag_catalog, "$user", public;
216216
```
217217

218+
<h2><img height="20" src="/img/contents.svg">&nbsp;&nbsp;Using AGE with Non-Autocommit Clients (psycopg, JDBC, etc.)</h2>
218219

220+
If you are using AGE from a database client that does **not** default to autocommit — most commonly `psycopg` v3 or JDBC — you must understand how PostgreSQL's transaction semantics apply to AGE's setup and DDL-like functions. Otherwise, you may see graphs or labels that appear to be created successfully, but are not visible from new connections.
221+
222+
This is **not** a bug in AGE — it is standard PostgreSQL behavior. AGE's DDL-like functions write to the catalog, and catalog writes only become visible to other sessions after the enclosing transaction is committed.
223+
224+
### What is and isn't transactional
225+
226+
| Statement | Scope | Needs commit to be visible elsewhere? |
227+
|---|---|---|
228+
| `LOAD 'age'` | Session-local (loads the .so into the current backend) | No |
229+
| `SET search_path = ag_catalog, "$user", public` | Session-local | No |
230+
| `SELECT create_graph('g')` | **Writes** to `ag_graph` and creates a schema | **Yes** |
231+
| `SELECT create_vlabel('g', 'L')` / `create_elabel(...)` | **Writes** to `ag_label` and creates a table | **Yes** |
232+
| `SELECT drop_graph('g', true)` / `drop_label(...)` | **Writes** to catalog | **Yes** |
233+
| `SELECT load_labels_from_file(...)` / `load_edges_from_file(...)` | **Writes** to catalog + data | **Yes** |
234+
| `cypher('g', $$ CREATE (:L {...}) $$)` | **Writes** data | **Yes** |
235+
236+
In a client that defaults to autocommit (e.g. `psql`), every statement commits automatically, so this is never noticed. In a non-autocommit client, the first statement you run implicitly opens a transaction that stays open until you call `commit()`, `rollback()`, or close the connection.
237+
238+
### psycopg v3 — the "savepoint gotcha"
239+
240+
The common pitfall is that `with connection.transaction():` in psycopg does **not** start a new top-level transaction when one is already open — it creates a **savepoint** inside the existing outer transaction. Releasing a savepoint is not a commit, so your `create_graph` write stays invisible to other sessions until the outer transaction is explicitly committed.
241+
242+
#### ❌ Broken: graph is not visible from a new connection
243+
244+
```python
245+
import psycopg
246+
247+
params = {"host": "localhost", "port": 5432, "user": "postgres",
248+
"password": "pw", "dbname": "mydb"}
249+
250+
# --- First connection ---
251+
conn = psycopg.connect(**params)
252+
conn.execute("LOAD 'age'") # implicitly opens a txn
253+
conn.execute("SET search_path = ag_catalog, '$user', public")
254+
255+
with conn.transaction(), conn.cursor() as cur: # <-- SAVEPOINT, not a real txn
256+
cur.execute("SELECT * FROM create_graph('my_graph')")
257+
# outer transaction is STILL OPEN here
258+
259+
conn.close() # outer transaction is rolled back on close → my_graph is gone
260+
261+
# --- New connection ---
262+
conn = psycopg.connect(**params)
263+
conn.execute("LOAD 'age'")
264+
conn.execute("SET search_path = ag_catalog, '$user', public")
265+
with conn.cursor() as cur:
266+
cur.execute("SELECT name FROM ag_graph;")
267+
# 'my_graph' is NOT in the results
268+
```
269+
270+
#### ✅ Fix 1: explicit `commit()` after setup
271+
272+
```python
273+
conn = psycopg.connect(**params)
274+
conn.execute("LOAD 'age'")
275+
conn.execute("SET search_path = ag_catalog, '$user', public")
276+
conn.commit() # <-- closes the implicit outer txn
277+
278+
with conn.transaction(), conn.cursor() as cur:
279+
cur.execute("SELECT * FROM create_graph('my_graph')")
280+
# this transaction block is now top-level and commits on exit
281+
conn.close()
282+
```
283+
284+
#### ✅ Fix 2: enable autocommit on the connection
285+
286+
```python
287+
conn = psycopg.connect(**params, autocommit=True)
288+
conn.execute("LOAD 'age'")
289+
conn.execute("SET search_path = ag_catalog, '$user', public")
290+
conn.execute("SELECT * FROM create_graph('my_graph')") # commits immediately
291+
conn.close()
292+
```
293+
294+
You can also toggle autocommit at runtime with `conn.set_autocommit(True)`.
295+
296+
### JDBC
297+
298+
JDBC connections also default to autocommit **true** per the JDBC spec, but many frameworks (Spring, etc.) flip it off. If you are running AGE DDL-like calls from JDBC, either:
299+
300+
```java
301+
connection.setAutoCommit(true);
302+
// ... LOAD 'age'; SET search_path ...; SELECT create_graph(...);
303+
```
304+
305+
or keep autocommit off and explicitly commit after DDL-like calls:
306+
307+
```java
308+
stmt.execute("LOAD 'age'");
309+
stmt.execute("SET search_path = ag_catalog, \"$user\", public;");
310+
stmt.execute("SELECT create_graph('my_graph');");
311+
connection.commit(); // make the graph visible to other sessions
312+
```
313+
314+
### Rule of thumb
315+
316+
> If an AGE call creates, drops, or modifies a graph, label, vertex, edge, or property, it is a **transactional write**. In a non-autocommit client, it will not be visible to other sessions until you explicitly `commit()`.
219317
220318
<h2><img height="20" src="/img/contents.svg">&nbsp;&nbsp;Quick Start</h2>
221319

0 commit comments

Comments
 (0)