feat(postgres): browse multiple databases on one connection - #822
Draft
aesslinger wants to merge 45 commits into
Draft
aesslinger wants to merge 45 commits into
aesslinger wants to merge 45 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.
…ping
Multi-database opt-in Postgres-plugin connections persisted `database` as
either an array ("Choose databases") or an empty string ("All databases",
the default mode). Both shapes reach the plugin's ConnectionParams.database:
Option<String> as either None (array silently lost by Value::as_str()) or
Some(""), and deadpool-postgres rejects both — "configuration property
"dbname" not found" / "...contains an empty string" — since test_connection
and ping run before any per-command database override exists.
Add with_primary_database() to coerce database to a single, non-empty name
before these two RPCs, falling back to the "postgres" maintenance db (same
convention the built-in driver already uses for get_databases) when the
selection is empty and the plugin's manifest declares a postgres/postgresql
engine. Gated on the manifest engine so it doesn't leak Postgres-specific
assumptions into other plugin drivers.
…ch other setSelectedSchemasForDatabase, loadNestedSchemaData, refreshNestedSchemaData, and loadNestedSchemas each rebuilt the whole nestedDatabaseDataMap from a closure-captured connectionDataMap snapshot. Confirming a schema selection on a non-primary database (in the nested multi-db tree) fired three sequential updates from independently stale closures: the load kicked off by the confirm, and the confirm's own activeSchema follow-up, both overwrote needsSchemaSelection back to true right after it was cleared. The picker reappeared with nothing checked and no schema ever loaded. Add updateNestedDatabaseData(), which patches one database's nested state through a functional setConnectionDataMap(prev => ...) updater instead of a stale closure, and route all four functions through it. This also fixes a related latent bug where those functions' own "fresh" re-reads after an await were reading the same stale closure, not live state. Add a regression test that reproduces the exact symptom; verified it fails against the old code and passes with the fix.
The single-database Postgres sidebar has always had a gear icon next to its schema header to re-open the checkbox picker and change the selection after confirming. The nested multi-db tree's per-database schema picker (added for PostgreSQL's opt-in multi-database mode) never got the same affordance -- once confirmed, a database's schema selection was permanent short of removing and re-adding it via the Databases tab. Add the same gear-icon edit dropdown to SidebarNestedDatabaseItem, reusing the already-fixed setSelectedSchemasForDatabase and the existing sidebar.editSchemas/selectAll/deselectAll/confirmSelection i18n keys.
SidebarTableItem.refreshMetadata destructured `database` from props but never forwarded it to get_columns/get_foreign_keys/get_indexes, unlike the sibling SidebarViewItem which already does this correctly. For a table in a non-primary database (the nested multi-db tree), all three queries ran against the connection's primary database instead, where the table typically doesn't exist -- so every table showed 0 columns, 0 foreign keys, and 0 indexes despite the table itself loading real data. Also add `database` to areTableItemPropsEqual's compared fields (same reason the existing `capabilities` comparison was added): without it, a database-only prop change on an otherwise-identical memoized instance would be silently skipped by React.memo. Add regression coverage: a component test asserting database is forwarded to all three invoke calls (and omitted when absent, matching the existing single-db behavior), a re-render test proving a database prop change actually triggers a re-fetch, and a comparator test case for the new compared field.
…t_connection We've now hit the same crash twice: 'Pool creation failed: configuration property "dbname" not found', first on connect (fixed in 314a5c9 for ping/test_connection only), and now again from dropping a table in the nested multi-db tree -- a schema refresh after the drop called get_tables without a database override, sending the connection's raw DatabaseSelection::Multiple(...)/empty Single("") straight to the plugin. An audit found only 2 of 52 RpcDriver methods were protected. Every other method independently forwards "params" to the plugin subprocess, so any command missing a database override (there are several with none at all: get_available_databases, get_ai_schema_context, build_routine_call_sql, save_blob_to_file, fetch_blob_as_data_url, all user-management commands, every MCP tool) -- or a frontend call site that omits one it should have passed -- hits the same crash. Rather than whack-a-mole individual methods as they're found, apply with_primary_database() at every "params" JSON-RPC payload site in this impl (50 call sites, including for_connection's get_connection_metadata -- the earliest point any of these commands reach the plugin). For an already-resolved single database (the normal case for both single-db connections and per-command multi-db overrides), primary() returns it unchanged, so this is a no-op there; it only changes behavior for the specific gap case, converting a connection-wide crash into a safe fallback to the primary/first-selected database. Add regression tests for get_schemas (the drop-table repro's actual failing call) and get_tables, mirroring the existing ping/test_connection coverage.
…porting Both handlers called the generic connection-level refreshTables() after a mutation that had itself correctly targeted a specific database in the nested multi-db tree (drop-table already passed ctxDatabase/ctxSchema to execute_query; the import modal already resolved its targetDatabase from importModal.database/schema). refreshTables() calls get_tables with no database override, so for a multi-db opt-in connection it sent the raw unresolved database selection to the plugin -- before the driver.rs fix, crashing the whole connection's sidebar with the same "dbname not found" error the connect-time bug produced; now (with that fix) it silently falls back to refreshing the primary database's tables instead of the one actually mutated. Route both through refreshNestedSchemaData(database, schema) instead when the action targeted a nested database, leaving refreshTables() untouched for the single-database and flat (schema-less) multi-db cases, which were never affected. The import fix keys off importModal.schema rather than importModal.database, because database always has a value (it falls back to activeDatabaseName for a plain single-db import) while schema is only ever set by the nested tree's own Import button.
findExistingTableTab deduplicated table tabs on (connectionId, activeTable, schema) only. For a nested multi-db connection, the same table name + schema can exist in more than one database (or a tab opened before database was threaded through at all can be sitting in state with no database set) -- double-clicking a table would reactivate that stale tab instead of opening a fresh one, reusing its wrong/missing database. If that tab's query then re-ran with no database override, it sent the connection's raw multi-db selection to the plugin: the same 'Pool creation failed: dbname not found' crash as the other two instances of this bug, now from simply opening a table. Add database to the match criteria and thread it through from addTab's partial. Single-database and flat multi-db tabs are unaffected -- both sides of the comparison are undefined there, same as before.
… or trigger ViewEditorModal/TriggerEditorModal's onSuccess handlers always called the generic connection-level refreshViews()/refreshTriggers(), which write into the top-level views/triggers arrays. Those are only ever rendered for a schema-less driver -- every schema-having case (a single connection's own schema, a database in the nested multi-db tree, or a database in the flat schema-less multi-db list) renders from schemaDataMap/nestedDatabaseDataMap/ databaseDataMap instead, which the generic refresh never touches. A newly created view or trigger was therefore invisible in the sidebar for any of those three cases -- confirmed via psql that the view actually exists; the sidebar just never asked for it again. Add refreshObjectScope(), shared by both onSuccess handlers, which resolves the same nested/schema/database ambiguity refreshAfterCreateTable already handles for tables (a flat multi-db modal reuses the schema field to carry a database name; activeCapabilities?.schemas disambiguates it from a real schema, since each is only reachable from the sidebar branch gated on that same flag), falling back to each modal's original top-level refresh when neither is set.
…tine Same gap as view/trigger creation (94dcc3e), just on the drop side: the drop-view, drop-trigger, and drop-routine context-menu actions already scoped the drop_view/drop_trigger/drop_routine call correctly via schema/database, but then refreshed with the generic connection-level refreshViews()/refreshTriggers()/refreshRoutines() -- a no-op for any schema-having case, so the dropped object stayed listed until a manual refresh. Route all three through the same refreshObjectScope() helper. Column/ index/FK actions on an existing table were never affected by this class of bug -- they bump schemaVersion instead, which SidebarTableItem's own (already-fixed) per-table metadata refetch picks up; only whole-object create/drop for views, triggers, and routines goes through the schema's own object list in schemaDataMap/nestedDatabaseDataMap/databaseDataMap.
…nline body TriggerEditorModal's guided mode generated the same CREATE TRIGGER ... BEGIN ... END template for every driver, but PostgreSQL has no inline trigger body -- a trigger can only reference a separate trigger function. Every PostgreSQL trigger created through guided mode failed with a syntax error (TabularisDB#837, found while testing PR TabularisDB#822). Extract the SQL generation into src/utils/triggerSql.ts (now unit-tested). For postgres/postgresql, generate a CREATE OR REPLACE FUNCTION ... RETURNS TRIGGER AS $$ ... $$ LANGUAGE plpgsql statement plus a CREATE TRIGGER ... EXECUTE FUNCTION fn() referencing it, run as two separate calls (via execute_query then create_trigger) since neither Postgres client here supports multiple statements in one call. The function name is deterministic (<trigger_name>_fn) and schema-qualified in *both* statements -- qualifying only the table (matching the existing pattern) left CREATE FUNCTION and EXECUTE FUNCTION as two calls that aren't guaranteed to resolve an unqualified name against the same search_path, which surfaced its own 'function ... does not exist' failure even after the function had just been created successfully. Also clean up the function when a trigger created this way is dropped via the sidebar's context menu (DROP FUNCTION IF EXISTS, no CASCADE, and the drop itself is not blocked if cleanup fails) -- otherwise every drop would leave an orphaned function behind. CREATE OR REPLACE makes re-saving/editing a trigger idempotent, so the modal's own edit flow (drop trigger, recreate function + trigger under the same name) needed no separate cleanup step. MySQL and SQLite are unaffected: both still get the original inline BEGIN/END body, which is valid for both. Verified end-to-end against a live PostgreSQL connection -- create, verify SQL, drop, and confirm via psql that both the trigger and its function are fully removed.
…ones dump_database's structure export already went through the generic DatabaseDriver trait (get_table_ddl, fixed for the plugin path by PR TabularisDB#119), but three other spots in the same dump/import flow only matched the builtin driver id "postgres" literally, silently breaking or hard-failing for the "postgresql" plugin driver: - format_table_ref (dump_utils.rs) fell through to its unqualified '_' arm for "postgresql", so a plugin-backed dump's DROP TABLE/INSERT statements silently lost their schema qualifier -- not just an error, a statement that could hit the wrong same-named table in a different schema. - dump_database's schema_for_listing gate had the same literal check, so get_tables() would list tables from every schema. - export_table_data's row-export match only had "mysql"/"postgres"/ "sqlite" arms, each streaming rows through that driver's own local connection pool -- structurally impossible for a plugin, which owns its pool inside its subprocess. Any other driver id hit a hard 'Unsupported driver' error. Widen the two string checks to also match "postgresql" (same fix pattern as TabularisDB#614, just in files that predated or were missed by it), and replace export_table_data's error fallback with a generic RPC-based path: paginate through execute_query, using get_columns beforehand to know which columns are JSON/JSONB (QueryResult only carries column names, not types, so a JSON column can't be told apart from a plain text column by inspecting a cell's value alone). Extracted the column-classification and row-formatting logic into pure, unit-tested helpers. import_database has the identical shape of gap -- three builtin-only transaction blocks with the same 'Unsupported driver' fallback -- fixed with an analogous generic path via execute_query per statement. This one is NOT atomic (no cross-plugin RPC for transaction begin/commit/rollback exists yet, unlike the builtin drivers' single wrapped transaction); schema-scoping doesn't depend on that, since it's passed per-call rather than relying on session state persisting across pooled connections. Verified end-to-end: dumped a nested multi-db PostgreSQL-plugin connection's non-primary database, confirmed schema-qualified DDL/DML and the composite-PK/view-exclusion checks from PR TabularisDB#822's own test plan.
…i-db tree The nested multi-db tree (this PR's own new feature) had no way to open an ad-hoc console scoped to a database with no existing table or routine to anchor a context-menu action on -- confirmed while testing A.4's import step against a freshly-created, still-empty target database. The flat schema-less multi-db path (MySQL) already has this via the console's own database-selector dropdown; that dropdown is deliberately hidden for a schema-based driver like PostgreSQL (it reuses the `schema` field to carry a database name, which would be wrong for a real schema), and nothing was added to replace it for the new nested tree. Add a "New Console" icon to SidebarSchemaItem's header, next to the existing Refresh icon. Since SidebarSchemaItem already receives its `database` prop from the nested tree (undefined for a plain single-database connection), one handler in ExplorerSidebar works correctly for both: a single-schema connection opens an unscoped console exactly as before, and a nested database opens one correctly scoped to both the schema and the database.
…he schema list loadNestedSchemas guards on `schemasLoaded` to make expanding a database lazy-load its schema list only once -- correct for that purpose, but the same guard made the database row's own Refresh button (which calls this same function) a permanent no-op after the very first load: clicking it any number of times could never surface a schema created afterwards (hit directly while testing A.4 -- a `store` schema created via the new per-schema console, from the previous commit, never appeared no matter how many times Refresh was clicked, even though psql confirmed it existed in the right database). Add a `force` parameter that bypasses the `schemasLoaded` check (but not the concurrent-fetch guard) and wire the Refresh button to pass it. The initial lazy-load-on-first-expand call site is unaffected -- it still omits `force`, so it still loads a nested database's schemas exactly once.
The connection-level header's "View ER Diagram" (and Import/Dump) button
was only ever gated off for the flat schema-less multi-db case
(`!isMultiDb`, using `usesMultiDatabaseLayout` — schemas === false), never
for the nested schema-based case this PR adds. It stayed visible for a
nested connection and opened a diagram using activeDatabaseName/
activeSchema — the connection's top-level fields, which a nested
connection never populates (each database's own active schema lives in
nestedDatabaseDataMap instead). Confirmed live: clicking it opened a
diagram with no schema picker and no tables at all.
Gate it on `isNestedMultiDb` (isSchemaBasedMultiDb) too, matching the
flat case's own comment ("actions move to each database node"). The
nested tree has no context menu on the database/schema row to reach an
equivalent (only a table's context menu correctly passes database+schema,
once a table exists to right-click) -- add a "View ER Diagram" icon to
SidebarNestedDatabaseItem's header, next to its existing Import/Dump/
Refresh/New Console icons, using the row's own databaseName and
activeSchema so it's always correctly scoped.
…ks get_schema_snapshot Opening the ER diagram for a nested multi-db PostgreSQL-plugin database rendered a silently blank canvas: get_schema_snapshot is a batch RPC the plugin doesn't implement (returns "method not found"), and the frontend's catch block only logged the error to the console -- no user-facing message at all, so an empty canvas was indistinguishable from "this schema genuinely has no tables". RpcDriver already has this exact fallback shape for other optional/batch RPCs (get_materialized_views, save_blob_to_file, etc.) -- an empty-Vec fallback when the plugin doesn't implement them. That shape doesn't fit here: unlike materialized views, "no tables" is never a valid reading for a schema snapshot. Compose the same TableSchema[] shape instead from get_tables/get_columns/get_foreign_keys, which the plugin does implement (confirmed throughout this branch's own manual testing). Also stop swallowing the error silently on the frontend -- show an alert with the actual failure reason, so a future gap in this class (a plugin missing some other optional metadata RPC) fails loud instead of looking like an empty result. New i18n key added to all 11 locales. The plugin-side gap (get_schema_snapshot, and two related unused batch methods, are all `not_implemented` stubs) is filed as its own issue for the plugin repo separately, since the fix belongs there long-term for performance (this fallback is O(tables) round trips, not one batch call).
… into feat/postgres-multi-database-v2
cleanTabForStorage built an explicit object literal that omitted Tab.database, so nested multi-db table tabs lost their database routing after a restore, silently re-running queries against the connection's primary database instead.
Every other RpcDriver call site coerces a Multiple/empty database selection via with_primary_database before the JSON-RPC call; get_table_query_template was the one exception, so a multi-db Postgres-plugin connection generating a query template would send the plugin a raw JSON array where it expects a string, or an empty selection with no fallback.
for_connection stored the raw params.clone() as connection_params, which call_with_connection injects verbatim into SQL-building RPCs (create table/column/index/FK DDL templates, routine templates, privilege catalog). A multi-db opt-in connection's Multiple(...) selection reached the plugin as a bare JSON array instead of the coerced primary database string every other call site already uses.
Every sibling metadata command (get_columns, get_foreign_keys, etc.) accepts an optional database param to retarget a non-primary database on a multi-db connection; get_table_query_template was missing it, so Generate SQL templates always used the connection's primary database regardless of which one the user was browsing.
TableTarget had no database field, so GenerateSQLModal's get_columns/ get_foreign_keys/get_indexes/get_tables/get_table_query_template calls always resolved against the connection's primary database. Generate SQL on a non-primary database in the nested multi-db tree would silently show SQL for the wrong database's table. The request cache key also omitted database, so switching tables of the same name across databases could serve stale cached SQL.
…base through the row editor save_blob_to_file/fetch_blob_as_data_url had no database parameter, so BLOB download/preview always targeted the connection's primary database on a multi-db connection. Wiring that up surfaced a broader, pre-existing gap: DataGrid never received schema/database as props at all, reading the connection's globally active schema from context instead of the active tab's own schema. That could already misdirect a BLOB fetch on a plain single-schema-picker connection if the sidebar's selected schema changed while a background tab stayed open — multi-db just adds a second, parallel instance of the same problem for database. Both are now threaded as explicit props through DataGrid -> RowEditorPanel -> FieldEditor -> BlobInput, sourced from the active tab (tab value wins, falls back to the global active schema to match this codebase's existing tab.schema ?? activeSchema convention).
useCommandPaletteObjectItems gated its loading effect on hasSchemas (capabilities.schemas === true), which is also true for a nested multi-db connection, and unconditionally read the top-level selectedSchemas/schemaDataMap — fields a nested connection never populates, since its schema state lives per-database in nestedDatabaseDataMap instead. The palette's object search silently returned nothing for these connections. Added a nested branch to the loading effect (loadNestedSchemaData per selected database x schema), a third database-then-schema branch in getNavigatorItems, and a database field on NavigatorItem/ NavigatorGroup/DatabaseObject so the palette's Inspect/New Console/ Generate SQL/Open/Count actions all target the right database - reusing the database field already on TableTarget/DatabaseObjectTarget from the dump/GenerateSQL fixes earlier in this pass.
…ump modal The refresh-on-open effect explicitly excluded the schema-based (nested multi-db) case, relying entirely on the sidebar having already expanded that schema. If it hadn't, nestedDatabaseDataMap had no entry for it and the dump modal's table list stayed permanently empty with no loading state and no way to recover short of expanding the schema elsewhere first.
get_schema_preference looked up only the database-scoped key (connection_id::database). A connection's schema preference saved before it opted into multi-database browsing lives under the old flat connection_id key, so it was silently orphaned and lost the first time each database was looked up under its new per-database key. Extracted the lookup into a pure resolve_schema_preference so it's unit-testable without an AppHandle.
…ing plugin drivers export_table_data's plugin fallback defaulted a missing pagination field to has_more=false, silently truncating any table with more than one page (1000 rows) of data when a plugin's execute_query RPC doesn't populate pagination - the dump looked complete but wasn't, with no error raised. Extracted plugin_dump_has_more, mirroring export.rs's existing fetched >= page_size fallback for the same gap.
get_table_ddl's error propagated via ? straight out of dump_database's task, so a plugin that doesn't implement it (structure dumps enabled) aborted the entire dump after the first table - no data for any table got written either, even though every other RPC the dump needs works fine. Considered composing an approximate CREATE TABLE from get_columns/ get_foreign_keys/get_indexes (as the get_schema_snapshot fallback does for the read-only ER diagram) and rejected it: a dump is meant to be restored, and a plausible-looking but subtly wrong DDL statement is worse than a visible gap. The structure step now writes a WARNING comment for that table and the dump continues - data export for it, and everything else, is unaffected.
… it's Postgres-flavored Both export_table_data's and import_database's generic plugin fallbacks unconditionally passed a public-defaulted schema to execute_query/get_columns for every plugin driver, regardless of engine. A non-Postgres plugin's schema RPC parameter may mean something else entirely (e.g. a database selector for a MySQL-like plugin with no real schema concept), so this could silently retarget such a connection to a database that doesn't exist instead of leaving the plugin's own default alone. Extracted is_postgres_driver and gated both fallbacks on it.
… stringified message is_method_not_found checked a stringified error for "method not found"/"-32601", but PluginCallError's Display only ever writes error.message - the JSON-RPC code is never present in that string at all. A plugin returning a proper -32601 with any message other than that exact English phrase (a different wording, a non-English message) silently bypassed every one of the 13 method-not-found fallbacks in this file, propagating a real error where a graceful degradation was intended. Switched every one of those 13 call sites from call() (which discards the structured PluginCallError into a plain String before the check ever runs) to call_detailed(), and is_method_not_found now matches on the Remote variant's actual code - falling back to the message-string heuristic only for Transport errors, which have no JSON-RPC code at all. Added call_with_connection_detailed alongside call_with_connection for the one fallback (routine_create_template) that goes through that path instead of a direct process.call.
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; New Console; 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. Every one of these actions now has the same feature parity in the nested database tree that it already has for single-database and flat (MySQL-style) multi-database connections.
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. A newisSchemaBasedMultiDb/isSchemaBasedMultiDbCapablepair (src/utils/database.ts) identifies the nested (schema-per-database) case specifically, distinct from the flat multi-db case, so connection-level chrome (like the header's Import/Dump/ER-diagram actions) only shows where it's actually wired to work.Backend. A
database: Option<String>override — cloningConnectionParams, filtering empty strings, overriding.database— is 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. The same coercion is applied on the plugin RPC path (RpcDriver::with_primary_database) across every JSON-RPC call the driver makes, not just connection-test calls, so plugin-backed Postgres drivers get the same per-database targeting as the builtin client. Schema selection and preference persistence is scoped per-database via a composite storage key.Dump/import works for plugin-backed drivers too.
dump_database's table-listing and DDL-fetch previously matcheddriver.as_str()against hardcoded"postgres"strings, returning "Unsupported driver" for the plugin-registered"postgresql"driver id. Table listing and DDL now route throughDriverTrait(get_tables,get_table_ddl— now implemented plugin-side in tabularis-postgresql-plugin#119). The data-export and import steps are now generalized too: for any driver that isn't one of the builtin SQL clients, export paginates throughdrv.execute_query/get_columnsover the RPC boundary (tracking JSON/JSONB columns separately so their values are still escaped correctly instead of losing type fidelity), and import replays each parsed statement individually throughdrv.execute_query(non-atomic, since there's no cross-plugin transaction RPC). Several otherdriver == "postgres"literal-string checks in the dump path were widened to also match"postgresql".Postgres trigger SQL dialect. Postgres has no inline trigger body — a trigger needs a backing function.
src/utils/triggerSql.tsbuilds both statements (CREATE OR REPLACE FUNCTION ... RETURNS TRIGGER ... LANGUAGE plpgsqlandCREATE TRIGGER ... EXECUTE FUNCTION), schema-qualifying the function name consistently in both since the two calls don't share asearch_path.TriggerEditorModalexecutes the function-creation statement before callingcreate_trigger.ER diagram works even when a plugin doesn't implement batch schema metadata.
RpcDriver::get_schema_snapshotnow falls back to composing the snapshot fromget_tables/get_columns/get_foreign_keyswhen a plugin doesn't implement the batch RPC (tabularis-postgresql-plugin#121 tracks a real plugin-side implementation for better performance). The frontend also now surfaces schema-load errors instead of silently leaving the canvas blank.Frontend.
DatabaseProvidergained anestedDatabaseDataMapand aconnect()branch, with all writes going through asetState-based updater to avoid stale-closure races between concurrent per-database loads. A newSidebarNestedDatabaseItemrenders each selected database's own schema picker, wires New Console and View ER Diagram actions, and reusesSidebarSchemaItemper schema (including its own New Console action). 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. Refresh-after-mutation logic (create/drop view, trigger, routine, table) was extended to disambiguate nested (real database+schema), flat multi-db (schema field reused for a database name), and plain single-schema cases.Follow-up local code-review pass. After the manual walkthrough above, a local multi-angle code review of this branch's diff surfaced several remaining gaps in
database-override coverage that the manual test plan didn't happen to exercise: a tab'sdatabasefield wasn't persisted across app restart (tabCleaner.ts), twoRpcDrivercall sites (get_table_query_template's RPC, and the metadata snapshotfor_connectionhands tocall_with_connection) skipped thewith_primary_databasecoercion every other call site has, theget_table_query_templateTauri command had nodatabaseparameter at all, andGenerateSQLModalnever threaded a table's database into its metadata/template fetches or its cache key. Each is fixed with its own commit and a regression test that fails without the fix.One of these follow-ups (BLOB download/preview for a table in a non-primary database) turned out to be a symptom of something broader:
DataGridnever receivedschema/databaseas props at all — it read the connection's globally active schema from context instead of the specific tab's own schema. That's a latent bug independent of multi-database work (a single-schema-picker connection with two tabs open on different schemas could already misdirect a BLOB fetch if the sidebar's schema selection changed while a background tab was open), which this PR'sdatabasefield surfaced rather than caused. Rather than bolt on adatabase-only patch beside a still-wrongschemapath, both are being threaded properly as explicit props throughDataGrid→RowEditorPanel→FieldEditor→BlobInput, sourced from the active tab.Verification
Automated:
cargo test --libandcargo test --test postgres_integration -- --ignored(live Postgres 16 container) pass, including tests added in this branch for the database-override routing, the JSON/JSONB-aware dump formatting, and theget_schema_snapshotfallback.pnpm vitest run,pnpm run build, andpnpm lintare clean, including new tests fortriggerSql.ts, the nested-tree New Console/ER-diagram actions, and the schema-list force-refresh fix.Manually verified against a real Postgres 16 instance + the plugin-backed driver, on this branch:
store→analytics) without reopeningBugs found during this manual pass were fixed directly in this branch rather than deferred, since they were feature-parity gaps between the new nested tree and the existing single-db/flat-multi-db behavior: refresh-after-mutation scoping, the trigger SQL dialect issue, the New Console and ER diagram actions missing from the nested tree, a permanent no-op on the nested tree's schema-list refresh button, and the ER diagram blank-canvas issue above. A plugin-side stub (
get_schema_snapshot) that the host now works around is filed as tabularis-postgresql-plugin#121; a related plugin-side trigger validation gap is filed as tabularis#837 (fixed as part of this same branch since it surfaced during this testing pass).