feat(peek): table size, and the orphan-FK query behind "Peek orphans" - #389
Merged
Conversation
Counted, not judged by eye: the frontend held 85 distinct card/panel class strings, and the small uppercase label above a group came in eight spellings across some seventy uses — text-[10px] and text-[11px], tracking-wide and tracking-wider, text-slate-400 and text-slate-500. None of that variation carried meaning. It is what screens look like when each is written alone. shared/components/surfaces.tsx now holds SectionLabel, Panel and StatCard. Named for the set and lowercase, per CONVENTIONS: a components file is PascalCase only when it exports one component. The access feature already had `labelCls` saying this once, but a feature cannot lend a primitive to another feature — architecture.test.ts forbids reaching into `features/*/lib`, and rightly. It now defers to the shared string so there is a single definition rather than a matching pair. Migrated the two places that had each grown their own stat card: Peek Insight (Rows / Null-heavy / High distinct) and the snapshot briefing. The briefing was a run of coloured numbers — "+3 ~2 −0" — which asks the reader to supply the nouns; it now names them, which is what the mockup shows. StatCard keeps the hint line optional on purpose. A row count estimated from a catalog and one arrived at by counting are different claims, and that line is where the difference gets said; rendering it empty would read as a value that failed to load. Eight tests, A/B'd against three deliberate breaks — tone moved to the label, hint rendered unconditionally, a falsy value blanked to an em dash. Each break fails the test written for it. Suite: 3625 passed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Peek Insight showed rows, nulls and distinct counts but not size, which the
mockup puts second among its four cards. Size is a catalog fact like the
others, so it rides the same probe rather than a new round trip: one extra
column, aliased size_bytes, picked up by the tolerant matching the row count
already uses.
Three factories cover nine of the fourteen dialects, so the expression is a
factory parameter rather than a constant. That is not tidiness — it is the
only reason the next two findings did not ship as wrong numbers:
**Redshift** shares the Postgres factory, and has no pg_total_relation_size;
size lives in SVV_TABLE_INFO, a different query shape. The "redshift" service
in this repo is a real Postgres, so baking the expression in would have passed
every test here and failed on an actual warehouse. It passes 'NULL'.
**CockroachDB** accepts the call and always answers NULL — a compatibility
stub, not an implementation. Measured on v26.3.0:
SELECT pg_total_relation_size('demo_a.orders'::regclass); -> NULL
so it passes 'NULL' too, rather than spending a round trip to learn nothing.
Verified live, running the probe SQL as written:
postgres 5000 rows -> size_bytes 1441792 (1.4 MB)
mysql 4838 est. -> size_bytes 1736704 (1.7 MB)
Not verified: sqlserver/azuresql, whose container will not start here
(/docker-init/entrypoint.sh missing), and yugabytedb, which is not accepting
connections. Yugabyte keeps the Postgres expression: if the function is absent
or stubbed there too the column returns null and the card reads "Not reported",
which is the same graceful answer as opting out.
Oracle, Db2, SQLite, DuckDB and ClickHouse stay null on purpose. Oracle and Db2
expose only a page count whose page size varies per tablespace, and a plausible
wrong size is worse than a dash for a figure people act on.
formatBytes keeps one decimal below 10 so 1.2 GB does not collapse to 1 GB, and
answers "—" for a negative or non-finite figure rather than claiming "0 B".
Suite: 3637 passed, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Peek Insight mockup shows "14 orphans · 99.99% matched" and a Peek orphans
button. This is the query behind both.
Kept out of the table-insight probe deliberately. That probe is catalog-only —
pg_stats, TABLE_ROWS, a page count — and answers in constant time whatever the
table's size. Counting orphans scans the child against the parent, so it is
something the reader asks for once, not something a 2.4M-row table pays for
because a tab was opened. The mockup's own footer says "Profile from pg_stats +
TABLESAMPLE 1%", and a sampled profile cannot tell anyone there are exactly 14
orphans; the two claims needed separating rather than merging.
NOT EXISTS rather than NOT IN, and this is measured, not received wisdom. With
a NULL in the parent's key column, on live Postgres against the same data:
NOT EXISTS (ours) 2
NOT IN 0
NOT IN silently reports a table full of orphans as clean. The first attempt at
demonstrating this proved nothing — the parent key was a PRIMARY KEY, so the
NULL was rejected and both forms agreed on 2. It only shows up against a
nullable key column, which is exactly the case a foreign key may reference.
Rows with a NULL foreign key are excluded: a NULL FK is an absent relationship,
not a broken one, and counting it would call every optional reference an orphan.
Both sides are aliased so a self-referencing table stays apart from itself, and
the parent is resolved through its own schema — a cross-schema FK that loses it
resolves a bare name wherever the connection happens to point, which is a
different table with the same name.
The generated SQL was run verbatim against live engines, over five rows holding
two orphans and one NULL:
postgres fox_orphans = 2
mysql fox_orphans = 2
Nine tests. buildOrphanPeek asks the same question as the count — the test
compares the two predicates directly, so the button and the number cannot drift
into disagreeing.
Suite: 3646 passed, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Bugbot couldn't run - usage limit reachedBugbot 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_8ec9f1ba-7244-4ded-9a00-61abe477f1de) |
Finishes the two remaining mockup slices. **Peek orphans.** The count query landed last commit; this puts it on screen. An Orphan FKs card, a row per foreign key showing its orphan count and matched percentage, and a Peek orphans button that opens those rows in the peek stack through openDataPeekOrphans — the same shape as openDataPeekFromFk, so the grid, paging and breadcrumbs come for free. The check is a button, not a mount effect, and the label next to it says why: "Scans the table — not a catalog read". Everything else on that tab answers in constant time from the catalog; this one reads the table, and the difference is the reader's to decide, not something to spend on their behalf because a tab opened. A foreign key that has not been checked says "not checked" rather than "0 orphans", which would be a claim nobody made. **Account stage.** It described an account and sent you elsewhere to change it. It now names the alterations this engine can express for this kind of principal — a role has no password, and most engines have no login to disable, so offering either would generate SQL the server rejects — and shows the drop safety notes, which are the only warning before an irreversible step. Both answers already existed as private functions inside UserManagement.tsx. Extracted to features/access/lib/accountAlterations.ts and imported by both screens rather than copied, along with ALTERATION_LABEL so the two cannot end up calling the same action different things. One test needed fixing rather than the code: PeekInsight's store mock supplied only sessionPasswords, so reading schemaCache threw. The mock now mirrors the store's real shape. Making the component defensive instead would have hidden that the mock had drifted from what it stands for. Nine tests for the extracted lib. Full suite 3655 passed, 0 failed, twice; the one intermittent DatabaseAccessModal failure is the pre-existing flake noted on PR #379, and passes 4/4 in isolation with nothing here touching it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Bugbot couldn't run - usage limit reachedBugbot 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_5ce1f801-8463-458a-9ffc-8bbbd85e1050) |
…e-orphans # Conflicts: # apps/web/src/frontend/features/sql-editor/components/PeekInsight.tsx
Contributor
Bugbot couldn't run - usage limit reachedBugbot 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_ff5b8d8a-ce23-4da5-8dcf-d8110154f8d8) |
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.
Second of the three mockup slices. Stacked on #387 (
ux/professional-polish), since it usesStatCard.Size
Peek Insight showed rows, nulls and distinct counts but not size — second of the four cards in the mockup. Size is a catalog fact like the others, so it rides the same probe: one extra column, aliased
size_bytes.Three factories cover nine of the fourteen dialects, so the expression is a factory parameter. That is not tidiness — it is the only reason two wrong numbers did not ship:
pg_total_relation_size(size lives inSVV_TABLE_INFO). Theredshiftservice in this repo is a real Postgres, so baking the expression in would have passed every test here and failed on an actual warehouse.NULL— a compatibility stub. Measured on v26.3.0:SELECT pg_total_relation_size('demo_a.orders'::regclass)→NULL.Both pass
'NULL'instead.Verified live, running the probe SQL as written:
Not verified:
sqlserver/azuresql— the container will not start here (/docker-init/entrypoint.shmissing) — andyugabytedb, not accepting connections. Yugabyte keeps the Postgres expression: if the function is stubbed there too, the column returns null and the card reads "Not reported", the same graceful answer as opting out.Oracle, Db2, SQLite, DuckDB and ClickHouse stay null on purpose. Oracle and Db2 expose only a page count whose page size varies per tablespace, and a plausible wrong size is worse than a dash for a figure people act on.
Orphan foreign keys
NOT EXISTS, notNOT IN— measured, not received wisdom. With a NULL in the parent's key column, on live Postgres over the same rows:NOT INsilently reports a table full of orphans as clean.Worth recording: my first attempt at demonstrating this proved nothing — the parent key was a
PRIMARY KEY, so the NULL was rejected and both forms agreed on 2. It only appears against a nullable key column, which is exactly what a foreign key may reference.Other decisions:
Generated SQL run verbatim on live engines over five rows holding two orphans and one NULL: postgres 2, mysql 2.
Why this is not in the insight probe
The probe is catalog-only and answers in constant time whatever the table's size. Counting orphans is a scan. So it is offered as something the reader asks for once, not something a 2.4M-row table pays for because a tab was opened — the mockup's own footer says "Profile from pg_stats + TABLESAMPLE 1%", and a sampled profile cannot report exactly 14 orphans. The two claims needed separating.
Tests
Nine for the orphan queries, five more for
formatBytes, six for the size probes.buildOrphanPeekis asserted to ask the same question as the count — the test compares the predicates directly, so the button and the number cannot drift apart.Suite: 3646 passed, 0 failed. Typecheck clean.
Still to come
Slice 3 — Account stage inline alterations. The UI affordance for "Peek orphans" is not wired yet; this PR lands the query and the size card.
🤖 Generated with Claude Code
Note
Medium Risk
Adds on-demand table scans and changes catalog insight SQL across multiple dialects; orphan detection must stay semantically correct (NOT EXISTS, NULL FKs) to avoid misleading integrity signals.
Overview
Data Peek Insight gains a catalog Size stat (
sizeByteson the table-insight probe, with dialect-specific SQL and explicit opt-outs for engines that cannot report bytes reliably) plus human-readableformatBytes. Orphan foreign keys are detected only on user action: per-FKNOT EXISTScount queries, summary cards, and Peek orphans opening a new Data Peek viaopenDataPeekOrphansandbuildOrphanPeek.Access → Account now surfaces engine-supported alteration chips and drop safety warnings by reusing logic moved into
accountAlterations(shared with User Management).Tests cover orphan SQL shape, size normalization,
formatBytes, and alteration/drop-note rules.Reviewed by Cursor Bugbot for commit 4fd1280. Bugbot is set up for automated code reviews on this repo. Configure here.