Skip to content

feat(connections): add enforced read-only mode per connection - #1463

Merged
NewtTheWolf merged 3 commits into
TabularisDB:mainfrom
pokertour:codex/issue-301-read-only-connections
Nov 10, 2026
Merged

NewtTheWolf merged 3 commits into
TabularisDB:mainfrom
pokertour:codex/issue-301-read-only-connections

Conversation

@pokertour

@pokertour pokertour commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Closes #301

Adds a persistent read-only option in the connection settings to prevent accidental database writes, including queries generated through text-to-SQL.

Builds on #473: read-only mode blocks writes before production confirmation, regardless of the session's “don't ask again” setting. Existing production warnings remain unchanged when read-only mode is disabled.

Changes

  • Add a connection-level read-only setting, badge and editor banner.
  • Disable editing and database modification actions using the existing capability system, including in split panes.
  • Enforce the policy in the shared backend for desktop, web and MCP, covering queries, batches, record mutations, schema changes and imports.
  • Isolate protected connection pools and enable database-level protection for the built-in PostgreSQL, MySQL/MariaDB and SQLite drivers.
  • Keep exports available through protected reads.
  • Preserve the setting through persistence, duplication and connection transfers.

Plugin support

Writable plugins can opt in through capabilities.read_only_queries and implement the execute_read_only_query RPC.

Unsupported plugins cannot enable the option. RPC or protection failures never fall back to ordinary execution. Intrinsically read-only plugins require no new RPC.

Includes the manifest schema, TypeScript contract, documentation and compatibility tests.

Current limitations

  • Saved startup scripts are skipped while read-only mode is enabled.
  • Explicit transaction commands are rejected; read-only batches execute statements independently.
  • Visual EXPLAIN is unavailable in this mode. Raw EXPLAIN can use the protected query path.
  • Existing writable plugins need to implement the new contract.

Validation

  • Manually tested with SQLite and MariaDB.
  • 274 targeted Rust tests and 140 targeted frontend tests passed.
  • TypeScript, targeted ESLint, theme tokens, frontend boundaries and cargo check --no-default-features passed.
  • PostgreSQL has not been tested against a live server.
  • Full-suite runs are not entirely green on this Windows host: failures remain in unchanged number-formatting, system-path, web audit and socket tests.

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

"mysql" => {
// Protected exports use the same bounded query path as the editor,
// including for built-ins, rather than bypassing it with a raw stream.
match (driver, params.read_only) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Read-only dumps of built-in drivers fall into the plugin paging arm

With params.read_only set, mysql/postgres/sqlite no longer use their native streaming arms and instead go through the generic _ arm: LIMIT/OFFSET paging of SELECT * FROM table (1000 rows per page), plugin_row_cells, and json_column_names. That is quadratic on large tables and loses the type-aware SqlCell extraction (extract_value, type_info() == "JSON") the native arms provide, so dump fidelity can differ between a writable and a read-only connection. Consider keeping the native streaming arms for built-ins, since their pools are already protected at connection level (default_transaction_read_only, SET SESSION TRANSACTION READ ONLY, query_only), or at least add a test that compares dump output for both modes.

@kilo-code-bot

kilo-code-bot Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental review of afc9c2ff..712f1d5d:

  • The previous suggestion (read-only dumps losing native streaming arms in stream_sql_rows) is resolved: mysql/postgres/sqlite keep their native streaming and type-aware cell extraction in both modes, and a new test (read_only_native_dumps_match_writable_dumps_across_page_boundaries) compares writable and read-only dump output across page boundaries.
  • The previously noted plugin export bypass is resolved: export.rs now calls validate_query up front for read-only connections, drops any pre-existing editor transaction session, and routes plugin pagination through application::read_only::execute_query (protected RPC). New tests in export/read_only_tests.rs cover this.

Assumption: PostgreSQL protection (default_transaction_read_only=on plus BEGIN READ ONLY) was not exercised against a live server in this review.

Files Reviewed (4 files)
  • src-tauri/src/application/database_transfers.rs
  • src-tauri/src/application/read_only/tests.rs
  • src-tauri/src/export.rs
  • src-tauri/src/export/read_only_tests.rs
Previous Review Summary (commit afc9c2f)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit afc9c2f)

Status: 1 Issue Found | Recommendation: Merge after considering the suggestion

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
src-tauri/src/application/database_transfers.rs 659 With read_only set, built-in drivers (mysql/postgres/sqlite) no longer use their native streaming arms in stream_sql_rows. They fall into the generic LIMIT/OFFSET paging arm, which is quadratic on large tables and loses the type-aware SqlCell extraction. Dump output can differ from a writable connection.

Notes outside the changed lines (not inline-commented)

  • src-tauri/src/export.rs (stream_query_via_plugin, line 136) is unchanged by this PR. It still calls execute_query for plugin drivers, with no validate_query and no execute_read_only_query, when params.read_only is set. export_query_to_file takes user-supplied SQL, so on a read-only plugin connection (one with read_only_queries) an export bypasses the protected RPC. The built-in drivers are covered by their protected pools. Consider routing this path through application::read_only::execute_query when params.read_only is true.
  • The PR description states PostgreSQL was not tested against a live server. The protection there relies on default_transaction_read_only=on plus BEGIN READ ONLY.

Backend enforcement (ensure_writable, validate_query, pool key isolation, startup-script skipping, MCP integration) and the frontend capability and guard changes were reviewed with no other defects found.

Fix these issues in Kilo Cloud

Files Reviewed (53 files)
  • src-tauri/src/application/database_transfers.rs - 1 issue
  • src-tauri/src/application/queries.rs
  • src-tauri/src/application/read_only.rs
  • src-tauri/src/application/read_only/tests.rs
  • src-tauri/src/application/records.rs
  • src-tauri/src/application/table_transfers.rs
  • src-tauri/src/application/database_objects.rs
  • src-tauri/src/application/connections.rs
  • src-tauri/src/mcp/mod.rs
  • src-tauri/src/pool_manager.rs
  • src-tauri/src/plugins/driver.rs
  • src-tauri/src/drivers/* (sqlite, mysql, postgres, driver_trait)
  • src-tauri/src/{models,commands,clipboard_import}.rs
  • packages/web-ui/src/** (connectionPolicy, DatabaseProvider, useProductionGuard, NewConnectionModal, ReadOnlyBanner, ConnectionChips, locales, types)
  • packages/web-ui/tests/**, plugins/PLUGIN_GUIDE.md, plugins/manifest.schema.json

Reviewed by claude-sonnet-5.5 · Input: 252 · Output: 1.6K · Cached: 176.4K

Review guidance: REVIEW.md from base branch main

@NewtTheWolf NewtTheWolf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for enforcing the saved connection policy in the shared backend as well as the UI, and for keeping protected pools separate from writable sessions. I reviewed 712f1d5, including startup-script suppression, mutation/import guards, protected exports and the plugin's fail-closed RPC path.

The full frontend suite and targeted read-only/backend coverage pass locally, and typecheck and lint are green. I will now exercise read-only SQLite, PostgreSQL and MySQL connections in the real app, including a database-side write hidden behind a SELECT, finish the full checks, and merge this revision if verification passes.

@NewtTheWolf

Copy link
Copy Markdown
Collaborator

Tested 89773a5 in the real Linux Tauri app with disposable SQLite, PostgreSQL 17 and MySQL 8.4 connections. After resolving the main/translation conflicts, reads work, saved startup writes are skipped, and direct IPC DELETE attempts are blocked for all three. On PostgreSQL, a SELECT calling a write function is rejected by the read-only transaction. A protected read still exports all three rows to CSV, and the protected MySQL path also stops at its one-second query timeout and remains usable.

pnpm lint, pnpm typecheck and pnpm test:all pass, including 2,493 Rust library tests and all 8,799 frontend tests. Ready to merge.

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.

[Feat]: Read only connection limiter support

2 participants