Skip to content

feat(sidebar): find tables in every schema from Open Quickly and the sidebar filter - #3060

Merged
datlechin merged 7 commits into
mainfrom
feat/cross-schema-search
Sep 23, 2026
Merged

datlechin merged 7 commits into
mainfrom
feat/cross-schema-search

Conversation

@datlechin

@datlechin datlechin commented Sep 23, 2026

Copy link
Copy Markdown
Member

Fixes #3048

Browsing public on PostgreSQL, neither Open Quickly nor the sidebar filter could find attendance.timesheet, and nothing accepted attendance.timesheet or attendance. as a search.

Root cause

  • Open Quickly took its tables from the autocomplete provider for the browse scope. On a .bySchema engine (PostgreSQL, DuckDB) that holds the browsed schema only. On a .hierarchicalSchema engine (Oracle, Snowflake, Trino) it is a snapshot taken at connect or refresh that a tree expansion never updates. A schema. completion in the editor also widens it silently.
  • Nothing in the app owned "every table in this database, across its schemas", and PluginKit had no call for it: only fetchSchemas() and fetchTables(schema:).
  • Ranking scored the name, keyword and subtitle as flat strings, so no query could name a schema.
  • The sidebar's Tree layout judged only schemas whose lists were already loaded, and dropped the rest from a search. The List layout searched the browsed schema only.

What changed

One listing, one owner.

  • PluginDatabaseDriver.fetchTablesInAllSchemas() -> [PluginTableInfo]?, defaulting to nil. Additive. currentPluginKitVersion is already 33 on main while v0.75.0 shipped 32, so this reuses the pending number: no bump, no Info.plist edits.
  • PostgreSQL answers it with one query: the per-schema listing with its schema filter replaced by table_schema IN (<listSchemas>). Same degradation ladder, partition exclusion, comments and information_schema privilege rules, so the two listings agree by construction. PGlite inherits it; Cockroach and Redshift fall back to the host.
  • CatalogTableListing asks once, or schema by schema through one metadata scope, naming any schema whose read failed instead of reading it as empty. BackupScopeLoader uses it too; it used to open a pooled connection per schema.
  • A schema whose read fails keeps its earlier rows and is asked for again, alone, on the next read. A lost connection, or every schema failing, fails the whole read, so it cannot pass for an empty database.
  • DatabaseTreeMetadataService holds the result per database. Freshness is stale-while-revalidate (CatalogFreshness): a catalog change marks it stale and fetches nothing, because every COMMIT reports a catalog change; the next reader refetches. A fetch that started before a change delivers its rows but cannot make the listing current.

Open Quickly.

  • Tables leave the cached catalog. They merge live from SchemaService (authoritative for the schemas it holds) and the all-schema listing (for the rest), and re-merge while the panel is open. The SQLSchemaProvider dependency is gone.
  • Tables the schema service still holds from a database the connection just left are withheld, and a schema it loaded as empty keeps the listing's stale rows for it out. On a hierarchical engine the listing, keyed by database, answers for every schema once it arrives.
  • Rows outside the browsed schema name it, attendance or attendance · View, including when selected and to VoiceOver.
  • QualifiedSearchQuery reads attendance.timesheet, attendance., att.time, shop.attendance.timesheet, and "x", [x], `x` quoting. A dotted query is also scored as plain text, so a table named b.c is still found.
  • The browsed schema wins ties. A search that names no table, attendance., lists the schema alphabetically rather than shortest name first.

Sidebar filter.

  • List layout: other schemas holding a match are listed below the browsed schema's sections, as the tree's own schema rows.
  • Tree layout: an unexpanded schema is judged from the listing, and stays visible and collapsed while that is still loading.
  • schema.table and schema. work there too, with the sidebar's substring matching, in all three layouts. A table whose own name holds a dot still matches when its name is typed.
  • The sidebar and Open Quickly ask for the listing again after a database switch, a reconnect, a catalog change, or a schema list arriving, only while connected, and never twice for the same revision, so a read that failed is not retried on every redraw.

Also fixed, in the code this rewrites:

  • An Open Quickly catalog whose fetchSchemas or fetchDatabases failed was cached and served on every reopen, never retried.
  • Opening a sequence or materialized view from Open Quickly recorded it as a view, so Recent offered Drop View.
  • tableItemId joined schema and name with an unescaped dot, so a."b.c" and "a.b".c shared one id. Dots and backslashes are now escaped, and every id without either is unchanged, so no Recent history is lost.
  • While filtering, every kind section of a matching container opened, so each empty one showed a "No items" row under the match. A section now opens during a search only when it holds a match, the rule the List layout's own sections already follow. This was already true of the Tree layout on main; the List layout's other-schema rows would have inherited it.

Measured

On a local PostgreSQL 17.11 with 10,504 relations in 502 schemas, the all-schema listing takes 460 to 700 ms in one query, on a machine under load (load average above 30). The per-schema query takes 11 to 16 ms, about 6.5 s across all 502 before any network round trip.

scripts/check-postgres-table-listing-parity.sh compiles the plugin's real query builder and compares the two listings on every ladder rung. It uses a database with declarative partitions, a partition in another schema than its parent, legacy inheritance, a view, a materialized view, a foreign table, a schema the role cannot use, and quoted names with dots and mixed case:

rung 0: 11 tables, listings agree
rung 1: 9 tables, listings agree
rung 2: 9 tables, listings agree
rung 3: 11 tables, listings agree
PASS

Tests

  • verify.sh build: pass, no new warnings in changed files.
  • verify.sh plugins (AllPlugins): pass.
  • verify.sh abi <merge-base>: the one diff is fetchTablesInAllSchemas() plus its default, which is additive.
  • verify.sh docs: pass.
  • SwiftLint --strict on every changed Swift file, shellcheck --severity=warning on the script: clean.
  • New suites, 106 cases: QualifiedSearchQueryTests, SidebarSearchTests, CatalogFreshnessTests, CatalogTableListingTests, DatabaseTreeAllSchemaTablesTests, AllSchemaTablesDemandTests, DatabaseTreeFilterQualifiedSearchTests, QuickSwitcherCrossSchemaTests, PostgreSQLFetchTablesAllSchemasTests, plus new cases in DatabaseTreeCatalogRefreshPlanTests. Each suite's executed count was checked, since a suite added without regenerating the project compiles to nothing and still reports a pass.
  • Existing neighbours, including DatabaseTreeFilterTests, DatabaseTreeMetadataService*Tests, SidebarViewModel*Tests, SchemaService*Tests, CatalogChangeServiceTests, NativeDumpScopeTests, BackupScopeExpansionTests and the PostgreSQL listing suites. After the third review: 343 of 343 pass.
  • QuickSwitcherViewModelTests waits a fixed 200 ms for its filter, and under a load average above 40 it failed intermittently in a 34-suite run. It passes 57/57 alone, after the third review too, and 157/157 beside every new suite.
  • After the section and ordering fix: QuickSwitcherCrossSchemaTests, DatabaseTreeFilterQualifiedSearchTests, DatabaseTreeFilterTests, SidebarOutlineScaffoldTests and QuickSwitcherViewModelTests, 134 of 134, the four new cases among them.

No UI automation: the flow needs a PostgreSQL server with several schemas, UI tests run against the bundled SQLite sample, and plugins do not load under XCTest.

Before / After

A PostgreSQL 17 database with schemas public, attendance and payroll, each holding a timesheet, browsing public. The after shots are from this branch's Debug build. No before build was captured: on main each of these shows the public rows alone, and attendance. shows nothing.

Open Quickly, timesheet. Before: one row. After: all three, the browsed schema first and the others named.

Open Quickly searching timesheet, with rows for public, attendance and payroll

Open Quickly, attendance.. Before: no results. After: everything in attendance, alphabetically, the view named as one.

Open Quickly searching attendance., listing holidays, leave_requests, shifts, timesheet and the daily_hours view

Sidebar as List, filter timesheet. Before: public.timesheet alone. After: attendance and payroll below it, opened to their match, their empty sections closed.

Sidebar with timesheet in public at the top, and attendance and payroll below it, each opened to its timesheet table

Found while investigating, not changed here

Real, verified by reading the code path, and independent of this fix:

  • QuickSwitcherViewModel.swift history rows: an item's id is the newest execution's UUID, so picking a recent query and running it leaves its Recent entry pointing at an id that no longer exists. A few such picks empty the All scope's Recent list. Small.
  • QuickSwitcherItem.tableItemId and SharedSidebarState.commitTableOpen: frecency is keyed without the database, so public.users opened in app_prod shows in Recent and gets the boost after switching to app_staging. Small, but changes stored ids.
  • QuickSwitcherViewModel.makeCrossConnectionItems: the same table has one id in the All scope and another in the Connections scope, so Recent is split between them and each shows about half of its ten. Small.
  • SQLSchemaProvider.swift:432 (tableCompletionItems(inSchema:) then mergeTables): after attendance. completes once, FROM times offers timesheet and inserts it unqualified, which fails with relation "timesheet" does not exist. Medium.
  • SchemaService.refreshLoadedSchemaObjects (:304) and SidebarTreeView.scheduleSearchLoad: on Oracle, Snowflake and other hierarchical engines, one search keystroke loads 2 to 4 queries per schema, and every COMMIT reloads every loaded schema one after another. Medium.
  • SchemaService.perSchemaStates (:50) is keyed without the database, so on Snowflake or Trino an in-place database switch shows the old database's PUBLIC tables under the new one until each schema reloads, and for good if that reload fails. Medium.
  • TableInfo.id (QueryResult.swift:114) joins schema and name with an unescaped dot, so allLoadedTables drops one of a."b.c" and "a.b".c. Small.
  • PostgreSQLPluginDriver.swift:842 and :867 log error.localizedDescription publicly, which the public_error_text_in_log rule forbids. Plugins/ sits outside SwiftLint's included:, so nothing flags it. Small.

Follow-ups this listing makes possible: single-call overrides for MSSQL, Oracle, DuckDB and Snowflake, which use the per-schema fallback today, and MCP search_schema across every schema when no schema is given.

Review

Three Codex passes read the diff, and every finding below is fixed here with a test unless it says otherwise.

  • Code review: tables from the previous database shown during a switch, a partial listing marked current, and a table named with a dot no longer matching in the sidebar.
  • Adversarial review: rows dropped when one schema's read failed, an empty schema not counting as authoritative, the sidebar asking for the listing only when the filter text changed, the parity script dropping fixed-name objects on any server, and three-part searches failing in the hierarchical layout.
  • Branch review: on a hierarchical engine the per-schema lists can still hold the old database after a switch, so the database-keyed listing now answers for every schema once it arrives; a reconnect's revision was counted as asked for while the session was still connecting; shop.shop.orders could not reach a schema named like its database; Open Quickly did not ask again for a listing that went stale while it was open. A fifth, dotted names colliding in allLoadedTables, reaches Open Quickly only through the hierarchical per-schema lists that the listing now outranks.

Declined: the materialized-view and foreign-table arms check no per-object privilege. That is how the per-schema listing the sidebar already runs behaves, pg_class names are readable by every role, and changing it belongs with that listing.

@mintlify

mintlify Bot commented Sep 23, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 23, 2026, 6:13 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>

This branch was successfully deployed

1 active deployment
staging - docs ec4c2f4e Deployed Sep 23, 2026 by mintlify[bot]
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.

How to search table with prefix in schemas?

1 participant