diff --git a/README.md b/README.md index dad7f1032..819d9dcde 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,105 @@ LOAD 'age'; SET search_path = ag_catalog, "$user", public; ``` +

  Using AGE with Non-Autocommit Clients (psycopg, JDBC, etc.)

+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. + +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. + +### What is and isn't transactional + +| Statement | Scope | Needs commit to be visible elsewhere? | +|---|---|---| +| `LOAD 'age'` | Session-local (loads the .so into the current backend) | No | +| `SET search_path = ag_catalog, "$user", public` | Session-local | No | +| `SELECT create_graph('g')` | **Writes** to `ag_graph` and creates a schema | **Yes** | +| `SELECT create_vlabel('g', 'L')` / `create_elabel(...)` | **Writes** to `ag_label` and creates a table | **Yes** | +| `SELECT drop_graph('g', true)` / `drop_label(...)` | **Writes** to catalog | **Yes** | +| `SELECT load_labels_from_file(...)` / `load_edges_from_file(...)` | **Writes** to catalog + data | **Yes** | +| `cypher('g', $$ CREATE (:L {...}) $$)` | **Writes** data | **Yes** | + +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. + +### psycopg v3 — the "savepoint gotcha" + +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. + +#### ❌ Broken: graph is not visible from a new connection + +```python +import psycopg + +params = {"host": "localhost", "port": 5432, "user": "postgres", + "password": "pw", "dbname": "mydb"} + +# --- First connection --- +conn = psycopg.connect(**params) +conn.execute("LOAD 'age'") # implicitly opens a txn +conn.execute("SET search_path = ag_catalog, '$user', public") + +with conn.transaction(), conn.cursor() as cur: # <-- SAVEPOINT, not a real txn + cur.execute("SELECT * FROM create_graph('my_graph')") +# outer transaction is STILL OPEN here + +conn.close() # outer transaction is rolled back on close → my_graph is gone + +# --- New connection --- +conn = psycopg.connect(**params) +conn.execute("LOAD 'age'") +conn.execute("SET search_path = ag_catalog, '$user', public") +with conn.cursor() as cur: + cur.execute("SELECT name FROM ag_graph;") + # 'my_graph' is NOT in the results +``` + +#### ✅ Fix 1: explicit `commit()` after setup + +```python +conn = psycopg.connect(**params) +conn.execute("LOAD 'age'") +conn.execute("SET search_path = ag_catalog, '$user', public") +conn.commit() # <-- closes the implicit outer txn + +with conn.transaction(), conn.cursor() as cur: + cur.execute("SELECT * FROM create_graph('my_graph')") +# this transaction block is now top-level and commits on exit +conn.close() +``` + +#### ✅ Fix 2: enable autocommit on the connection + +```python +conn = psycopg.connect(**params, autocommit=True) +conn.execute("LOAD 'age'") +conn.execute("SET search_path = ag_catalog, '$user', public") +conn.execute("SELECT * FROM create_graph('my_graph')") # commits immediately +conn.close() +``` + +You can also toggle autocommit at runtime with `conn.set_autocommit(True)`. + +### JDBC + +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: + +```java +connection.setAutoCommit(true); +// ... LOAD 'age'; SET search_path ...; SELECT create_graph(...); +``` + +or keep autocommit off and explicitly commit after DDL-like calls: + +```java +stmt.execute("LOAD 'age'"); +stmt.execute("SET search_path = ag_catalog, \"$user\", public;"); +stmt.execute("SELECT create_graph('my_graph');"); +connection.commit(); // make the graph visible to other sessions +``` + +### Rule of thumb + +> 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()`.

  Quick Start