Skip to content

feat(postgres): browse multiple databases on one connection - #822

Draft
aesslinger wants to merge 13 commits into
TabularisDB:mainfrom
aesslinger:feat/postgres-multi-database-v2
Draft

aesslinger wants to merge 13 commits into
TabularisDB:mainfrom
aesslinger:feat/postgres-multi-database-v2

Conversation

@aesslinger

@aesslinger aesslinger commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Closes #340. Addresses #725.

What this does

PostgreSQL connections can now opt into browsing multiple databases from one connection, the same way MySQL already does. Pick databases in the connection dialog's "Databases" tab and each one shows up as a top-level node in the sidebar expanding into its own schemas and tables. All browsing, mutation, and tooling actions are fully wired: Create/Drop table, column, index, FK, view, trigger; Run/Edit/Drop routine; Edit/Drop trigger; ER diagram with in-window schema switcher; Dump/Import. The ER diagram window shows a schema-picker dropdown so users can switch which schema they're viewing without reopening.

This builds on work started in #402, re-scoped against the current plugin architecture rather than the deprecated in-tree driver. The plan was shared with @debba before starting: #402 (comment)

How it works

Capability model. A new hasOptedIntoDatabaseSelection helper distinguishes "user explicitly opted into multi-database browsing" from "this connection just has a database set." MySQL uses capability alone as the gate (unchanged); Postgres requires an array or empty-string in the persisted database field as the opt-in signal, so no existing single-database connection is silently reclassified. isMultiDatabaseCapable (MySQL) is untouched.

Backend. A database: Option<String> override — cloning ConnectionParams, filtering empty strings, overriding .database — is now applied uniformly across every Tauri command that needs to target a specific database: table/column/index/FK/schema/view/trigger/routine metadata, query execution, explain, count, export, dump/import, ER diagram snapshot, and all DDL mutation commands. Schema selection and preference persistence is scoped per-database via a composite storage key.

Dump/import plugin-bypass fix. dump_database's table-listing and DDL-fetch previously matched driver.as_str() against hardcoded strings, returning "Unsupported driver" for any plugin-registered driver. Both now route through the DriverTrait (get_tables, new get_table_ddl method). The data-export step is intentionally unchanged — it opens its own raw connection independently and rerouting it would risk losing JSON/binary type fidelity.

Frontend. DatabaseProvider gained a nestedDatabaseDataMap and a connect() branch. A new SidebarNestedDatabaseItem renders each selected database's own schema picker and reuses SidebarSchemaItem per schema. All 7 mutation modals, DumpDatabaseModal, ImportDatabaseModal, and the ER diagram window route correctly using both schema and database. Tab/AddTabInput/EditorNavigationRequest carry a new database field threaded through databaseObjectActions.ts and useDatabaseObjectNavigation.

Known limitation

get_table_ddl in the plugin protocol is not yet implemented — a dump on a plugin-installed Postgres driver returns "method not implemented" for the DDL (structure) step while the data step works fine. Tracked in tabularis-postgresql-plugin#118.

Verification

Automated:

  • cargo test --lib — 1319 passed.
  • cargo test --test postgres_integration -- --ignored — all 10 multi-database integration tests pass against a live Postgres 16 container (port 54320), including three new tests that directly validate the params.database override routing: test_database_override_routes_to_secondary, test_get_tables_with_database_and_schema_override, test_empty_string_filter_prevents_maintenance_db_override.
  • pnpm vitest run — 5000 passed. pnpm run build and pnpm lint clean.

Still needs a human — app running against real Postgres + plugin. Checklist:

  • Create a connection, opt into 2+ databases via the Databases tab — sidebar shows nested database → schema → table tree
  • Browse and open tables/views/routines/triggers in a non-primary database
  • Create and drop table, column, index, FK, view, trigger in a non-primary database's schema
  • Commit edits (Ctrl+S), Export CSV/JSON, Dump, Import from a non-primary database
  • View ER Diagram → schema picker in the window switches schemas without reopening
  • Existing single-database Postgres connections are unchanged
  • MySQL multi-database flow is unaffected

…ow schema-based multi-db in connection UI

Stage 1-2 of closing TabularisDB#725/TabularisDB#340: add a database: Option<String> override
(mirroring the pattern already used by insert/update/delete_record) to
get_schemas, get_tables, get_columns, get_foreign_keys, get_indexes,
execute_query, execute_query_batch, explain_query_plan, and count_query,
plus a missing .filter(|d| !d.is_empty()) guard on the three existing
overrides. Add isSchemaBasedMultiDbCapable/isSchemaBasedMultiDb to
src/utils/database.ts and wire NewConnectionModal's Databases tab/picker
to schema-based drivers (Postgres), not just MySQL-style flat drivers.
…chema-based multi-db drivers

Stage 3 of closing TabularisDB#725/TabularisDB#340. Adds isSchemaBasedMultiDbCapable/isSchemaBasedMultiDb
and hasOptedIntoDatabaseSelection to src/utils/database.ts, a NestedDatabaseData
shape in DatabaseContext.ts, and DatabaseProvider.tsx provider functions
(loadNestedSchemas, setSelectedSchemasForDatabase, loadNestedSchemaData,
refreshNestedSchemaData) plus a connect() branch that pre-loads the first
database's schema/table data for a connection with an explicit multi-database
selection.

The critical fix: capability alone can't gate this the way it does for MySQL,
because every existing Postgres connection already stores a required non-empty
database string from the traditional single-database mode — identical in shape
to a one-element selection. hasOptedIntoDatabaseSelection requires an array (any
length) or an empty string ("all databases") for schema-based drivers, so a
plain string always means "the one database this connection always pointed
at." NewConnectionModal.tsx's save/edit/parse paths were updated to match
(never collapsing a single Databases-tab selection to a plain string for
schema-based drivers). Backend: extend the database: Option<String> override
already used by insert/update/delete_record to get_schemas, get_tables,
get_columns, get_foreign_keys, get_indexes, get_views, get_materialized_views,
get_triggers, get_routines, get_routine_definition, get_trigger_definition,
execute_query, execute_query_batch, explain_query_plan, and count_query
(discovered while wiring the sidebar's click-to-open path); scope schema
selection/preference persistence (set/get_selected_schemas,
set/get_schema_preference) per-database via a composite storage key so two
databases on the same connection don't share one selection.

Frontend: a new SidebarNestedDatabaseItem renders each selected database's
own schema picker and, once schemas are selected, a SidebarSchemaItem per
schema (same component the single-database layout already uses). Tab/
AddTabInput/EditorNavigationRequest gained a database field alongside schema,
threaded through databaseObjectActions.ts and useDatabaseObjectNavigation so
double-clicking a table/view/routine/trigger opens the right connection pool.
Table/view/routine/trigger CRUD actions (create/drop table, index, foreign
key, trigger) are intentionally not wired for the nested tree yet — surfaced
via a translated "not yet supported" message instead of a silent no-op or a
half-wired backend call; that work is scoped to the routing-correctness pass
next (PR TabularisDB#402's review checklist, re-verified against today's plugin
architecture).
…paths through database

Stage 4 of closing TabularisDB#725/TabularisDB#340 — PR TabularisDB#402's review checklist, re-verified against
today's code and fixed:

- getTableDataChangeScope gains a tabDatabase param and a first branch that
  returns {database, schema} together for a nested tab; a plain single-db
  Postgres tab (no tab.database) still falls through to the unchanged
  schemas-only branch, so the commit-path regression TabularisDB#402's reviewer caught
  can't recur here.
- execute_query (all three call sites), execute_query_batch, count_query, and
  export_query_to_file now pass tab.database (nested) ahead of the existing
  schema-as-database fallback (flat MySQL), instead of only ever resolving one
  qualifier.
- Visual EXPLAIN (useExplainPlan, VisualExplainModal, Editor.tsx) gained a
  database field alongside schema, threaded through to explain_query_plan.
- Sidebar context menus (table/index/foreign_key/view/materialized_view/
  routine/trigger/column) now carry `database` in ContextMenuData and forward
  it to every action — drop_index_action, drop_foreign_key_action, drop_view,
  drop_trigger, refresh_materialized_view, get_view_columns,
  get_materialized_view_columns, get_routine_parameters, get_routine_definition,
  get_trigger_definition, and the objectNavigation.open/count/newConsole/
  openRoutineDefinition/openTriggerDefinition chain all gained a database
  override (mirroring the pattern from Stage 1). Also fixed a pre-existing bug
  where "Drop View" ignored the view's own schema in favor of the global
  active schema.

Still not wired for the nested tree (unchanged from Stage 3, tracked as
follow-up): Run/Edit/Drop Routine and Edit/Drop Trigger still resolve their
target through modal state that doesn't carry a database field yet; the ER
diagram window and Dump Database modal don't have a schema/database picker of
their own. None of these regress existing single-database Postgres or MySQL
behavior — they're simply not yet reachable with the right routing from a
nested database's context menu.
…mbined data-change scope

Stage 5 of closing TabularisDB#725/TabularisDB#340.

- database.test.ts: getTableDataChangeScope's new combined {database, schema}
  branch (nested tab), its fallback to the active schema, and the two
  regression guards — a plain single-db Postgres tab (no tab.database) keeps
  returning {schema} only, and a flat multi-db driver (MySQL) ignores
  tabDatabase entirely since it never sets it.
- DatabaseProvider.test.tsx: a new "PostgreSQL Nested Multi-Database
  Selection" describe block exercises connect() end-to-end for a connection
  saved with an array `database` (the opt-in signal) — asserts
  nestedDatabaseDataMap gets the first database's schemas/tables pre-loaded
  with schema+database routed correctly to get_schemas/get_tables, that a
  database with no saved schema selection is marked needsSchemaSelection,
  and — the regression guard — that the connection-level (non-nested)
  schemas/selectedSchemas/schemaDataMap fields stay completely untouched.

Full suite: 5000 frontend tests passing, 1319 backend tests passing
(excluding 3 test modules — theme_packages, askpass, storage_location_tests —
that fail identically on a clean upstream/main checkout in this environment;
confirmed via git stash comparison, unrelated to this branch).

Not done in this pass, needs a human with the app running against a real
Postgres + the tabularis-postgresql-plugin: clicking through the actual
nested sidebar tree, exercising every Stage 4 checklist row end-to-end, and
confirming a plain single-database Postgres connection's UI is pixel-for-
pixel unchanged.
…ryEntry union

`ContextMenuData` includes `QueryHistoryEntry`, whose `database` field is
typed `string | null` (not `string | undefined`). Every
`"database" in contextMenu.data ? contextMenu.data.database : undefined`
extraction across the union therefore inferred as `string | null | undefined`
instead of `string | undefined`, which `objectNavigation.open/count/newConsole`
and `hasOptedIntoDatabaseSelection` don't accept.

`pnpm typecheck` (`tsc --noEmit`) didn't catch this because the root
tsconfig.json has `files: []` and relies on project references (`tsc -b`) —
running plain `tsc --noEmit` from the root checks nothing. Only `pnpm run
build` (`tsc -b && vite build`), which is what CI's `test` job actually runs,
exercises real typechecking. Confirmed `pnpm run build` is now clean.
… commands

Stage 6 (backend half) of closing PR TabularisDB#822's remaining gaps.

Added the standard database: Option<String> override to the 7 commands that
actually perform a live query/execution for view/trigger/routine mutations:
get_view_definition, create_view, alter_view, create_trigger,
get_routine_edit_script, drop_routine, get_materialized_view_definition (the
last one fixes a latent bug from PR TabularisDB#822's Stage 4 — ExplorerSidebar.tsx
already sends `database` to this invoke call, but the backend silently
dropped it since the param didn't exist).

Deliberately did NOT touch get_create_table_sql, get_add_column_sql,
get_alter_column_sql, get_create_index_sql, get_create_foreign_key_sql,
build_routine_call_sql, or get_routine_create_template — these are pure SQL-
text generators that never touch a ConnectionParams/live connection (verified
against each driver's trait impl; several don't even take `params`, and the
ones that do leave it unused, prefixed `_params`). A database override there
would be a no-op parameter with no effect. Only the schema for the identifier
text matters, and that already gets threaded via the frontend modal changes
in this stage.
…abase aware

Stage 6 (part 2) of closing PR TabularisDB#822's remaining gaps.

CreateTableModal already had an explicit schema prop; added database
alongside it and a new CreateTableTarget "nested" kind (schema + database
together, distinct from the existing "database" kind which is flat-MySQL
where schema holds the database name).

ModifyColumnModal, CreateIndexModal, CreateForeignKeyModal had NO explicit
schema prop at all — they pulled activeSchema from connection context, which
already mistargeted a non-active schema in today's single-connection
Postgres tree (e.g. right-clicking a table in a schema other than the one
currently expanded). Added explicit schema/database props to all three,
falling back to the context value only when omitted, and threaded database
into every live invoke() call inside them (get_columns, get_tables,
execute_query) — the pure SQL-text generators (get_create_index_sql,
get_create_foreign_key_sql) don't need it, confirmed against their driver
trait impls.

ExplorerSidebar.tsx: threaded schema (single-schema branch) / dbName (flat
multi-db branch) into every onAddColumn/onEditColumn/onAddIndex/
onAddForeignKey modal-opening call that was missing it, and did the same for
the folder_indexes/folder_fks context-menu entries which read tableName from
contextMenu.data but never its schema/database. The plain single-connection
flat branch is unchanged (no schema concept for those drivers).
…nd their context-menu wiring

Stage 6 (part 3) of closing PR TabularisDB#822's remaining gaps.

ViewEditorModal, TriggerEditorModal, RunRoutineModal gained a database prop
alongside their existing (or newly explicit) schema prop, threaded into every
live invoke() call inside them (get_view_definition, execute_query preview,
create_view, alter_view, get_trigger_definition, drop_trigger, create_trigger,
get_routine_parameters).

ExplorerSidebar.tsx: the routine/trigger context-menu blocks already
extracted routineDatabase/triggerDatabase (PR TabularisDB#822) but dropped them when
opening these modals — now threaded into setRunRoutineModal, the
get_routine_edit_script invoke, setRoutineDropConfirm, and
setTriggerEditorModal (previously only drop_trigger used it). Widened the
shared runQuery() helper with an optional database param so "Edit Routine"
and "Run Routine" open their console tab against the right database too.

Known, pre-existing, unchanged limitation: "New Routine" (routines-new
context menu, triggered with no contextMenu.data at all) still always
targets the connection's global active schema regardless of which schema's
folder was right-clicked — not a regression from this branch, left as-is.
…/FK/view/trigger actions

Stage 6 (final part) of closing PR TabularisDB#822's remaining gaps.

SidebarNestedDatabaseItem's 9 mutation callbacks (onAddColumn, onEditColumn,
onAddIndex, onDropIndex, onAddForeignKey, onDropForeignKey, onCreateTable,
onCreateView, onCreateTrigger) previously all routed to a single
onUnsupportedAction placeholder that showed a "not yet supported" toast.
Replaced with real callback signatures carrying (schema, database) alongside
the existing SidebarSchemaItem-shaped args, wired in ExplorerSidebar.tsx to
open the now schema+database-aware modals from the previous three commits —
the exact same modals and backend commands the single-schema and flat
multi-db branches already use, just closing over the nested tree's own
(schemaName, databaseName) pair instead of a single qualifier.

Removed handleUnsupportedNestedAction and the sidebar.nestedDbActionUnsupported
i18n key (all 11 locale files) now that nothing routes to them — no dead
code or unused strings left behind.

This closes out Stage 6 of the PR TabularisDB#822 follow-up plan (CRUD mutations for
the nested tree). Stages 7 (ER diagram routing) and 8 (dump/import plugin-
bypass fix + nested picker) still to come on this branch.
… the nested tree

Stage 7 of closing PR TabularisDB#822's remaining gaps.

get_schema_snapshot gained the standard database: Option<String> override.
open_er_diagram_window's database_name param stays display/window-label-only
as before; a new, separate database param carries the routing value and is
only appended to the window's URL when explicitly provided — so the flat
multi-db (MySQL) and plain single-database call sites (which never pass it)
are completely unaffected.

Threaded a new `database` query param end to end: SchemaDiagramPage.tsx
reads it (distinct from `databaseName`, which resolveDiagramSchema already
uses as a MySQL-specific schema fallback) → SchemaDiagram.tsx →
EditorProvider's getSchema(), whose schema cache key now also includes
`database` so two databases with the same schema name don't collide.

Only the table-level "View ER Diagram" context-menu action (the one
nested-tree call site that already extracts ctxDatabase, from PR TabularisDB#822) now
passes it through. The toolbar, flat multi-db, and database-type
context-menu call sites are unchanged. Did not add a schema-picker dropdown
to the diagram window itself — out of scope for this routing-correctness
fix, tracked separately if wanted.
…or the nested tree

Stage 8 of closing PR TabularisDB#822's remaining gaps.

Backend: dump_database's two hard-coded driver-string match blocks
(table-listing and DDL-fetch) now route through the DriverTrait instead of
calling mysql/postgres/sqlite module functions directly, fixing the
"Unsupported driver" failure for any plugin-registered driver. Added a new
get_table_ddl method to the DriverTrait with implementations for all three
in-tree drivers (delegating to their existing module-level get_table_ddl
functions) and for RpcDriver (dispatching a new "get_table_ddl" RPC call).
The export_table_data step is intentionally unchanged — it opens its own raw
per-driver connection from ConnectionParams regardless of which driver id is
registered, doesn't have the "Unsupported driver" failure mode, and
rerouting it through execute_query would risk losing the JSON/binary type
fidelity that raw column introspection currently provides. The plugin-side
RPC handler is tracked in a follow-up issue on tabularis-postgresql-plugin.

Frontend: DumpDatabaseModal gained explicit schema/database props for the
nested case, reads its table list from nestedDatabaseDataMap (already loaded
by the sidebar) instead of triggering a fresh fetch, and scopes the
dump_database invoke correctly. ImportDatabaseModal similarly gained an
explicit schema prop overriding the global activeSchema. SidebarNestedDatabaseItem
now has onDump/onImport optional callbacks (with Download/Upload icon buttons
in the database header when provided), wired in ExplorerSidebar. handleImportDatabase
widened to accept an explicit schema.
SchemaDiagramPage.tsx now fetches the available schemas for the connection
(scoped to the selected database when opened from a nested multi-db tree)
and renders a <select> in the window header so users can switch which schema
the diagram shows without reopening the window.

The picker is only shown for schema-capable drivers (identified by the
presence of an explicit `schema` URL param — MySQL flat-db connections
where "schema" is the database name don't have one and don't need a
schema switcher). Changing the selection re-renders the canvas immediately
via refreshTrigger. Falls back gracefully when get_schemas fails (keeps
the initial schema).
…base override

Three new #[ignore] tests that run against the live pg-tabularis-test
container (port 54320), validating the exact params.database = Single(db)
override pattern used by every Tauri command added in Stage 1-8:

- test_database_override_routes_to_secondary: starts with testdb, overrides
  params.database to tabularis_test_secondary, calls get_schemas — confirms
  secondary_schema is visible and test_schema (testdb-only) is not.
- test_get_tables_with_database_and_schema_override: same override, then
  get_tables for secondary_schema — confirms the remote_data table is
  reachable from a params object that originally pointed at testdb.
- test_empty_string_filter_prevents_maintenance_db_override: baseline check
  that testdb params still see testdb's own schemas, confirming the
  .filter(|d| !d.is_empty()) guard doesn't clobber valid connections.

All 10 multi_database integration tests pass against the live container.

This branch has not been deployed

No deployments
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]: Browse/access all databases on same connection

1 participant