Skip to content

fix(plugin-oracle): commit Oracle statements the way every other engine does - #3001

Merged
datlechin merged 2 commits into
mainfrom
fix/oracle-autocommit
Sep 20, 2026
Merged

datlechin merged 2 commits into
mainfrom
fix/oracle-autocommit

Conversation

@datlechin

Copy link
Copy Markdown
Member

What was wrong

Oracle has no autocommit of its own. Every write opens a transaction that stays open until something commits it, and oracle-nio sends no commit unless the statement asks for one (StatementOptions.autoCommit, default false, OracleTask.swift:268 at the pinned f09d088). TablePro never asked. So on Oracle nothing committed unless the app happened to wrap the work in its own transaction and send a COMMIT:

Path Before
macOS editor, one statement (Cmd+Enter) never committed
MCP execute_query never committed
macOS Run All, two or more statements committed by the app's COMMIT, together with any earlier uncommitted statement
macOS grid save, import, Compare & Sync, structure changes committed (app transaction, COMMIT from the PluginKit default)
Table transfer with Wrap each table in a transaction off, Compare & Sync with Run in a transaction off or Skip and continue never committed (read from the code, not run)
iOS query editor (executeStreaming) never committed
iOS row save, insert, delete (one statement through executeWrite) never committed: WriteTransactionPolicy opens no transaction for one statement when the state is .unknown
iOS multi-row write committed

An uncommitted write is invisible to every other session, keeps its row locks, and is decided later by whatever runs next on that session: a later Run All or grid save commits it silently, a failed Run All's ROLLBACK throws it away, a graceful disconnect commits it, and a dropped network loses it. Neither app has a Commit button or a transaction indicator, and every other engine in TablePro commits each statement as it runs.

Measured (Oracle AI Database 26ai Free 23.26, pinned oracle-nio f09d088)

Before, through OracleCoreConnection exactly as the plugin and the iOS driver call it, with a second session reading:

  • streamQuery("INSERT …") (iOS editor path): session A counts 1, session B counts 0.
  • executeQuery("INSERT …") (macOS single statement, MCP, iOS execute): A counts 2, B counts 0.
  • A second session's SELECT … FOR UPDATE NOWAIT on a pending row cannot see it at all.
  • A later failed Run All (empty begin, ROLLBACK): A's count went from 2 to 0, the two earlier single-statement inserts gone.
  • A later successful Run All: its COMMIT also committed an earlier single-statement insert (B 0 to 2).
  • A graceful disconnect commits pending work (B 2 to 3).
  • A query timeout while pending work is held: B reads 0 at +2 s and 1 at +5 s, once the in-flight statement ended and the logoff committed it.

oracle-nio's commit flag, raw:

  • autoCommit: true on an INSERT is visible to another session at once; on a PL/SQL block that inserts 1000 rows, the same; a streamed UPDATE still reports its count (3).
  • SELECT of 1001 rows with autoCommit: true: fine. SELECT … FOR UPDATE of 1001 rows with autoCommit: true: ORA-01002: fetch out of sequence, because the commit ends the transaction the cursor belongs to.
  • SAVEPOINT with autoCommit: true then ROLLBACK TO: ORA-01086. SET TRANSACTION READ ONLY with autoCommit: true: DBMS_TRANSACTION.LOCAL_TRANSACTION_ID is NULL right after, so the transaction is gone.
  • Without the flag: SET TRANSACTION, SAVEPOINT alone, and LOCK TABLE each leave LOCAL_TRANSACTION_ID set; ROLLBACK TO keeps it; COMMIT WORK and ROLLBACK WORK clear it; a failed DML with the flag leaves it NULL; CREATE TABLE commits the open transaction.
  • ALTER SESSION SET CURRENT_SCHEMA hangs on this build with and without the flag (2 of 2 each, 10 s cap). That is the nio PR's defect and the flag does not change it.

What changed

One owner, in the package both apps compile: TableProOracleCore.

  • OracleSessionTransaction decides per statement whether oracle-nio's commit flag is sent. Outside a transaction every statement carries it and the server commits in the same round trip, as every other engine in the app behaves. A query never carries it, because the flag breaks a multi-fetch SELECT … FOR UPDATE.
  • A transaction opens with OracleCoreConnection.beginTransaction(), which is what beginTransaction() on both drivers now calls (it was an empty body on both), or with a statement that only means something inside one: SET TRANSACTION, SAVEPOINT, LOCK TABLE (OracleTransactionRole). It ends with COMMIT or a full ROLLBACK, the app's or the user's, so commitTransaction() and rollbackTransaction() keep sending the plain statements.
  • The transaction is bound to the connection its first write ran on. A write that finds that connection replaced by a reconnect fails with a new transactionLost error and runs nothing, instead of carrying on in a fresh session holding none of the earlier work and letting the app's COMMIT report the half it saw as the whole.
  • Both drivers answer sessionTransactionState() from it (.inTransaction/.explicitTransaction or .idle), so a batch, a grid save or an iOS row edit joins a transaction the user or an MCP client opened instead of committing it, as they already do on PostgreSQL and MySQL.
  • BatchTransactionPolicy: SET TRANSACTION in an Oracle script makes it a script that manages its own transaction, the way BEGIN does elsewhere (TransactionEngineFamily.oracle, setTransactionOpensTransaction). savepointOpensTransaction now includes Oracle, measured.

No PluginKit change. sessionTransactionState() already exists in the pending kit 33 with a default, and the Oracle plugin is already stamped 33.

Tests

  • OracleTransactionRoleTests, OracleSessionTransactionTests (package, swift test): which statements carry the flag, open, end, and when a replaced connection reports the transaction lost.
  • TransactionEngineFamilyTests, BatchTransactionPolicyTests, AutocommitOnlyStatementTests: the Oracle family and SET TRANSACTION as script transaction control.
  • OracleDriverTransactionStateTests (iOS, no server) and three new OracleDriverTests integration cases (iOS, skipped without ORACLE_TEST_HOST): an editor write and a row edit visible to a second session, an opened transaction held until rollback.
  • scripts/check-oracle-autocommit.sh: 15 rules against a live server through OracleCoreConnection, read from a second session. All 15 pass on 23ai. With the commit flag turned off in a copy of the package, 7 fail, so it catches the bug it exists for.

No UI automation: the flow needs a live Oracle server, which the UI test runner does not have.

Not done

  • A graceful close commits whatever an open transaction holds: oracle-nio's close() sends a logoff, and Oracle commits on logoff. A query timeout or iOS Stop inside an app transaction closes the connection that way, so the earlier statements of a failing batch are committed while the app reports a rollback. Measured on the timeout path. Fixing it means rolling back before the logoff inside oracle-nio's close path; that belongs with the nio PR.
  • Dameng keeps its current transaction handling. Its SET TRANSACTION semantics were not measured.

@mintlify

mintlify Bot commented Sep 20, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 20, 2026, 2:58 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant