Skip to content

Repository files navigation

table-diff

npm version CI license: MIT

Diff two tables by key, from the command line. Point it at two CSV / TSV / JSON / JSONL files, name the key column, and get exactly what changed — rows added, rows removed, and per-field changes — immune to row order and to volatile columns like updated_at. Colored output for your terminal, Markdown for PR comments, JSON for machines, and a meaningful exit code for CI.

Built on rowset-diff; the only runtime dependency.

npm i -g table-diff        # or: npx table-diff …

Why

Line-based diff/git diff is the wrong tool for tabular data: re-export the same table with a different ORDER BY and every line "changed". Generic JSON diff tools compare arrays by position and have the same problem. table-diff matches rows by a key you name, so a reorder is silence and a real change is a one-line finding.

$ table-diff examples/orders.before.csv examples/orders.after.csv --key order_id --ignore updated_at
examples/orders.before.csv → examples/orders.after.csv   key: order_id

- order_id=A-1003   customer=Chris, total=50.00, status=refunded, updated_at=2026-07-02T08:00:00Z
+ order_id=A-1005   customer=Eve, total=75.25, status=pending, updated_at=2026-07-03T11:00:00Z
~ order_id=A-1004
    total: 320.75 → 299.99

+1 added  -1 removed  ~1 changed  2 unchanged

The after-file was reordered and every updated_at changed — none of that appears. Only the dropped order, the new order, and the one real price change.

Usage

table-diff <before> <after> --key <columns>

<before> and <after> are file paths, or - for stdin (one side only). The format is inferred from the extension (.csv, .tsv/.tab, .json, .jsonl/.ndjson); override with --input-format.

Option Default
-k, --key <cols> key column(s); comma-separate for a composite key required
--ignore <cols> columns to exclude from comparison (e.g. updated_at)
--fields <cols> compare only these columns
-f, --format <fmt> pretty | markdown | json | summary pretty
--input-format <fmt> csv | tsv | json | jsonl by extension
--duplicates <mode> throw | keep-first | keep-last when a key repeats throw
--limit <n> rows shown per section; 0 = unlimited 20
-q, --quiet print nothing; the exit code is the answer
--color / --no-color force colors on/off (NO_COLOR respected) auto

Exit codes (GNU diff convention, so it drops straight into CI): 0 tables match · 1 differences found · 2 error.

Recipes

Guard data files in CI — fail the build when a committed dataset drifts:

table-diff expected.csv actual.csv --key id --quiet || exit 1

PR comment / job summary — Markdown tables, made for GitHub:

table-diff old.csv new.csv --key id -f markdown >> "$GITHUB_STEP_SUMMARY"
**+1 added · −1 removed · ~1 changed · 2 unchanged** — key: `order_id`

#### ~ Changed (1)

| order_id | column | before | after |
| --- | --- | --- | --- |
| A-1004 | total | 320.75 | 299.99 |

Compare a live query against a snapshot — pipe stdin with -:

psql "$DB" -c 'select * from users order by 1' --csv | table-diff - snapshot.csv --key user_id

Reconcile two systems on a composite key, ignoring churn:

table-diff erp.csv warehouse.csv --key store_id,sku --ignore synced_at,etag

Feed the result to a script:

table-diff old.csv new.csv --key id -f json | jq '.changed[].fields'

Library API

Everything the CLI does is exported as a library (ESM + CJS, no Node-specific imports — works in any JS runtime):

import { parseTable, tableDiff, formatMarkdown } from 'table-diff';

const before = parseTable(oldCsvText, 'csv');   // { columns, rows }
const after = parseTable(newCsvText, 'csv');

const result = tableDiff(before, after, { key: 'id', ignore: ['updated_at'] });
// { added, removed, changed, same, columns, columnsAdded, columnsRemoved, summary }

if (!result.same) console.log(formatMarkdown(result, { beforeLabel: 'old', afterLabel: 'new' }));
  • parseCsv(text, { delimiter? }) — strict RFC 4180 parser: quoted fields, embedded delimiters/newlines, "" escapes, CRLF, BOM. Duplicate headers and ragged rows are errors, not guesses.
  • parseTable(text, format) / parseJsonTable(text) / parseJsonl(text) / inferFormat(path)
  • tableDiff(before, after, options) — header-aware wrapper around rowsetDiff: validates the key against both headers, tracks column-level adds/removes, and counts a summary. Accepts key, ignore, fields, duplicates, and a custom equals (e.g. numeric tolerance).
  • formatPretty(result, opts) / formatMarkdown(result, opts) / formatJson(result, opts) / formatSummary(result)

Notes

  • CSV values are strings. table-diff old.csv new.json works, but 1 (JSON number) ≠ "1" (CSV string) — for cross-format diffs keep values as strings, or use the library with a custom equals.
  • (empty) vs (missing) in the output distinguish an empty string from a column that does not exist on that side. In --format json, missing values are null.
  • Duplicate keys throw by default — a duplicate almost always means the key you chose is not actually unique. --duplicates keep-first|keep-last opts into collapsing.
  • Rows only in one file are reported whole; --ignore affects which field changes count, not which rows exist.

Roadmap

  • .xlsx input (optional dependency)
  • A GitHub Action wrapping --format markdown into a PR comment
  • Streaming for very large files

Releasing

Publishing is automated. A pushed version tag triggers the Publish workflow, which runs the tests and publishes to npm (with provenance):

npm version patch      # bumps package.json + creates the vX.Y.Z tag
git push --follow-tags # pushes the commit and the tag → CI publishes

Publishing uses npm Trusted Publishing (OIDC) — no npm token is stored anywhere. Set up once via the package's Trusted Publisher settings on npmjs.com (org hojoongdev, repo table-diff, workflow publish.yml). Every push and PR is tested across Node 18 / 20 / 22 by the CI workflow.

License

MIT © Hojoong Kim

About

Diff two tables (CSV/TSV/JSON/JSONL) by key from the command line — added, removed, field-level changes. Markdown for PR comments, JSON, CI exit codes. Powered by rowset-diff.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages