Skip to content

feat(peek): table size, and the orphan-FK query behind "Peek orphans" - #389

Merged
huyplb merged 6 commits into
mainfrom
feat/peek-insight-size-orphans
Sep 9, 2026
Merged

feat(peek): table size, and the orphan-FK query behind "Peek orphans"#389
huyplb merged 6 commits into
mainfrom
feat/peek-insight-size-orphans

Conversation

@huyplb

@huyplb huyplb commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Second of the three mockup slices. Stacked on #387 (ux/professional-polish), since it uses StatCard.

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:

  • Redshift shares the Postgres factory and has no pg_total_relation_size (size lives in SVV_TABLE_INFO). 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.
  • CockroachDB accepts the call and always answers 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:

engine rows size_bytes
postgres 5000 1441792 → 1.4 MB
mysql 4838 est. 1736704 → 1.7 MB

Not verified: sqlserver/azuresql — the container will not start here (/docker-init/entrypoint.sh missing) — and yugabytedb, 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, not NOT IN — measured, not received wisdom. With a NULL in the parent's key column, on live Postgres over the same rows:

NOT EXISTS (ours)   2
NOT IN              0

NOT IN silently 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:

  • Rows with a NULL foreign key are excluded — a NULL FK is an absent relationship, not a broken one.
  • Both sides aliased, so a self-referencing table stays apart from itself.
  • The parent resolves through its own schema; a cross-schema FK that loses it resolves a bare name wherever the connection points — a different table with the same name.

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. buildOrphanPeek is 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 (sizeBytes on the table-insight probe, with dialect-specific SQL and explicit opt-outs for engines that cannot report bytes reliably) plus human-readable formatBytes. Orphan foreign keys are detected only on user action: per-FK NOT EXISTS count queries, summary cards, and Peek orphans opening a new Data Peek via openDataPeekOrphans and buildOrphanPeek.

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.

huyplb and others added 4 commits September 7, 2026 23:26
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>
@cursor

cursor Bot commented Sep 8, 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_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>
@cursor

cursor Bot commented Sep 9, 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_5ce1f801-8463-458a-9ffc-8bbbd85e1050)

Base automatically changed from ux/professional-polish to main September 9, 2026 00:16
…e-orphans

# Conflicts:
#	apps/web/src/frontend/features/sql-editor/components/PeekInsight.tsx
@cursor

cursor Bot commented Sep 9, 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_ff5b8d8a-ce23-4da5-8dcf-d8110154f8d8)

@huyplb
huyplb merged commit e872a2a into main Sep 9, 2026
12 checks passed
@huyplb
huyplb deleted the feat/peek-insight-size-orphans branch September 9, 2026 00:22
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