Skip to content

feat(database): add project-scoped PostgreSQL query tabs#8685

Open
jae-heo wants to merge 7 commits into
stablyai:mainfrom
jae-heo:feat/database-query-tabs
Open

feat(database): add project-scoped PostgreSQL query tabs#8685
jae-heo wants to merge 7 commits into
stablyai:mainfrom
jae-heo:feat/database-query-tabs

Conversation

@jae-heo

@jae-heo jae-heo commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Closes #8684.

Adds PostgreSQL as a first-class project workspace tab next to Terminal, Browser, and Markdown. Database queries execute on the runtime that owns the project and follow the project's configured SSH connection when the worktree is SSH-backed.

This is intentionally different from the global database page in #6983: the tab, connection ownership, and query route remain attached to the project so local, paired-remote, and SSH-worktree environments use the same execution context.

User-visible behavior

  • New Database Query in the existing new-tab menu
  • persistent database tabs in workspace sessions and tab groups
  • runtime/SSH route disclosure in the tab toolbar
  • PostgreSQL profile create/select/edit/delete flows scoped to the owning node
  • remembered passwords usable by clients paired to that runtime without returning the password to those clients
  • database and schema selectors plus a schema tree
  • read-only query execution with bounded rows, timeout, and cancellation

Implementation

  • adds a small database-provider boundary with an initial PostgreSQL provider
  • packages the pg runtime dependencies used by desktop and headless server builds
  • routes database RPCs through the owning runtime, then through the project's SSH connection when present
  • persists profile metadata separately from encrypted credentials
  • stores no password in tab state or workspace-session payloads
  • extends the workspace-session schema and protocol version for database tabs

Screenshots

  1. New-tab menu entry

    Insert new-tab-menu-review.png here (shows New Database Query alongside the existing workspace tab types).

  2. Database Query tab running in the project environment

    Insert project-database-tab-review.png here (shows the runtime/SSH route, database and schema selectors, editor, and bounded result grid).

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm build
  • Added high-quality tests for provider behavior, query cancellation, SSH routing, profile persistence, vault behavior, runtime ownership, credential stripping, tab ordering, and session serialization

Full suite result:

  • 2,783 test files passed; 7 skipped
  • 29,447 tests passed; 43 skipped

Manual/package validation:

  • installed and exercised the packaged macOS dogfood app
  • installed and exercised the Linux AppImage/headless server
  • paired the macOS desktop to a Linux runtime
  • opened a project whose runtime routes through ssh-p8
  • listed PostgreSQL databases and schemas, introspected a schema, and executed a live query through that route
  • verified saved-profile summaries omit the password while the owning runtime can resolve it from its vault

AI Review Report

The implementation was reviewed with an AI coding agent for:

  • local vs paired-remote vs SSH-worktree runtime ownership
  • stale or edited SSH connection metadata
  • query cancellation and connection cleanup
  • bounded result sets and statement timeouts
  • tab-group ordering, dragging, splitting, cycling, persistence, and restore behavior
  • password leakage through UI state, RPC responses, logs, errors, and workspace sessions
  • packaging of runtime-only PostgreSQL dependencies
  • localization parity and existing Orca UI conventions
  • cross-platform paths, shortcut labels, and Electron/headless-runtime differences across macOS, Linux, and Windows

The review flagged and the branch addressed packaged dependency omission, missing server-side cancellation, edited SSH endpoint handling, credential/profile separation, and session-schema compatibility. macOS and Linux were exercised directly; Windows behavior was reviewed and covered by the cross-platform typecheck/test/build paths but was not manually dogfooded.

Security Audit

  • RPC inputs are schema-validated and query execution is bounded by row limits and timeouts.
  • Read-only mode is enforced with a PostgreSQL read-only transaction rather than SQL keyword matching.
  • Cancellation uses a separate PostgreSQL connection and pg_cancel_backend for the captured backend PID.
  • Passwords are excluded from tab state, profile summaries, query responses, and workspace-session serialization.
  • Remembered credentials use AES-256-GCM with per-record nonces and profile-ID additional authenticated data.
  • The vault key is protected with Electron safeStorage where available.
  • Saved passwords are bound to endpoint identity so editing host, user, port, or TLS cannot silently forward an existing credential.
  • Profile and vault files are created with mode 0600 and write/permission failures fail loudly.

Known limitation: a headless Linux runtime without a usable secret service falls back to a random vault key stored in a mode-0600 file. This protects against config leakage and other OS users, but not compromise of the Orca Unix account. The UI and documentation should not describe that fallback as equivalent to an OS keychain.

Notes

jae-heo added 6 commits July 14, 2026 11:37
…-tabs

# Conflicts:
#	src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx
#	src/renderer/src/i18n/locales/en.json
#	src/renderer/src/i18n/locales/es.json
#	src/renderer/src/i18n/locales/ja.json
#	src/renderer/src/i18n/locales/ko.json
#	src/renderer/src/i18n/locales/zh.json
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds PostgreSQL database connectivity with encrypted profile storage, SSH routing, runtime RPCs, query execution, catalog and schema inspection, and cancellation. Introduces shared database contracts and runtime capabilities. Adds database tabs, connection/profile controls, schema and result panes, tab ordering and lifecycle handling, palette and focus integration, localization, and packaged PostgreSQL dependencies.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: project-scoped PostgreSQL query tabs.
Description check ✅ Passed The description includes all required sections and covers summary, screenshots, testing, AI review, security, and notes.
Linked Issues check ✅ Passed The changes satisfy #8684 by adding project-scoped PostgreSQL tabs, runtime/SSH routing, scoped profiles, schema browsing, and bounded query execution.
Out of Scope Changes check ✅ Passed The diff stays focused on the database-tab feature and its supporting runtime, shared, and UI plumbing without clear unrelated additions.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 12

🧹 Nitpick comments (10)
src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx (1)

709-721: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared openDatabaseTab helper instead of duplicating tab construction.

Terminal.tsx's handleNewDatabaseTab already delegates to openDatabaseTab(worktreeId, targetGroupId) from database-tab-actions.ts. This callback instead re-implements the same initial-state construction by hand (connection/queryDraft/readOnly listed individually), which can silently drop future DatabaseTabState fields and diverges from the single source of truth.

♻️ Proposed refactor
-  const createFloatingDatabaseTab = useCallback(() => {
-    const tab = createUnifiedTab(FLOATING_TERMINAL_WORKTREE_ID, 'database', {
-      label: translate('auto.components.database.tab.title', 'Database Query'),
-      database: {
-        connection: { ...DEFAULT_DATABASE_TAB_STATE.connection },
-        queryDraft: DEFAULT_DATABASE_TAB_STATE.queryDraft,
-        readOnly: DEFAULT_DATABASE_TAB_STATE.readOnly
-      },
-      targetGroupId: activeGroup?.id,
-      activate: true
-    })
-    activateTab(tab.id)
-  }, [activateTab, activeGroup, createUnifiedTab])
+  const createFloatingDatabaseTab = useCallback(() => {
+    const tabId = openDatabaseTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id)
+    activateTab(tabId)
+  }, [activateTab, activeGroup])
src/shared/workspace-session-schema.ts (1)

128-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate connection-shape schema (see consolidated comment).

This connection sub-schema (host/port/database/schema/user/sslMode) re-declares the same shape already validated in database-profile-store.ts's StoredConnectionSchema. See the consolidated comment for the cross-file DRY recommendation.

src/main/database/database-profile-store.ts (2)

127-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate persistedConnection helper (see consolidated comment).

This function duplicates database-profile-service.ts's same-named helper with slightly different behavior (no trimming here). See the consolidated comment for the cross-file DRY recommendation.


1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Postgres connection shape has no single source of truth; extract a shared schema + normalizer.

The same 7-field Postgres connection shape (providerId/host/port/database/schema/user/sslMode) is independently hand-declared as a Zod schema in database-profile-store.ts and workspace-session-schema.ts (and again in the RPC layer's DatabaseConnection schema), and the "normalize connection for persistence" logic is duplicated as two same-named persistedConnection functions with diverging behavior (one trims, one doesn't). This increases the risk that a future change to a constraint (e.g., sslMode values, field length limits) is applied inconsistently across boundaries.

  • src/main/database/database-profile-store.ts#L14-22: treat this StoredConnectionSchema as the canonical shape (or extract it to a shared module) and have other layers .extend()/reuse it instead of re-declaring the same fields.
  • src/main/database/database-profile-store.ts#L127-137: replace this persistedConnection with a single shared normalizer (shared with the service-layer version below) so trimming behavior is consistent regardless of call path.
  • src/main/database/database-profile-service.ts#L116-126: same as above — consolidate with the store's persistedConnection into one shared helper.
  • src/shared/workspace-session-schema.ts#L131-139: derive this nested connection schema from the same shared Postgres-connection schema instead of re-declaring the field list.
src/main/database/database-profile-service.ts (1)

116-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate persistedConnection helper (see consolidated comment).

This function duplicates database-profile-store.ts's same-named helper (that one doesn't trim). See the consolidated comment for the cross-file DRY recommendation.

src/main/database/database-service.ts (1)

41-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the repeated resolve→route→dispatch pattern.

testConnection, introspect, catalog, and execute are identical except for the provider method invoked. Extracting a shared helper prevents future new operations from accidentally skipping profile resolution or SSH routing.

♻️ Proposed consolidation
+  private async run<TRequest extends DatabaseConnectionRequest, TResult>(
+    request: TRequest,
+    operation: (
+      provider: DatabaseProvider,
+      routedRequest: ResolvedDatabaseRequest<TRequest>
+    ) => Promise<TResult>
+  ): Promise<TResult> {
+    return this.withProjectConnection(this.resolveRequest(request), (routedRequest) =>
+      operation(this.getProvider(routedRequest.connection.providerId), routedRequest)
+    )
+  }
+
   async testConnection(
     request: DatabaseConnectionRequest,
     signal?: AbortSignal
   ): Promise<DatabaseConnectionTestResult> {
-    return this.withProjectConnection(this.resolveRequest(request), (routedRequest) =>
-      this.getProvider(routedRequest.connection.providerId).testConnection(routedRequest, signal)
-    )
+    return this.run(request, (provider, routedRequest) => provider.testConnection(routedRequest, signal))
   }
   // ...same for introspect, catalog, execute
src/main/database/database-ssh-connection-route.test.ts (1)

5-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixture host coincides with the loopback address, weakening the tlsServerName assertion.

The fixture uses host: '127.0.0.1', the same value the tunnel rewrites connection.host to. The assertion at line 46-51 checks tlsServerName: '127.0.0.1', which would pass even if the implementation mistakenly hardcoded the loopback address instead of preserving the original remote host. Use a distinct remote-looking host (e.g. 'db.internal.example.com') to actually exercise that TLS verification still targets the real server name.

Also applies to: 46-51

src/renderer/src/components/database/useDatabaseProfiles.ts (1)

43-66: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Selecting a profile re-triggers a full profile-list reload.

selectProfile (lines 73-88) already syncs profileName/rememberPassword from the in-memory profiles array and then updates database.profileId. Because this effect depends on database.profileId, that update re-runs the whole listDatabaseProfiles IPC call (with vault decryption) again — redundant work on every profile selection. Consider keying this effect only on [worktreeId, nodeIdentity] and deriving the "selected profile" sync via a separate useEffect/useMemo over the already-loaded profiles state.

As per coding guidelines, **/*.{js,jsx,ts,tsx,sh,ps1} should "avoid unnecessary API calls".

Source: Coding guidelines

src/main/database/postgres-catalog.ts (1)

8-28: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bounding the catalog queries like the rest of the provider.

postgres-provider.ts bounds query rows and schema columns (MAX_QUERY_ROWS, MAX_SCHEMA_COLUMNS), but the pg_database/information_schema.schemata listings here have no LIMIT. On a multi-tenant instance with many databases/schemas this could return an unexpectedly large payload back to the renderer.

src/main/database/postgres-provider.test.ts (1)

211-230: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the cancel client is closed

Add an expectation that the short-lived cancel connection calls end(); that keeps this test from missing a cleanup regression.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4e7078d9-a83a-42ca-b684-41ffa07942a2

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb6f91 and 18e93f3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (66)
  • config/packaged-runtime-node-modules.cjs
  • config/scripts/electron-builder-config.test.mjs
  • package.json
  • src/main/database/database-credential-vault.ts
  • src/main/database/database-profile-service.test.ts
  • src/main/database/database-profile-service.ts
  • src/main/database/database-profile-store.ts
  • src/main/database/database-provider.ts
  • src/main/database/database-service.test.ts
  • src/main/database/database-service.ts
  • src/main/database/database-ssh-connection-route.test.ts
  • src/main/database/database-ssh-connection-route.ts
  • src/main/database/database-vault-key-protection.ts
  • src/main/database/postgres-catalog.ts
  • src/main/database/postgres-provider.test.ts
  • src/main/database/postgres-provider.ts
  • src/main/ipc/ssh.ts
  • src/main/runtime/rpc/methods/database.ts
  • src/main/runtime/rpc/methods/index.ts
  • src/renderer/src/components/Terminal.tsx
  • src/renderer/src/components/WorktreeJumpPalette.tsx
  • src/renderer/src/components/database/DatabaseConnectionForm.tsx
  • src/renderer/src/components/database/DatabaseContextToolbar.tsx
  • src/renderer/src/components/database/DatabasePane.tsx
  • src/renderer/src/components/database/DatabaseResults.tsx
  • src/renderer/src/components/database/DatabaseSchemaTree.tsx
  • src/renderer/src/components/database/database-tab-actions.ts
  • src/renderer/src/components/database/database-tab-credentials.test.ts
  • src/renderer/src/components/database/database-tab-credentials.ts
  • src/renderer/src/components/database/useDatabaseProfiles.ts
  • src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
  • src/renderer/src/components/tab-bar/DatabaseTab.tsx
  • src/renderer/src/components/tab-bar/TabBar.tsx
  • src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx
  • src/renderer/src/components/tab-bar/group-tab-order.ts
  • src/renderer/src/components/tab-bar/reconcile-order.test.ts
  • src/renderer/src/components/tab-bar/reconcile-order.ts
  • src/renderer/src/components/tab-bar/tab-create-menu-options.test.ts
  • src/renderer/src/components/tab-bar/tab-create-menu-options.ts
  • src/renderer/src/components/tab-group/TabGroupPanel.tsx
  • src/renderer/src/components/tab-group/tab-drag-preview-activation.ts
  • src/renderer/src/components/tab-group/useTabDragSplit.ts
  • src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
  • src/renderer/src/components/terminal/tab-type-cycle.ts
  • src/renderer/src/hooks/modal-return-focus-action.ts
  • src/renderer/src/hooks/resolve-zoom-target.ts
  • src/renderer/src/hooks/useModalReturnFocus.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/simulator-palette-search.ts
  • src/renderer/src/lib/workspace-tab-palette-search.ts
  • src/renderer/src/lib/worktree-runtime-owner.test.ts
  • src/renderer/src/lib/worktree-runtime-owner.ts
  • src/renderer/src/runtime/runtime-database-client.test.ts
  • src/renderer/src/runtime/runtime-database-client.ts
  • src/renderer/src/store/selectors.ts
  • src/renderer/src/store/slices/tabs.ts
  • src/renderer/src/store/slices/worktrees.ts
  • src/shared/database-types.ts
  • src/shared/protocol-version.ts
  • src/shared/types.ts
  • src/shared/workspace-session-schema.test.ts
  • src/shared/workspace-session-schema.ts

Comment thread src/main/database/database-profile-service.ts
Comment thread src/main/database/database-service.ts
Comment thread src/main/database/postgres-provider.ts Outdated
Comment thread src/renderer/src/components/database/DatabaseConnectionForm.tsx
Comment thread src/renderer/src/components/database/DatabaseContextToolbar.tsx
Comment thread src/renderer/src/components/database/useDatabaseProfiles.ts
Comment thread src/renderer/src/components/tab-bar/DatabaseTab.tsx
Comment thread src/renderer/src/components/tab-bar/TabBar.tsx
Comment thread src/renderer/src/components/terminal/tab-type-cycle.ts
Comment thread src/renderer/src/runtime/runtime-database-client.ts

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
src/renderer/src/components/terminal/tab-type-cycle.ts (1)

34-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider removing redundant branches.

Since the fallback return at the end of this function is return activeTabId, the explicit branch for database (along with the one for simulator) is redundant. You can optionally remove them for a more concise implementation that falls through to the default behavior.

♻️ Proposed refactor
-  if (activeTabType === 'simulator') {
-    return activeTabId
-  }
-  if (activeTabType === 'database') {
-    return activeTabId
-  }
   return activeTabId

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f35b4ce3-5ae9-4e37-b0ef-55d6973e84b4

📥 Commits

Reviewing files that changed from the base of the PR and between 2905fe1 and 81f205a.

📒 Files selected for processing (23)
  • src/main/database/database-profile-service.test.ts
  • src/main/database/database-profile-service.ts
  • src/main/database/database-profile-store.ts
  • src/main/database/database-service.test.ts
  • src/main/database/database-service.ts
  • src/main/database/database-ssh-connection-route.test.ts
  • src/main/database/postgres-catalog.ts
  • src/main/database/postgres-provider.test.ts
  • src/main/database/postgres-provider.ts
  • src/renderer/src/components/database/DatabaseConnectionForm.tsx
  • src/renderer/src/components/database/DatabaseContextToolbar.tsx
  • src/renderer/src/components/database/DatabasePane.tsx
  • src/renderer/src/components/database/useDatabaseProfiles.ts
  • src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
  • src/renderer/src/components/tab-bar/DatabaseTab.tsx
  • src/renderer/src/components/tab-bar/TabBar.tsx
  • src/renderer/src/components/terminal/tab-type-cycle.test.ts
  • src/renderer/src/components/terminal/tab-type-cycle.ts
  • src/renderer/src/hooks/ipc-tab-switch.test.ts
  • src/renderer/src/hooks/ipc-tab-switch.ts
  • src/renderer/src/runtime/runtime-database-client.test.ts
  • src/renderer/src/runtime/runtime-database-client.ts
  • src/shared/database-types.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • src/main/database/postgres-catalog.ts
  • src/shared/database-types.ts
  • src/renderer/src/runtime/runtime-database-client.test.ts
  • src/renderer/src/components/database/DatabaseConnectionForm.tsx
  • src/renderer/src/components/database/DatabaseContextToolbar.tsx
  • src/renderer/src/components/database/DatabasePane.tsx
  • src/renderer/src/runtime/runtime-database-client.ts
  • src/main/database/database-profile-service.test.ts
  • src/main/database/database-ssh-connection-route.test.ts
  • src/renderer/src/components/tab-bar/DatabaseTab.tsx
  • src/main/database/postgres-provider.ts
  • src/renderer/src/components/database/useDatabaseProfiles.ts
  • src/renderer/src/components/tab-bar/TabBar.tsx
  • src/main/database/postgres-provider.test.ts

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.

[Feature]: Project-scoped database query tabs that run on the worktree runtime

2 participants