From 2762cfcf2fe51857251789557681f8485af760ed Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Tue, 3 Nov 2026 01:35:29 +0900 Subject: [PATCH 1/2] Test and docs: a read-only transaction may commit on engines from 26.11.1 (#199) ArcadeDB PR #11124 (issue #11057) lets commit() and rollback() of a read-only transaction succeed; writes inside it still raise. The test now keeps the save() assertion strict and accepts either commit() outcome (the 26.11.1 snapshot spans both), checking the data is unchanged and no transaction stays active. The DatabaseFactory.open and open_database docstrings no longer say the commit raises. Co-Authored-By: Claude Opus 5.5 --- src/arcadedb/core.py | 12 ++++++---- tests/test_open_read_only.py | 46 +++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/arcadedb/core.py b/src/arcadedb/core.py index e4c13f3..08fb8e1 100644 --- a/src/arcadedb/core.py +++ b/src/arcadedb/core.py @@ -2465,8 +2465,10 @@ def open(self, read_only: bool = False) -> Database: read_only: Open in ``ComponentFile.MODE.READ_ONLY``. Reads, queries and scans work; every write and schema change raises ``ArcadeDBError``. ``begin()`` does not raise: the first write - does, and so does the commit. Use it to inspect a database that - must not change, such as a backup restored for audit. + does. From ArcadeDB 26.11.1 a read-only transaction commits or + rolls back without error; engines before that raise the + read-only error on ``commit()``. Use it to inspect a database + that must not change, such as a backup restored for audit. """ try: if read_only: @@ -2516,8 +2518,10 @@ def open_database( path: Database path jvm_kwargs: Optional JVM args passed to start_jvm() Example: {"heap_size": "8g"} - read_only: Open the database read-only. Writes, schema changes and - transactions raise ``ArcadeDBError``. + read_only: Open the database read-only. Writes and schema changes raise + ``ArcadeDBError``. ``begin()`` does not raise, and from ArcadeDB + 26.11.1 a read-only transaction commits or rolls back without + error (writes inside it still raise). Example: >>> db = arcadedb.open_database("./mydb", read_only=True) diff --git a/tests/test_open_read_only.py b/tests/test_open_read_only.py index 2dec928..a3adfcf 100644 --- a/tests/test_open_read_only.py +++ b/tests/test_open_read_only.py @@ -6,6 +6,9 @@ statement (see `test_sql_constraints_and_maintenance.py`). """ +import contextlib +import re + import pytest import arcadedb @@ -43,21 +46,52 @@ def test_read_only_open_reads_and_refuses_writes(populated_path): assert not db.schema.exists_type("Other") -def test_read_only_begin_is_not_refused_the_write_and_the_commit_are(populated_path): - """begin() returns on a read-only database; the first write raises, and so does the commit - (the engine starts the transaction then). A write through Document.save() is an - ArcadeDBError like the rest of the API. Reads need no transaction.""" +def test_read_only_begin_reads_and_end_cleanly_and_the_write_is_refused(populated_path): + """begin() returns on a read-only database; a write through Document.save() raises an + ArcadeDBError like the rest of the API, and reads need no transaction. + + Whether commit() of that read-only transaction raises depends on the engine. + Before ArcadeDB PR #11124 (issue #11057) it raised the read-only error. From + 26.11.1 on it returns, as does rollback(), because ending a transaction that + wrote nothing is not a write. The 26.11.1-SNAPSHOT line spans both behaviours, + so a version check cannot tell them apart: accept either outcome. Both must + leave the data unchanged and no transaction active. + """ with arcadedb.open_database(populated_path, read_only=True) as db: db.begin() # not refused assert db.count_type("Note") == 2 with pytest.raises(ArcadeDBError, match="(?i)read.?only"): db.new_document("Note").set("n", 3).save() - with pytest.raises(ArcadeDBError, match="(?i)read.?only"): - db.commit() + try: + db.commit() # engines from #11124 on: succeeds + except ArcadeDBError as e: # engines before #11124: the read-only error + assert re.search(r"(?i)read.?only", str(e)), str(e) + if db.is_transaction_active(): # the old engine may keep it open: end it + with contextlib.suppress(ArcadeDBError): + db.rollback() + assert not db.is_transaction_active() assert db.count_type("Note") == 2 assert db.query("sql", "SELECT n FROM Note").to_list() +def test_read_only_rollback_of_a_read_transaction(populated_path): + """rollback() of a read-only transaction: ArcadeDB PR #11124 (issue #11057) made it + succeed from 26.11.1. Engines before that may raise the read-only error; either way + the data is unchanged and no transaction stays active.""" + with arcadedb.open_database(populated_path, read_only=True) as db: + db.begin() + assert db.count_type("Note") == 2 + try: + db.rollback() + except ArcadeDBError as e: + assert re.search(r"(?i)read.?only", str(e)), str(e) + if db.is_transaction_active(): # the old engine may keep it open: end it + with contextlib.suppress(ArcadeDBError): + db.rollback() + assert not db.is_transaction_active() + assert db.count_type("Note") == 2 + + def test_factory_open_read_only_and_default_is_writable(populated_path): factory = arcadedb.DatabaseFactory(populated_path) From 31c552cfeb6864205eff3c03b1ce817f4f4ace50 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Tue, 3 Nov 2026 01:54:45 +0900 Subject: [PATCH 2/2] Test: a refused read-only commit leaves the transaction active on old engines (#199) The native parity leg (engine 26.10.2) failed both new tests: the old engine refuses commit() with "Cannot begin a transaction on a read only database", leaves the transaction active, and refuses the rollback after it the same way, so the is_transaction_active() assertion cannot hold there. The shared helper now asserts inactive only when the commit or rollback returns (26.11.1 and later) and, when it raises, only that the message names read-only. Co-Authored-By: Claude Opus 5.5 --- tests/test_open_read_only.py | 59 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/tests/test_open_read_only.py b/tests/test_open_read_only.py index a3adfcf..b649519 100644 --- a/tests/test_open_read_only.py +++ b/tests/test_open_read_only.py @@ -6,7 +6,6 @@ statement (see `test_sql_constraints_and_maintenance.py`). """ -import contextlib import re import pytest @@ -46,49 +45,49 @@ def test_read_only_open_reads_and_refuses_writes(populated_path): assert not db.schema.exists_type("Other") -def test_read_only_begin_reads_and_end_cleanly_and_the_write_is_refused(populated_path): - """begin() returns on a read-only database; a write through Document.save() raises an - ArcadeDBError like the rest of the API, and reads need no transaction. - - Whether commit() of that read-only transaction raises depends on the engine. - Before ArcadeDB PR #11124 (issue #11057) it raised the read-only error. From - 26.11.1 on it returns, as does rollback(), because ending a transaction that - wrote nothing is not a write. The 26.11.1-SNAPSHOT line spans both behaviours, - so a version check cannot tell them apart: accept either outcome. Both must - leave the data unchanged and no transaction active. +def _end_read_only_transaction(db, end): + """Call `end` (db.commit or db.rollback) on a read-only transaction. + + From 26.11.1 (ArcadeDB PR #11124, issue #11057) it returns and no transaction is + active afterwards. Engines before that refuse with the read-only error ("Cannot begin + a transaction on a read only database") and leave the transaction active, and a + rollback after the refused commit is refused the same way, so there is nothing to + clean up and no state to assert beyond the unchanged data. The 26.11.1-SNAPSHOT line + spans both behaviours, so a version check cannot tell them apart: accept either. """ + try: + end() + except ArcadeDBError as e: + assert re.search(r"(?i)read.?only", str(e)), str(e) + return False + assert not db.is_transaction_active() + return True + + +def test_read_only_begin_reads_and_the_write_is_refused_the_commit_is_either( + populated_path, +): + """begin() returns on a read-only database; a write through Document.save() raises an + ArcadeDBError like the rest of the API, and reads need no transaction. commit() of + that read-only transaction returns on engines from 26.11.1 and raises the read-only + error before (see `_end_read_only_transaction`).""" with arcadedb.open_database(populated_path, read_only=True) as db: db.begin() # not refused assert db.count_type("Note") == 2 with pytest.raises(ArcadeDBError, match="(?i)read.?only"): db.new_document("Note").set("n", 3).save() - try: - db.commit() # engines from #11124 on: succeeds - except ArcadeDBError as e: # engines before #11124: the read-only error - assert re.search(r"(?i)read.?only", str(e)), str(e) - if db.is_transaction_active(): # the old engine may keep it open: end it - with contextlib.suppress(ArcadeDBError): - db.rollback() - assert not db.is_transaction_active() + _end_read_only_transaction(db, db.commit) assert db.count_type("Note") == 2 assert db.query("sql", "SELECT n FROM Note").to_list() def test_read_only_rollback_of_a_read_transaction(populated_path): - """rollback() of a read-only transaction: ArcadeDB PR #11124 (issue #11057) made it - succeed from 26.11.1. Engines before that may raise the read-only error; either way - the data is unchanged and no transaction stays active.""" + """rollback() of a read-only transaction returns on engines from 26.11.1 and raises + the read-only error before; the data is unchanged either way.""" with arcadedb.open_database(populated_path, read_only=True) as db: db.begin() assert db.count_type("Note") == 2 - try: - db.rollback() - except ArcadeDBError as e: - assert re.search(r"(?i)read.?only", str(e)), str(e) - if db.is_transaction_active(): # the old engine may keep it open: end it - with contextlib.suppress(ArcadeDBError): - db.rollback() - assert not db.is_transaction_active() + _end_read_only_transaction(db, db.rollback) assert db.count_type("Note") == 2