Skip to content

Unblock the SQL Editor e2e suites, stop dropping inserted SQL, and add a reverse FK drill - #393

Closed
huyplb wants to merge 10 commits into
mainfrom
fix/sql-editor-sidebar-e2e-and-referenced-by
Closed

Unblock the SQL Editor e2e suites, stop dropping inserted SQL, and add a reverse FK drill#393
huyplb wants to merge 10 commits into
mainfrom
fix/sql-editor-sidebar-e2e-and-referenced-by

Conversation

@huyplb

@huyplb huyplb commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Why this is a new branch

PR #391 was squash-merged into main as 85cd9aed91, but it only carried 17 files — the branch was merged while it sat at 40d097226b, and further commits were pushed to it afterwards. Those later commits never reached main even though the PR reads as merged. This branch is cut fresh from origin/main and carries the 10 unmerged commits, so nothing is stranded on a merged branch.

Verified by content, not by patch-id: App.tsx, ConnectionChips.tsx and surfaces.guard.test.ts are already in main; the destinations dropdown, the Oracle privilege remedy, and everything after are not.

What is here

The e2e suites had been failing in setup since v3. The v3 sidebar ships every section collapsed, and a collapsed section renders no content at all — so sql-conn-check-* and sql-schema-explorer were absent, not merely scrolled out of view. Because six suites died in beforeAll, every test inside was reported as skipped rather than failed, so nothing downstream had been exercised in a long time. The sections are an exclusive accordion, so ticking a destination closes Schema; tests that then read the tree reopen it, the way a reader would.

A silent data-loss bug this uncovered. insertAtCursor returned void and no-oped when no editor pane was mounted. Clone Table and the table blueprint both live in the Utilities workspace, where the pane is unmounted: pressing Insert SQL set the status to "Clone SQL inserted into the editor", switched views, and showed an empty tab. The app reported success while discarding the SQL. It now falls back to the active tab's buffer and returns whether anything took the text; the store supplies the writer, keeping the bridge→store dependency one-way.

Two layout bugs, both introduced by the dropdown in this branch. The trigger sat in a block wrapper, so max-w left it sized by its own content — it overflowed 93px onto the Run button and swallowed its clicks (measured: trigger 398→522, Run 438→524; elementFromPoint at Run's centre returned the dropdown's label). w-full binds it to the wrapper. Widening the block then crushed the layout segmented control from 94px to 39px, clipping its second button behind overflow-hidden — visible and hit-testable, but not clickable; a segmented control has no slack to give, so it is shrink-0.

New feature — Data Peek "Referenced by". The reverse of the existing FK drill: from a customer, the orders that point at it — the question asked before deleting or editing a row. It reuses what is here rather than adding a parallel path (inboundForeignKeysFor mirrors foreignKeyLinksFor; buildInboundDrilldown mirrors buildForeignKeyDrilldown). A child whose parent columns are not all in the grid gets no button, because a WHERE built from a partial key matches the wrong rows.

Verification

  • tsc --noEmit -p apps/web clean
  • 3,698 unit tests pass (276 files)
  • SQL Editor e2e: 92 passed, 12 of 13 files green (was: 6 suites failing in setup, their tests reported as skipped)
  • The two sides of an FK drill are easy to swap and a swapped query still parses, so the FROM and the WHERE are pinned by tests and checked against a deliberately reversed implementation. The insert fallback and the missing-column guard were likewise each verified against a deliberate break.

Known, and deliberately not papered over

  • Apply clone still fails (sql-editor-utilities). A pre-existing ~30s client-side stall in schema readiness eats the test's 120s budget. Measured: selectUtilityConnection 46ms, click 33ms, waitReady 30,086ms, select 61ms — and the server log shows every request finishing in single-digit ms with no clone-apply request ever sent. This branch touches neither ensureSchema, CloneTableModal, nor schemaApi; it has simply been invisible while the suite could not get past its own setup. I did not raise the timeout to make it green — that would hide a 30-second wait a real user also sits through.
  • Toolbar overflows below ~1200px — pre-existing, 11 controls in one row. Clipping only, no overlap, and Run stays clickable.

🤖 Generated with Claude Code


Note

Medium Risk
Touches SQL editor state (insert fallback), Data Peek query building, and DBA probe error handling; e2e-only risk is low, but inbound FK and privilege messaging affect user-facing data navigation and admin workflows.

Overview
Unblocks SQL Editor and dialect e2e by aligning page objects with v3 sidebar behavior (collapsed accordion sections, Sync workspace before Compare/Snapshot) and fixes silent SQL loss when Insert runs from Utilities while the Monaco pane is unmounted.

SQL Editor UX: Destination “chips” become a filterable dropdown with a selection summary; toolbar connection chips use bounded width (flex-initial, max-w-[17rem]) so Run and layout controls stay clickable. Insert SQL uses insertAtCursor → boolean plus a store fallback that appends to the active tab when no pane handler is mounted.

Data Peek: Adds “Referenced by” inbound FK drill (inboundForeignKeysFor, buildInboundDrilldown, drillDataPeekInbound) and fixes schema-qualified tableName lookup so FK hints work on chained peeks. DB Access shows Grant (not Edit) on rows with no privileges yet.

Server / Oracle: DBA utility failures map to actionable grant text via dbaPrivilegeRemedy (403 when applicable). Oracle demo V_$ view grants run under SYSDBA in a separate init script so they actually apply.

Reviewed by Cursor Bugbot for commit 4c8852e. Bugbot is set up for automated code reviews on this repo. Configure here.

huyplb and others added 10 commits September 9, 2026 23:31
The SQL editor laid every saved connection out as chips in a horizontal
scroller. That is fine with four. This developer's app has 254, so almost all
of them sat off-screen behind a scrollbar and there was no way to see what was
selected without dragging through the row.

It is a dropdown now. The trigger says what is selected — the name when it is
one, a count when it is several — so the common question is answered without
opening anything. Inside: a filter input and a checkbox per connection.

The filter matches name, dialect, host, database and schema, not just the
visible name. The row shows name and dialect while the tooltip carries host and
database, and people are usually told to connect to a host or a database rather
than to whatever the connection was nicknamed.

Filtering is a view over the list, not an edit of it: a selected connection
that scrolls out of the filter stays selected, and there is a test saying so —
that is the mistake this shape invites.

Not built on the shared Autocomplete: it is a single-value combobox
(`value: string`, `onChange(value)`) and this is multi-select. Its theming and
filter idiom are the reusable parts and the rest would have been fought.

Eight tests, A/B'd against two deliberate breaks — filtering on name alone, and
rendering the list while closed. Both fail the tests written for them.

Verified in the running app against the real 254 connections: typing "oracle"
narrows to 11, the checkbox toggles the store, and the trigger updates to the
chosen name.

Suite 3665 passed, eslint clean, typecheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Server Insights on Oracle answered:

    ORA-00942: table or view "SYS"."V_$PARAMETER" does not exist
      — Oracle: sessions / processes from v$parameter + v$session.

The error names a view the reader has never heard of, says it does not exist,
and the hint repeats what we tried. All three are unhelpful, and the first is
actively wrong: the view exists, and Oracle reports one you may not read as one
that is absent. Two separate things were wrong here.

**The message.** dbaPrivilegeRemedy reads the engine's error and answers with
the grant that fixes it. Which codes mean "ask for a privilege" is per-engine
knowledge — ORA-00942 on a V_$ view, 42501 on Postgres, 1227 on MySQL,
VIEW SERVER STATE on SQL Server, SQL0551N on Db2 — so it sits in @foxschema/sql
next to the probes rather than in the service that catches the exception. The
response also becomes 403 rather than 500: this is a refusal, not a fault.

It names the three views the panel reads, not SELECT_CATALOG_ROLE. Both work; a
DBA asked for the minimum can say yes faster. ORA-00942 on an ordinary table
still returns nothing, because that one really is missing and offering a grant
would send the reader after a privilege they already hold.

**The grants had never applied.** docker/init/oracle/02_seed.sql has granted
these since it was written, with a comment describing this exact confusion —
but both the init hook and the reseed run that file as `system`, and granting on
a SYS.V_$ view as `system` is ORA-01031. sqlplus continues past the error, so
the file looked correct and no demo user ever had the grants.

They move to 03_vviews_sysdba.sql, run as SYSDBA by both paths, inside the PDB
where the users live.

Verified against Oracle 23, whole cycle: revoke the grants and the pool probe
gives the reported ORA-00942; reseed and it returns 322 / 81.

Seven tests for the remedy, including the two cases worth getting wrong — a
genuinely missing table, and a non-permission failure — where it stays quiet.

Suite 3672 passed, typecheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The privileges grid lists every object with its DML and DDL columns, and a row
holding no privileges shows — in both. The action that opens the grant editor
was labelled "Edit" on every row regardless, so on the screen a new user
actually sees — every row empty — the only way to set a permission was behind a
verb describing something that is not there. Reported as "I didn't see any
setting permission", which is exactly right: it was not visible.

The button now reads Grant with a plus when the row holds nothing, and Edit
with a pencil when it holds something. Same editor either way; only the label
and icon change, so nothing moves and no testid does either.

Revoke already got this right — it disables itself when there is nothing to
revoke. The complement was missing.

The help line above the sections said "Edit opens Grant / Revoke", which
described the mechanism rather than telling anyone what to do. It now names
Grant as the action and says what a dash in a row means.

Five tests, A/B'd by putting the unconditional "Edit" back: the row-label test
fails, the rest hold. One of them pins the emitter sentence, which is the part
of that paragraph worth not losing while rewording around it.

Suite 3677 passed, typecheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…its height back

**Data Peek lost its foreign keys one level down.** Drilling through a key
opens the next panel with a qualified name — `demo_a.customers` — while the
schema cache holds tables bare, as `customers`. The resolver gave up on any
name containing a dot, so the second peek in a chain found no table, produced
no links, and every foreign key in it rendered as plain text.

The `return undefined` was guarding something real: matching a bare cache entry
for `inventory.products` while connected to `public` would hand row-edit the
wrong table's primary key. So the qualifier is now compared rather than
dropped, and only when the connection actually records a schema — on engines
that do not, demanding a match would reject every qualified name and lose the
keys for everyone, which is worse than a risk that needs two schemas holding
the same table name to arise at all.

The first test I wrote for this passed against the broken code. It asserted
that the FK cell was inside a button, which it is not either way. The hint text
below the grid renders only when `linkColumns` is non-empty, so it reads the
thing that actually changed; with the old `return undefined` restored, it
fails.

**The toolbar took 132px — 18% of a 720px screen.** Two causes, one of them
mine. The connection chips carried `flex-1`, so each grew to 319px soaking up
free space and together pushed the rest of the row onto a second line; they now
size between a floor and a ceiling. The row's own `min-h-11` and `py-1.5` were
also more air than the controls need.

Worth saying plainly: the dominant factor is not the toolbar. This profile has
the appearance scale on `lg`, so `html` is 18px and every rem in the app is
12.5% larger than the design assumes. That is a preference, not a defect —
switching it to `md` takes the same toolbar from 123px to 112px.

Suite 3673 passed. Two failures under full parallel load — the code-cell worker
sandbox and a CLI TUI screen — both pass in isolation and neither is in code
this touches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last dialect e2e failure, on every engine at once. The test opens the
Snapshots pane after migrating and then returns to Compare — and Snapshots is
its own workspace since v3, so the toolbar's pane switcher, gated on
`activeView === 'sync'`, is not on that screen. Clicking the Compare pill
waited 15s for a control that had gone.

Two call sites: LokeeHistoryPage.openComparePane, and a raw clickWhen in
shared-flow that bypassed it. Both now select Sync first; the raw one goes
through the page object, which is where that knowledge belongs.

Worth recording how long this took to see. The error named
`sync-pane-history-btn`, so I spent several rounds proving that button was
fine — present, 54x47, visible, enabled, unobscured, and stable across eight
sampled frames. It was. The call log named a different locator further down,
`sync-pane-compare-btn`, and I had been reading a truncated error. The button I
was measuring was never the one failing.

Full matrix, all passing:

    dialects/postgres                          11
    dialects/mysql, mariadb, mssql             33
    dialects/oracle, db2, cockroachdb, sqlite  39 (5 skipped)
    database-access-dialects, 7 engines        49

132 e2e tests, 0 failures — the first clean run since #385 landed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
schema-history was 6/6 before the v3 UI and 1/6 after — five failures, all
cascading from one.

`snapshotTarget()` clicked Snapshot target and then waited up to 30s for that
button to come back enabled. It never does: capture bumps the Lokee epoch, and
by the time things settle the toolbar's compare controls are gone from the DOM
— the button and the target picker both. The run reported a timeout for a
snapshot that had already succeeded, with the toast still on screen saying
"Snapshot v1 · 10 object change(s) · 10 objects".

So it waits for the outcome instead. The toast is the thing that means the
snapshot happened; the button coming back is incidental, and either ending is
accepted.

The last failure was the mirror image: a second snapshot is taken after looking
at the first, from the Snapshots workspace, where the button is not on screen
at all. `snapshotTarget()` now returns to the compare pane first, the same way
`openComparePane()` does.

    schema-history        1/6 -> 6/6
    dialects re-checked   22/22 (the page object is shared)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e Run button

The v3 sidebar ships every section collapsed, and a collapsed section renders
no content at all — so `sql-conn-check-*` and `sql-schema-explorer` were absent
rather than merely scrolled away. Six suites had been failing on that since the
v3 chrome landed. The sections are an exclusive accordion, so ticking a
destination closes Schema; the tests that then read the tree reopen it, the way
a reader would. `ensureSidebarSectionOpen` no longer throws when the screen has
no sidebar (the Utilities workspace has none) — nothing to expand is not a
failure.

Two layout bugs surfaced with them, both mine. The destinations dropdown sat in
a block wrapper, so `max-w` left it sized by its own content: it overflowed 93px
onto the Run button and swallowed its clicks. `w-full` binds it to the wrapper.
Widening the block then crushed the layout segmented control from 94px to 39px,
clipping its second button behind `overflow-hidden` — visible and hit-testable,
but not clickable. A segmented control has no slack to give, so it is `shrink-0`.

sql-editor-sqlite: 5 failing to 0.
Data Peek could already follow a foreign key to its parent. This is the other
direction — from a customer, the orders that point at it — which is the question
asked before deleting or editing a row.

Reuses what is here rather than adding a parallel path: `inboundForeignKeysFor`
is the mirror of `foreignKeyLinksFor`, and `buildInboundDrilldown` mirrors
`buildForeignKeyDrilldown`. The blueprint lib already finds inbound tables by
name, which is enough to warn before a rename but not to build a query — a drill
needs the FK's columns.

The two sides are easy to swap and a swapped query still parses, so the FROM and
the WHERE are pinned by tests and both were checked against a deliberately
reversed implementation. A child whose parent columns are not all in the grid
gets no button: a WHERE built from a partial key matches the wrong rows.
SqlEditorPane wires the insert handler on mount and clears it on unmount, and
`insertAtCursor` returned void, so a call with no pane mounted went nowhere and
no caller could tell. Clone Table and the table blueprint both live in the
Utilities workspace, where the pane is unmounted: pressing Insert SQL set the
status to "Clone SQL inserted into the editor", switched to the editor, and
showed an empty tab. The text was gone and the app had said it arrived.

The bridge now falls back to the active tab's buffer and returns whether
anything took the text. The store supplies the writer — the bridge must not
import the store, the dependency runs the other way.

Found by the e2e suite once its own setup stopped failing first.
openTableBlueprint reached for the schema explorer wherever it was called from.
Run on its own it passed, because setup left the app on the SQL Editor; run
after the Clone Table tests it sat on the Utilities workspace, which has no
sidebar at all, and spent 25s timing out on a tree that was never going to
appear. Switching views first takes it to 1.1s.
@cursor

cursor Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_4021f6c1-a60e-4b20-aab1-f2afc63e3c35)

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.

1 participant