Skip to content

feat: allow passing a startup script per connection#352

Merged
debba merged 8 commits into
TabularisDB:mainfrom
boredland:feat/connection-startup-script
Jun 29, 2026
Merged

feat: allow passing a startup script per connection#352
debba merged 8 commits into
TabularisDB:mainfrom
boredland:feat/connection-startup-script

Conversation

@boredland

Copy link
Copy Markdown
Contributor

Closes #350.

Adds an optional startup script to a connection — SQL run on every new pooled connection, so session settings like SELECT set_config('app.bypass_rls', 'on', false); (or any SET) apply to every query regardless of which pooled connection serves it. This is the DataGrip-style "startup script" requested in the issue, and makes RLS-bypass-in-dev practical.

Changes

  • ConnectionParams.startup_script, persisted with the connection (non-secret).
  • Executed per physical connection so the setting sticks across the whole pool: MySQL/SQLite via sqlx after_connect, Postgres via deadpool post_create. Blank/whitespace scripts are skipped; multiple statements are separated by ;.
  • New Advanced tab in the connection editor with a startup-script field; i18n added for all 8 locales.

Tests

SQLite pool tests (no DB server needed) cover: script runs on connect, blank script is skipped, invalid script surfaces an error.

Run optional SQL on every new pooled connection (MySQL/SQLite
after_connect, Postgres deadpool post_create) so session settings like
set_config apply to every query. Editable via a new "Advanced" tab in
the connection editor.

Closes TabularisDB#350
@kilo-code-bot

kilo-code-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

All feedback from the previous review round has been addressed in this incremental update.

Files Reviewed (10 files)
  • src-tauri/src/pool_manager.rs — validation-only startup-script preflight for MySQL/SQLite/PostgreSQL, bounded by timeouts; clean error attribution
  • src/components/modals/NewConnectionModal.tsx — tab-fade DOM measurement now uses useLayoutEffect; scroll-tab labels are i18n-backed
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/zh.json
Previous Review Summaries (5 snapshots, latest commit 50e9ae6)

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

Previous review (commit 50e9ae6)

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0
Files Reviewed (3 files)
  • src-tauri/src/pool_manager.rs — previous SQLite timeout issue resolved; no new issues
  • src-tauri/src/pool_manager_tests.rs — unchanged since previous review
  • src/components/modals/NewConnectionModal.tsx — unchanged since previous review

Previous Issue Resolution

File Previous Issue Status
src-tauri/src/pool_manager.rs SQLite startup script preflight lacked timeout Fixed — now wrapped in tokio::time::timeout(30s)

Previous review (commit 947325b)

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0
Files Reviewed (3 files)
  • src-tauri/src/pool_manager.rs — previous SQLite timeout issue resolved; no new issues
  • src-tauri/src/pool_manager_tests.rs — unchanged since previous review
  • src/components/modals/NewConnectionModal.tsx — unchanged since previous review

Previous Issue Resolution

File Previous Issue Status
src-tauri/src/pool_manager.rs SQLite startup script preflight lacked timeout Fixed — now wrapped in tokio::time::timeout(30s)

Previous review (commit 2186106)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
src-tauri/src/pool_manager.rs 742 SQLite startup script preflight lacks timeout
Files Reviewed (3 files)
  • src-tauri/src/pool_manager.rs — 1 issue (SQLite preflight lacks timeout guard)
  • src-tauri/src/pool_manager_tests.rs — updated test for attributed errors
  • src/components/modals/NewConnectionModal.tsx — replaced textarea with SqlEditorWrapper

Fix these issues in Kilo Cloud

Previous review (commit 30b5548)

Status: No Issues Found | Recommendation: Merge

The previous CRITICAL issue has been resolved. build_connection_key now includes a SHA256 hash of the startup_script in the pool cache key, so editing a connection's startup script forces creation of a fresh pool whose new connections run the updated script.

Changes verified

  • build_connection_key folds startup_script into the cache key ({key}:startup:{digest:x})
  • startup_script() helper trims whitespace and treats blank scripts as absent
  • Pool after_connect/post_create hooks added for MySQL, PostgreSQL, and SQLite
  • Unit tests verify key changes on script change and blank-script equivalence
  • Integration tests verify script execution, blank-script skipping, and error surfacing
Files Reviewed (2 incremental files)
  • src-tauri/src/pool_manager.rs — startup script hashing in pool key, per-driver connection hooks
  • src-tauri/src/pool_manager_tests.rs — key change tests, integration tests for hook behavior

Previous review (commit 14eeac9)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

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

CRITICAL

File Line Issue
src-tauri/src/pool_manager.rs N/A (outside diff) startup_script not included in build_connection_key pool cache key

startup_script must be part of the pool cache keybuild_connection_key does not include startup_script, so editing a connection's startup script reuses the existing cached pool and the new script never runs. For saved connections (keyed by connection_id), the pool persists indefinitely until app restart. The fix is to include a hash (or trimmed string) of startup_script in build_connection_key so that script changes create a new pool.

Files Reviewed (15 files)
  • src-tauri/src/models.rs
  • src-tauri/src/plugins/driver.rs
  • src-tauri/src/pool_manager.rs
  • src-tauri/src/pool_manager_tests.rs
  • src/components/modals/NewConnectionModal.tsx
  • src/contexts/DatabaseContext.ts
  • src/utils/connections.ts
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/zh.json

Fix these issues in Kilo Cloud


Reviewed by kimi-k2.7-code-20260612 · Input: 82K · Output: 10.9K · Cached: 1M

build_connection_key hashed only connection_id/host/tls, so editing a
saved connection's startup script reused the cached pool and the new
script never ran until app restart. Fold a SHA-256 of the trimmed
script into the key (only when set, so script-free pools keep their
keys). Addresses the critical review finding on TabularisDB#352.
@boredland

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 30b5548.

build_connection_key now folds a SHA-256 of the trimmed startup_script into the pool cache key (only when a script is set, so script-free connections keep their existing keys). Editing the script therefore yields a new key → a fresh pool whose connections run the updated script, instead of reusing the stale cached pool.

Added two unit tests: the key changes when the script changes, and a blank/whitespace script is treated as absent (no key fragmentation).

@debba

debba commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this PR.

Couple of things before this goes in:

  1. The field takes arbitrary SQL, so what's the story for scripts that error out? From the hooks it looks like a failed statement bubbles straight out of after_connect/post_create and the connection never opens, so a single typo in the script locks you out of the data source entirely. Is blocking the behaviour we actually want here? I'd lean towards letting the connection through and surfacing the script failure as a warning instead. If we do want to keep it hard-blocking (which is defensible for stuff like RLS bypass, where you don't want a half-configured session), then the error needs to clearly say it came from the startup script, otherwise it just reads like a bad host or wrong credentials.

  2. Can we swap the plain textarea for SqlEditorWrapper? That's what the editor and the other SQL fields already use, so we'd get syntax highlighting, theming and the font settings for free and it stays consistent with the rest of the app.

A broken startup script now fails fast with a clearly attributed error
instead of a misleading pool timeout or generic connection error:

- MySQL/SQLite preflight the script on a throwaway connection. sqlx
  swallows after_connect errors and retries until the acquire timeout,
  reporting "pool timed out"; the preflight surfaces "Startup script
  failed: ..." up front. Connection failures are returned verbatim so a
  bad host/credentials is not mislabelled as a script error.
- Postgres post_create reports the startup script as the cause.

Also swap the Advanced-tab startup-script textarea for the shared
SqlEditorWrapper (Monaco) for syntax highlighting, theming and font
settings, matching the editor and other SQL fields.
@boredland

Copy link
Copy Markdown
Contributor Author

Both addressed in 2186106.

1. Error story. Kept it hard-blocking — for the RLS-bypass case a half-configured session silently serving queries is worse than a clear failure — but made the failure fast and clearly attributed, and distinct from a connectivity error.

Worth flagging what I found while doing this: for MySQL/SQLite the old behaviour wasn't actually "bubbles straight out". sqlx swallows after_connect errors (logs, hard-closes the connection, retries until acquire_timeout) and then returns PoolTimedOut — so a script typo read as "pool timed out", exactly the misleading case you described. Fixes:

  • MySQL/SQLite now preflight the script on a throwaway connection at pool-creation time. A failed executeStartup script failed: …; a failed connect is returned verbatim, so a bad host/credentials still reads as such rather than as a script error. The per-connection after_connect hook is unchanged — the script still runs on every pooled connection.
  • Postgres (deadpool doesn't swallow/retry) just gets the attribution: post_create now reports `post_create` hook failed: Startup script failed: <cause chain>, surfaced on ping/first query.

One residual edge: a script that passes preflight but fails non-deterministically on a later pooled connection still hits sqlx's retry → PoolTimedOut. The realistic deterministic failures (typos, bad permissions, unknown settings) are caught and attributed.

2. Editor. Swapped the textarea for SqlEditorWrapper, same as the editor and the other SQL fields — syntax highlighting, theming and font settings for free. Kept the RLS example as the Monaco placeholder, so no i18n changes.

The SQLite integration test now asserts the error is attributed (Startup script failed) and returns immediately via the preflight instead of timing out.

Comment thread src-tauri/src/pool_manager.rs Outdated
…meout

The MySQL preflight was wrapped in the connect timeout but SQLite's was
not. SQLite is file-based so a hang is unlikely, but a custom VFS or a
path on a stalled network mount could wedge pool creation indefinitely.
Wrap it in a 30s timeout to match MySQL.
@debba debba requested review from NewtTheWolf and debba June 23, 2026 08:58
debba added 2 commits June 23, 2026 10:58
Tabs no longer squish when many are present (network drivers show up to
7). The strip now scrolls horizontally with edge arrows that appear only
when there is overflow in that direction, a fading mask at the edges,
hidden scrollbar, and auto-scroll of the active tab into view.

@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 this — really clean work! 🙏 Pool-key fold for cache invalidation, error attribution, and the preflight-vs-post_create asymmetry are all well done. Tested locally against a Postgres RLS fixture (restricted role + app.bypass_rls policy): 2 → 4 → 2 rows across the pool, broken script → attributed error, 5/5 Rust tests green. Three blockers, one nit below.

connect_timeout.as_millis()
)
})??;
pool_options = pool_options.after_connect(move |conn, _meta| {

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.

blocker — the script runs twice on the first connection: once in the preflight throwaway connection above, then again here in after_connect. A side-effecting script (INSERT, counters, …) firing an extra time on pool creation is a real footgun. Please make the preflight validation-only (don't execute side effects) so the script runs exactly once per connection.

Comment thread src/components/modals/NewConnectionModal.tsx Outdated
Comment thread src/components/modals/NewConnectionModal.tsx Outdated
Comment thread src-tauri/src/pool_manager.rs
- Make preflight validation-only (rolled-back transaction) so side-effecting
  scripts run exactly once per connection via the per-connection hooks
- Add symmetric PostgreSQL preflight with clean error attribution (was
  surfacing deadpool's raw PoolError debug struct on first use)
- Switch tab-fade useEffect to useLayoutEffect to avoid one-frame flash
  (react.md rule TabularisDB#2)
- i18n hardcoded scroll-tabs aria-labels across all 8 locales

@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.

All four points addressed cleanly — thank you! 🙏 The transaction-rollback preflight is a neat way to keep the script side-effect-free until the per-connection hook, and giving Postgres a symmetric preflight kills the deadpool debug leak. Verified locally: builds, 5/5 Rust tests green.

One tiny FYI (non-blocking): the preflight validates inside a transaction, so a non-transactional statement (VACUUM, CREATE INDEX CONCURRENTLY, …) would be rejected here even though it would run fine in the real post_create/after_connect hook — and MySQL implicitly commits DDL, so a DDL script would still apply once despite the rollback. Both are irrelevant for the intended SET/set_config use; just worth a one-line code comment if you want. LGTM either way.

@boredland

Copy link
Copy Markdown
Contributor Author

One tiny FYI (non-blocking): the preflight validates inside a transaction, so a non-transactional statement (VACUUM, CREATE INDEX CONCURRENTLY, …) would be rejected here

True, but it's hard for me to come up with a reason to do that.

@debba

debba commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

It looks like something non-blocking.
I'll merge it!

@debba debba merged commit f885b31 into TabularisDB:main Jun 29, 2026
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]: Allow passing a startup script

3 participants