Skip to content

fix(datagrid): declare write intent when opening a transaction - #2011

Merged
datlechin merged 1 commit into
mainfrom
fix/2009-readonly-transaction
Aug 3, 2026
Merged

fix(datagrid): declare write intent when opening a transaction#2011
datlechin merged 1 commit into
mainfrom
fix/2009-readonly-transaction

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes the read-only transaction failure reported in discussion #2009, plus the two things that made it impossible for the reporter to diagnose.

The reported error

A user saving to a MySQL production connection got this on every save:

Save failed: [1792] Cannot execute statement in a READ ONLY transaction. (SQLSTATE: 25006)

They believed TablePro's Safe Mode had locked the connection to read-only and could not be changed back. Their Safe Mode was set to "Safe Mode", not "Read-Only".

Root cause

Three separate defects. Only the first produced the error.

1. We opened a bare transaction. MySQLPluginDriver.beginTransaction() sent START TRANSACTION with no access mode. MySQL seeds each transaction's access mode from the session variable transaction_read_only, so on a session where that is ON the transaction opens read-only and the first DML throws 1792.

Confirmed against sql/transaction.cc in mysql-server 8.0: the READ WRITE branch of trans_begin calls check_readonly(), which tests only the server --read-only option and never transaction_read_only. So START TRANSACTION READ WRITE clears a session-level read-only default with no privilege required. On a genuine read_only=ON replica it fails at BEGIN with 1290 ("running with the --read-only option") instead of a confusing mid-transaction 1792, which is earlier and self-describing.

PostgreSQL had the same latent bug: it had no beginTransaction override at all and inherited PluginKit's bare BEGIN, which inherits default_transaction_read_only.

2. Safe Mode edits in the connection form never reached an open session. ConnectionFormCoordinator.save() persists and sends connectionUpdated, whose only subscriber was WelcomeViewModel. ConnectionSession.safeModeLevel is seeded once at connect, so the form edit was invisible to the open window until reconnect, and the toolbar badge (which reads the live session) visibly disagreed with the form. docs/features/safe-mode.mdx already documented the intended behaviour: "There is no session-only override."

3. The raw driver string was surfaced with no explanation, twice, while TablePro's own Safe Mode denial used the same words ("connection is read-only") for a different layer.

Changes

Transaction access mode (ABI-additive). New PluginTransactionAccessMode and a beginTransaction(mode:) requirement on PluginDatabaseDriver with a default that falls back to the existing parameterless call. The published beginTransaction() is untouched, so every already-built registry plugin keeps loading and keeps its current behaviour. MySQL sends START TRANSACTION READ WRITE; the shared LibPQBackedDriver extension sends BEGIN READ WRITE, covering PostgreSQL, CockroachDB, and Redshift. SQLite, MSSQL, ClickHouse and Oracle are deliberately unchanged: the first three have no access-mode syntax, and a read-only Oracle session already fails with a self-describing ORA-16000.

All seven call sites now declare intent. The one mixed site, QueryExecutionCoordinator+Parameters, derives it from OperationKind.worst(of:), so an all-SELECT script still sends a bare BEGIN. That matters: forcing read-write there would have broken replica browsing that works today.

Live session reconciliation. DatabaseManager subscribes to connectionUpdated and reconciles the session's safe mode from storage through the existing setSafeModeLevel sink. That covers the connection form, an iCloud pull from SyncCoordinator, and bulk updates (nil payload). Chosen over having ConnectionFormCoordinator call setSafeModeLevel directly because safeModeLevel is a synced field and a change made on another Mac needs the same reconciliation. Also fixed a stale connection.safeModeLevel read in TableStructureView+Schema.

Error attribution. New DatabaseWriteRejectionDiagnosis detects SQLSTATE 25006 (shared by MySQL 1792, MariaDB 1792, and PostgreSQL read_only_sql_transaction) plus MySQL native codes 1290/1836/1874, which are all HY000. It produces a description and a recovery suggestion whose load-bearing sentence says the server enforced this, not TablePro's Safe Mode. AlertHelper.showErrorSheet gained an optional recoverySuggestion: (defaulted, so existing callers are unaffected). TablePro's own denials in the execution gate, structure view, and AI tool approval now name Safe Mode explicitly.

ABI

scripts/check-pluginkit-abi.sh main reports a diff with 0 removed symbols: one added protocol requirement with a default implementation, one added public enum. Additive per CLAUDE.md, so currentPluginKitVersion stays at its current value and no release-all-plugins.sh run is needed. Needs the abi-additive label.

Tests

18 new tests, all passing (35 total in the touched suites, 0 failures):

  • MySQLTransactionStatementTests, PostgreSQLTransactionStatementTests: the SQL builders. These would have caught the original bug.
  • TransactionAccessModePolicyTests: the regression guard that an all-read script never declares write intent, and that one write in a script promotes the whole transaction.
  • PluginDriverAdapterTransactionTests: the adapter forwards the mode, and a plugin built before the mode existed still opens a transaction (the ABI-safety guarantee).
  • DatabaseWriteRejectionDiagnosisTests: SQLSTATE and native-code classification, non-matches, and that the recovery text names Safe Mode.
  • SafeModeMigrationTests: four new tests covering form-edit reconciliation, bulk reconciliation, the no-session case, and the event-driven path.

swiftlint lint --strict is clean.

Not included

  • Renaming the Read-Only Safe Mode level to avoid the vocabulary collision with server read-only. It is a real collision, but it spans macOS, iOS, the strings catalog, docs, and the connection URL scheme. Rewording the denial messages addresses the reported confusion.
  • Making MainContentCoordinator.connection reactive. Every other connection-form field (name, color, host) is equally stale in an open window until reconnect. Same class of bug, separate change.

@mintlify

mintlify Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Aug 3, 2026, 7:48 AM

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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin datlechin added the abi-additive PluginKit ABI diff reviewed as additive; no version bump needed label Aug 3, 2026
@datlechin
datlechin merged commit f0dd639 into main Aug 3, 2026
3 checks passed
@datlechin
datlechin deleted the fix/2009-readonly-transaction branch August 3, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

abi-additive PluginKit ABI diff reviewed as additive; no version bump needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant