feat(ui): multi-schema awareness across Editor, Brain, and Advisor - #55
Conversation
Add shared schemaNames helpers and wire the SQL editor / tables overview so object browser, autocomplete, generated SQL, and index lookup APIs use schema.table for non-default schemas. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
Persist/display tableReference in Schema Docs notes, show schema in knowledge @ suggestions, and use qualified ids in ERD / classification / key-column grouping so crm.orders and sales.orders do not collide. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
Advisor scans non-public schemas, emits schema-qualified CREATE INDEX SQL, and Performance/privileges/dashboard skill copy follow suit. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
Keep expand/collapse on the chevron only so a normal click inserts schema-qualified SELECT … FROM schema.table. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
geekypunk
left a comment
There was a problem hiding this comment.
Reviewed backend + the new shared util.
Backend. PostgresIntrospectionProvider.getTableIndexes now joins pg_namespace and filters with (?::text IS NULL OR n.nspname = ?) — fully parameterized, and every column is table-qualified per the repo's SQL rule. The null-schema branch keeps bare names working, so this is backward compatible. {tableName:.+} on the two /tables/{id}/… mappings is the right fix for schema-qualified ids, and PathPatternParser keeps it to a single path segment.
Shared util. schemaNames.js is pure, documented, and ships with tests. objectKey including the schema is what stops crm.orders and sales.orders colliding as React keys — the actual bug class this PR exists to close. qualifyForSql correctly leaves public/dbo bare.
One non-blocking note for a follow-up: with a bare table name the Postgres schema filter is disabled (schemaName stays null), so indexes can still merge across same-named tables in different schemas. That only matters for callers that haven't been migrated to encodeTablePathId; the UI paths in this PR all pass qualified ids. Worth tightening to the session search_path later rather than blocking here.
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Prepares and documents the **v1.1.0** product cut from `main` since `v1.0.0`, and locks in a **weekly release cadence: Saturday 09:00 America/Los_Angeles**. ### Since v1.0.0 - Progressive dashboards (#57), dashboard improvements (#51) - Multi-schema UI (#55), Performance hub (#52) - CI CodeQL unblock (#56), cloud env caveats (#53) ### This PR - Bump `backend/pom.xml` → `1.1.0` - `CHANGELOG.md` + `docs/releases/RELEASE_NOTES-v1.1.0.md` - Cadence in `docs/oss-ux/RELEASE.md` - New `docs/oss-ux/WEEKLY_RELEASE_AUTOMATION.md` (cron + paste-ready prompt) - Daily triage doc clarified as optional (not the release cut) ### After merge 1. Tag `v1.1.0` on the merge commit and push → `.github/workflows/release.yml` publishes the GitHub Release. 2. Create the Cursor Automation once from `WEEKLY_RELEASE_AUTOMATION.md` (cannot be created via API). ### Pre-flight `scripts/self-host/e2e-agent-check.py` → `AGENT_OK True`, `DASH_OK True` on the current stack before this bump. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-8ce91e70-c67b-48c6-84b3-05bb9d06231a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-8ce91e70-c67b-48c6-84b3-05bb9d06231a&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
`getTablesAndViews()` passes the schema each table was found in, but
`getTableColumns` bound `DEFAULT_SCHEMA` (`'public'`) and discarded the
argument. Every table on a database whose objects live outside `public`
came back with **zero columns**.
Measured against a real dbt warehouse (195 tables across 17 schemas,
nothing in `public`):
```
marts.dim_person, schema bound to 'public' -> 0 columns
'marts' -> 91 columns
```
The PK subquery also had no schema predicate at all. Postgres auto-names
primary keys `<table>_pkey`, so two schemas holding a same-named table
cross-match by construction — producing duplicated `ColumnInfo` rows and
false-positive PK flags. This adds `tc.table_schema = ku.table_schema`
and `ku.table_schema = ?`.
Both problems were latent while the provider only ever read `public`.
#55 made them live: now that introspection walks every non-system
schema, a `staging`/`marts`/`public` collision on `orders` or
`customers` is the normal shape of a dbt warehouse, not the exception.
## Verification
Deployed and measured on a live 195-table warehouse:
| | before | after |
|---|---:|---:|
| schema snapshot | 37 tables / 837 cols | **195 / 4,070** |
| tables returning zero columns | 195 of 195 | **0 of 195** |
| tables with correctly-detected PKs | — | 67 |
| tables with duplicated column rows | — | 0 |
| tables with every column flagged PK | — | 0 |
A full brain re-init on that connection went from a 37-table snapshot to
195 tables / 4,070 columns, lifting `table_classification` 25 → 161 and
`inferred_table_relationship` 17 → 280. The agent went from being blind
to 14 of 18 schemas to correctly describing them.
Unit tests: `PostgresIntrospectionProviderTest` passes. A mocked
`ResultSet` can't exercise SQL semantics, so the correctness evidence is
the measurement above, against Postgres 18.
## Relationship to #40
#40 is open and `CONFLICTING`. It fixed the same class of problem with a
`current_schema()` approach that #55 superseded by scanning all
non-system schemas. This PR sits on top of #55 instead and is
independent of #40 — #40 can likely be closed once this lands.
Co-authored-by: deepsql-deploy <venkatesh.sakamuri@stayflexi.com>
…s across schemas The previous commit matched a protected table against a query's tables by adding both the qualified name and its bare part to each side, then intersecting. That collapsed public.customer_profiles and marts.customer_profiles to the same key, so protecting one refused queries against the other -- with a message naming a table the user never referenced. This product added multi-schema support in #55 and an acme_erp fixture with crm/sales/finance/hr/inventory in #65, so same-named tables across schemas are the expected shape here, not a corner case. Over-blocking is the safe direction, which is exactly why it would have survived review and surfaced later as unexplained refusals. Matching is now asymmetric, because the two sides carry different information. ConnectionChatAccessPolicyService.qualifyTable() drops the schema when it is "public", so a bare PROTECTED name means public.<table> -- it is not unknown. A bare REFERENCE in a query is genuinely unknown: it resolves through the session search_path and could be any schema. reference unqualified -> match on bare name (ambiguous, so block) protected public -> a qualified reference must actually say public both qualified -> exact match Every bypass stays closed: an unqualified reference to a protected table is still refused, and hr.salaries still matches a bare "salaries". Also replaces a characterization test asserting the opposite. It was written before qualifyTable's public-collapsing was discovered and encoded the wrong belief that a bare protected name is ambiguous; the case genuinely worth pinning is a bare reference, which it now covers. Verified: 17/17 in UserDataAccessPolicyServiceTest. Regression baseline on the same suite selection unchanged at 8 failures / 16 errors (414 run vs 412 before, the delta being these two tests passing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
End-to-end multi-schema UI pass so connections like Multi Schema Shop (
crm/sales) no longer look like a single bare-name catalog.P0 — Authoring
src/lib/schemaNames.jshelpers (canonicalTableReference,objectKey,qualifyForSql, …)FROM crm.orders), collision-safe React keys, schema-aware searchSELECT … FROM schema.table(chevron expands columns)schema.table; Postgres/MySQL introspection scopes indexes by schemaP1 — Brain / Knowledge / ERD
tableReference(no cross-schema note collisions)@/@@suggestions show qualified labels; bare-name lookup only when uniqueschema.tableP2 — Advisor / ops copy
publicschemas and emits schema-qualifiedCREATE INDEX … ONGRANTpatternVerification
On Multi Schema Shop:
crm.customers,sales.order_items,sales.ordersFROM crm.customersEditor with schema-qualified tables
Generated SQL FROM crm.customers
Brain Schema Docs with crm/sales tables
Test plan
crm.*/sales.*FROM schema.tableSQLschema.tablenode --test src/lib/schemaNames.test.jsNote: PR #53 is docs-only Cloud caveats; this PR is the multi-schema UI work.
To show artifacts inline, enable in settings.