Skip to content

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

Draft
debba wants to merge 10 commits into
mainfrom
feat/postgres-multi-database
Draft

feat(postgres): browse multiple databases on one connection#402
debba wants to merge 10 commits into
mainfrom
feat/postgres-multi-database

Conversation

@debba

@debba debba commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #340.

What this does

You can now point a single PostgreSQL connection at several databases and browse them all from the sidebar, the same way MySQL and MariaDB connections already work. Pick the databases you want in the connection dialog's "Databases" tab and each one shows up as a top level node you can expand into its schemas and tables.

Why Postgres needed special handling

MySQL lets one connection see every database, so the old multi database code treated a database name as if it were a schema and qualified table names with it. Postgres does not allow that: a connection is bound to one database and you cannot query across databases in a single session. So this PR keeps a separate connection pool per database and presents a real database to schema to table tree instead of the flat database to table layout MySQL uses.

The pool keying already included the database name, so most of the plumbing was about telling the backend which database a given read or query should run against.

How it works

  • Metadata reads (get_tables, get_views, get_schemas, get_columns, get_indexes, get_foreign_keys, the routine and trigger helpers) and execute_query / execute_query_batch now take an optional database argument. When set, the params get pointed at that database and the matching pool is used. This mirrors the convention the record mutation commands (delete_record, update_record, insert_record) already follow, so it is purely additive.
  • Editor tabs carry the target database alongside the schema, so previews, queries and row edits route to the correct pool while still qualifying tables with the Postgres schema.
  • isMultiDatabaseCapable no longer excludes schema based drivers; a new isSchemaBasedMultiDb helper distinguishes the hierarchical Postgres layout from the flat MySQL one.
  • Empty database selections fall back to the postgres maintenance database, since Postgres cannot connect without a target. This is what makes "Load Databases" work before you have picked anything.

UX change worth a look

Now that PostgreSQL counts as multi database capable, the connection dialog hides the single "database" field on the General tab and shows the "Databases" tab with a checklist instead, exactly like MySQL. Existing single database Postgres connections keep working, but the edit dialog looks different than before. Happy to keep the single field visible for Postgres if reviewers would rather not change that flow.

Known limitations / follow ups

These are not regressions, just things left out of this pass to keep the change focused on browsing and querying:

  • Object-creation DDL from the nested schema view is not database aware. Create Index, Create Foreign Key and Create Table / View / Trigger launched from a nested Postgres schema node still route via the connection's global active schema and carry no database, so they can hit the primary database (and the wrong schema). Add/edit column, drop index and drop foreign key are routed to the node's database in this PR.
  • SQL autocomplete is not database aware in multi-database mode. registerSqlAutocomplete / getTableColumns take a schema but no database, so get_columns runs against the connection's primary pool — completions for tables in a non-primary database are missing or wrong. It still assumes the flat MySQL shape.
  • Per-database dumps route to the correct database but scope schema via the global active schema. dump_database is bound to the selected database's pool (Postgres now counts as multi-database capable), but the schema it receives is the connection-wide active schema, so a dump taken while a different schema is active may be scoped incorrectly.
  • Sidebar highlight and auto-expand reuse activeSchema for both the database node and the schema node, so the highlight can be slightly off. Navigation works fine.
  • No cap or idle eviction on the per-database pools yet. Each selected database keeps its own pool (max 10 connections) for the lifetime of the connection; pools are released only on explicit disconnect.

Testing

  • New unit tests for the postgres_dbname fallback helper and for the isMultiDatabaseCapable / isSchemaBasedMultiDb changes.
  • Full suites pass locally: 753 Rust tests, 2705 frontend tests, plus tsc and ESLint clean.
  • Still needs a manual pass against a real Postgres server with two or more databases selected.

debba added 4 commits June 30, 2026 19:05
Lets a PostgreSQL connection hold and browse several databases at once,
the same way MySQL/MariaDB connections already can. Because Postgres
binds a connection to one database and cannot query across databases,
each selected database gets its own pool and the sidebar shows a
database to schema to table tree.

Backend reads (get_tables, get_views, get_schemas, get_columns, etc.)
and execute_query now take an optional database argument that routes the
work to the right pool; the convention matches the existing record
mutation commands. Empty selections fall back to the postgres
maintenance database since Postgres cannot connect server-wide.
…nd tab persistence on multi-database connections

On schema-based multi-database (PostgreSQL) connections the backend keeps a
separate pool per database, so every table-scoped call must carry both the
schema and the database. Several paths dropped the database, which made them
hit the connection's primary database and fail with relation-not-found:

- PK/column metadata (fetchPkColumn, new-row, insert validation) omitted the
  database, leaving pkColumns null and silently turning the grid read-only.
  Added buildTableRoutingParams() to centralize schema+database routing.
- Related-records / FK navigation ran execute_query without the database and
  dropped the referenced schema. Added ref_schema to ForeignKey (Rust + TS,
  mapped from foreign_schema_name) so cross-schema FKs qualify correctly, and
  routed the related-records query to the tab's database.
- Editor tabs lost their database on save: cleanTabForStorage didn't persist
  the field, so a restored tab queried the primary database. Also made
  findExistingTableTab database-aware so same-schema tables in different
  databases no longer reuse the wrong tab.

Adds a multi-schema demo database (erp_demo: hr/inventory/sales + cross-schema
FKs) and tests for the routing/persistence regressions.
…ulti-database connections

Replace the per-schema tree nodes under a database with a single compact
schema dropdown: pick one schema and its tables/views/routines render
directly below, like TablePro. Adds a hideHeader mode to SidebarSchemaItem to
render a schema's contents without its collapsible header, a
resolveActiveSchema() helper (picked -> connection-active -> public/first),
and optional triggerClassName/leadingIcon props on the shared Select so the
sidebar trigger is compact with a schema icon. Adds the sidebar.schema key to
all locales and tests for resolveActiveSchema.
The schema-based multi-database tree (database → schema → table) fetched
table metadata against the right connection pool, but right-clicking a
table dropped the database entirely: ContextMenuData and the context-menu
payload only carried schema, so Show Data, New Console, Count Rows,
Delete Table, View Schema, Generate SQL, Add Column, drop index/foreign
key, and the ER Diagram all silently fell back to the connection's
default database.
@NewtTheWolf

Copy link
Copy Markdown
Collaborator

Reviewed locally (tests pass: frontend 140, backend pool_manager 36). The architecture is solid — per-database pool keying (postgres:conn:<id>:<db>), the postgres maintenance-db fallback is contained to connection params (never reaches SQL), and the multi-db logic is capability-driven (no driver === "postgres" conditionals). Nice.

The one real issue is that database threading is incomplete — three call paths still route to the connection's primary DB:

Blockers

  • execute_query_batch (src-tauri/src/commands.rs:3172) never got the database param (only schema), so batch/multi-statement execution runs against the primary DB. execute_query does it right at commands.rs:3104 — mirror that.
  • Export sends the schema as the database — src/pages/Editor.tsx:2704 uses activeTab?.schema as targetDatabase. For Postgres (now multi-db-capable) that routes to the wrong pool. Use the buildTableRoutingParams(...) helper the PR already added (it's used at 575/1959/2132, just not here).
  • count_query has no database — frontend src/pages/Editor.tsx:1141 and backend src/commands.rs:3321 both lack it, so pagination totals run against the primary DB → wrong count or error for non-primary databases.

Nits

  • delete_record/update_record/insert_record (commands.rs:2761, :2792, :3045) use if let Some(db) = database without the .filter(|d| !d.is_empty()) guard the 13 read commands use — with the new maintenance-db fallback, a Some("") write lands on the postgres database.
  • driver === "mysql" added at NewConnectionModal.tsx:1187 (pipes_as_concat) violates .rules/frontend.md no-driver-conditionals.
  • The MySQL/SQLite pk_col → composite pk_map rework + added escape_identifier is a behavioural refactor riding along (.rules/rust.md Does not list databases, have to manually enter database name in connection window #6). Good change on its own (closes an identifier-injection vector, unit-tested) but ideally its own PR.

Repro for the three blockers, on a connection with multiple Postgres DBs selected: run a multi-statement query in a non-primary DB (B1), paginate a large table in a non-primary DB (B3 count), export a non-primary-DB table to CSV (B2) — each hits the primary pool instead.

debba added 6 commits June 30, 2026 22:13
…resh sidebar after table drop

Editing a timestamp/timestamptz/date/time/interval cell failed with
"error serializing parameter N" because tokio-postgres infers a CAST($N
AS type) placeholder's effective type from the cast target, rejecting a
bound Rust String before PostgreSQL's own text-to-temporal parsing ever
runs. The same root cause silently affected uuid-shaped string binds
(#392 was only a partial fix, for varchar PK columns). Every bound
PostgreSQL parameter now declares its wire type explicitly via
prepare_typed/execute_typed, so the client-side type check matches what
is actually sent and the CAST performs the real conversion server-side.
Verified against a live PostgreSQL 16 container (docker), including the
exact UPDATE from the report, with the fix landing in
src-tauri/src/drivers/postgres/{binding,client,mod}.rs and a new
ignored live-DB regression test.

Fixes #401.

Also: deleting a table from the sidebar context menu now refreshes the
schema/database node that actually lists it (PostgreSQL schema-based and
flat multi-database trees), instead of only the flat single-database
table list — the dropped table no longer lingers until a manual refresh.
- ctxDatabase from the context-menu payload is string | null | undefined;
  coerce null -> undefined so it satisfies the string | undefined sinks (tsc -b).
- Wrap schemaDataMap (databaseData?.schemaDataMap ?? {}) in useMemo so the
  lazy-load effect's dependency is stable (react-hooks/exhaustive-deps).
… multi-db

Opening New Console from a database node on a schema-based multi-database
connection (PostgreSQL) produced a tab with schema=<databaseName> and no
database. execute_query then ran on the connection's PRIMARY pool and issued
SET search_path TO "<databaseName>" — but that name is a database, not a
schema — so unqualified relations failed with relation-not-found.

newConsoleForDatabase now takes isSchemaBased: for schema-based drivers it
routes via database (no bogus schema); flat drivers (MySQL) keep overloading
schema as the database name. The sidebar threads spec.database into runQuery.
…ti-db

On a schema-based multi-database connection (PostgreSQL) the editor treated a
console tab like the flat MySQL layout: the active-database dropdown, new
console/notebook creation, and Convert to Console all set tab.schema to the
database name and never set tab.database. execute_query then ran on the
connection primary pool and issued SET search_path TO "<databaseName>" (a
database is not a schema), so console queries failed with relation-not-found
(e.g. SELECT * FROM "public"."app_meta" against the wrong database).

Schema-based connections now route a non-table tab via tab.database (the pool
key) and keep tab.schema as the real schema; flat drivers keep overloading
schema with the database name. Covers: the toolbar db dropdown (label, active
highlight, selection), new console/notebook defaults, Convert to Console
(inherits both schema and database from the source tab), the tab label and the
window title.
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

2 participants