Join Preview

Predicts a join's row count before it runs, so repeated keys that duplicate rows or keys that never match don't go unnoticed.

StopsSQL join duplicated every row

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/join-preview.json

The files land in your project. They're yours to edit.

Usage

join-preview-usage.tsx
import { JoinPreview } from "@/components/ui/join-preview"

<JoinPreview
  left={{ name: "orders", rows: orders }}
  right={{ name: "customers", rows: customers }}
  leftOn="customer_id"
  rightOn="id"
  expect="m:1"
  onJoin={() => new Promise((resolve) => setTimeout(resolve, 600))}
/>

What it catches

  • Repeated keys that fan rows out (and break an expected m:1)
  • Rows dropped for having no match or a blank key
  • Keys that differ only by type: number vs text
  • Keys that differ by spaces, case or leading zeros
  • Row count for inner, left, right and outer joins side by side

Props

JoinPreview props
PropTypeDefault
left* (required)

Left table: a display name and its rows.

{ name: string; rows: JoinRow[] }—
right* (required)

Right table: a display name and its rows.

{ name: string; rows: JoinRow[] }—
leftOn* (required)

Key column(s) on the left.

string | string[]—
rightOn

Key column(s) on the right. Defaults to `leftOn`.

string | string[]—
how

Controlled join type.

JoinHow—
defaultHow

Initial join type when uncontrolled.

JoinHow"left"
onHowChange

Called when the user picks a join type.

(how: JoinHow) => void—
normalize

Controlled key normalisation.

JoinNormalize—
defaultNormalize

Initial key normalisation when uncontrolled.

JoinNormalize{}
onNormalizeChange

Called when the user toggles a key normalisation.

(normalize: JoinNormalize) => void—
expect

Cardinality you expect. Breaking it turns the fan-out warning red.

JoinCardinality—
onJoin

Runs the join. May return a Promise; the button shows a pending state.

(plan: { how: JoinHow; normalize: JoinNormalize }) => void | Promise<unknown>—
locale

Locale for numbers.

string—
messages

Every visible string, with `{placeholders}`.

Partial<JoinPreviewMessages>—

* required. Also accepts the native props of <section> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Section is labelled with both table names
  • Join type is a radiogroup with roving tabindex, arrow keys, Home and End
  • Counts are in a polite live region; failures use role=alert
  • Run button shows aria-busy while pending

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Runs on the sample rows you pass, in the browser; large tables need a server-side count
  • Normalisations are opt-in and may merge keys that really are different
  • Predicts counts; it doesn't run your SQL or know your database's collation and type coercion

What it stops

SQL join duplicated every row

Also known as

sql join · left join · inner join · merge · pandas merge · vlookup · data pipeline · etl

Field type

Dependencies

Pairs well with