feat(postgres): browse multiple databases on one connection - #822
Draft
aesslinger wants to merge 13 commits into
Draft
aesslinger wants to merge 13 commits into
aesslinger wants to merge 13 commits into
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
hasOptedIntoDatabaseSelectionhelper 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 persisteddatabasefield as the opt-in signal, so no existing single-database connection is silently reclassified.isMultiDatabaseCapable(MySQL) is untouched.Backend. A
database: Option<String>override — cloningConnectionParams, 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 matcheddriver.as_str()against hardcoded strings, returning "Unsupported driver" for any plugin-registered driver. Both now route through theDriverTrait(get_tables, newget_table_ddlmethod). 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.
DatabaseProvidergained anestedDatabaseDataMapand aconnect()branch. A newSidebarNestedDatabaseItemrenders each selected database's own schema picker and reusesSidebarSchemaItemper schema. All 7 mutation modals,DumpDatabaseModal,ImportDatabaseModal, and the ER diagram window route correctly using both schema and database.Tab/AddTabInput/EditorNavigationRequestcarry a newdatabasefield threaded throughdatabaseObjectActions.tsanduseDatabaseObjectNavigation.Known limitation
get_table_ddlin 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 theparams.databaseoverride 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 buildandpnpm lintclean.Still needs a human — app running against real Postgres + plugin. Checklist: