daff: The Diff Tool That Actually Understands Tables

2026-09-10

You export a CSV, someone edits it in a spreadsheet, and now you're staring at a diff that thinks every row moved because they saved with different quoting rules. Or the primary keys got sorted differently. Or a column was inserted at position 3 and every subsequent field looks "changed."

Text diff is the wrong tool for tabular data. daff — by Paul Fitzpatrick, in Debian as daff, on npm as daff, in Homebrew as daff — is the right one. It treats rows, columns, and primary keys as first-class citizens.

The basic invocation

$ daff old.csv new.csv
@@,name,role,salary
+++,Rex,SRE,180000
->,Alex,Engineer -> Senior,120000 -> 145000
---,Sam,Manager,200000

Each leading marker tells you what happened: +++ row added, --- row removed, -> cell edited (with old→new inline). Column reordering is silently normalized. Row reordering is handled too — when a primary key can be inferred, daff finds the "same" row across files by content, not position.

Where daff earns its keep

Wiring it into git

The real superpower is teaching git to use daff for .csv diffs. In .gitattributes:

*.csv diff=daff-csv
*.csv merge=daff-csv

In .git/config:

[diff "daff-csv"]
    command = daff diff --git
[merge "daff-csv"]
    name = daff tabular merge
    driver = daff merge --inplace %A %O %B %A

Now git diff and git merge on CSVs Just Work. The daff project ships daff git csv which configures both for you in one command.

It's not just CSVs

daff reads TSV, SQLite tables, JSON arrays-of-objects, and Excel via daff-xlsx. Same diff semantics everywhere. Compare a SQLite dump against yesterday's:

$ daff yesterday.sqlite today.sqlite --table users

You get a proper row-level report instead of a binary "files differ."

Primary keys matter

By default daff guesses the primary key. When it guesses wrong, tell it:

$ daff --id user_id old.csv new.csv

This is the flag that transforms daff from "neat" to "essential" — once it knows the identity column, it stops caring about row order and starts tracking each row's lineage across the two files. Rows that were reordered, moved by a sort, or interleaved by an INSERT stop appearing as false churn.

Written in Haxe, compiled to Node, Python, PHP, and native binaries — so it runs anywhere your build server does.

Key Takeaway: Text diff on tabular data lies to you; daff understands rows, columns, and primary keys, and plugs directly into git so CSV commits stop being unreadable.

All newsletters