Library / SDK
paulfitz/daff avatar
paulfitz/daff

daff compares tables by their structure, not by their lines

align and compare tables

930 stars76 forksJavaMIT

At a glance

What is it?
daff is a library and command line tool for comparing tables, producing a summary of their differences, and using that summary as a patch file. It is built for tables that share a common origin, and its output is a diff table in a published tabular-diff format rather than a text patch.
Who is it for?
Adopt daff when your data has columns and rows that mean something, when you need a three-way merge against a common ancestor, or when you want git to stop treating a CSV as a bag of lines. Do not adopt it for plain text diffing, for tables with no stable identity between versions, or for a workflow that needs its output consumed by a tool expecting a unified diff.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 149 days ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The alignment step is where daff differs from a line diff

The README describes the purpose in one sentence: this is a library for comparing tables, producing a summary of their differences, and using such a summary as a patch file. Then it adds the important qualifier, that it is optimized for comparing tables that share a common origin, in other words multiple versions of the same table.

That qualifier decides what the tool is for. A CSV diff, at the level git performs it, is a line diff. If a row moves down by one because a row was inserted above it, every following line changed. A table-aware diff aligns rows and columns first, then reports the actual changes, so an inserted row does not cascade into a rewrite of everything below it.

The mechanism is visible in the library API. You build a table view from an array of arrays, then compute an alignment between two tables.

js
const table1 = new daff.TableView(data1);
const table2 = new daff.TableView(data2);
js
const alignment = daff.compareTables(table1,table2).align();

The alignment object is then used to produce the diff, with a flags object controlling the output and a TableDiff doing the highlighting.

js
const flags = new daff.CompareFlags();
const highlighter = new daff.TableDiff(alignment,flags);
highlighter.hilite(table_diff);

So the pipeline is: wrap the data, align the two tables, diff the alignment into an output table. Each stage is a separate object you can substitute, which matters for a library that has to work the same way in JavaScript, Python, Ruby, PHP and R.

The diff output is a table with a published spec

The output of the diff is not a text patch. It is a table whose first column is a marker and whose remaining columns are the fields of the diffed table.

js
[ [ '!', '', '+++', '' ],
  [ '@@', 'Country', 'Code', 'Capital' ],
  [ '+', 'Ireland', 'ie', 'Dublin' ],
  [ '+', 'France', 'fr', 'Paris' ],
  [ '->', 'Spain', 'es', 'Barcelona->Madrid' ],
  [ '+++', 'Germany', 'de', 'Berlin' ] ]

Reading that against the README's example data, the header row carries the marker @@ and the column names, the + rows are inserts, the -> row is an update where the changed value is written as before->after inside the cell, and the +++ row is the added row with the same marker used in the header for a column change. The '!' row at the top is the diff summary line.

This format has a specification of its own, published at the tabular-diff specification in the frictionless data specs, and the README links the project documentation for the highlighter format as well. That matters because the output is meant to be consumed by other tools, not only read by a human.

For visual output the library renders the diff to HTML with classes on the cells so inserts, deletes and updates can be colour-coded.

js
const diff2html = new daff.DiffRender();
diff2html.render(table_diff);
const table_diff_html = diff2html.html();

Three way differences are handled by a separate call. The README says that for 3-way differences, that is comparing two tables given knowledge of a common ancestor, you use daff.compareTables3 and give the ancestor table as the first argument. The command line offers the same capability with the merge subcommand and a parent.csv argument.

The command line exposes diff, patch, merge and trim

daff also runs as a utility program, invoked as daff, daff.py or daff.rb depending on the installation. The summary of the interface is short enough to take in at a glance.

console
daff a.csv b.csv
daff [--color] [--no-color] [--output OUTPUT.csv] a.csv b.csv
daff [--output OUTPUT.html] a.csv b.csv
daff [--www] a.csv b.csv
daff parent.csv a.csv b.csv
daff --input-format sqlite a.db b.db
daff patch [--inplace] a.csv patch.csv
daff merge [--inplace] parent.csv a.csv b.csv
daff trim [--output OUTPUT.csv] source.csv
daff render [--output OUTPUT.html] diff.csv
daff copy in.csv out.tsv
daff in.csv
daff git
daff version

The verbs are worth reading carefully. diff is the default when you pass two files. patch applies a previously produced diff to a table, and merge does a three-way merge given a parent. trim produces a normalised version of a source table. render turns a diff table into HTML, and copy converts between formats.

The --inplace option to patch and merge modifies the input file directly, which the README calls out explicitly because it is the one flag in the summary that changes your data on disk.

The formats are CSV, TSV, Sqlite (with --input-format sqlite or the .sqlite extension), and ndjson. The --input-format flag also accepts ssv, psv and json, and --output-format accepts csv, tsv, ssv, psv, json, copy and html.

One default worth knowing: --ordered assumes row order is meaningful, and that is the default for CSV, while --unordered assumes row order is meaningless and is the default for json formats. If your tables are keyed sets that happen to be exported in different orders, the CSV default will report spurious changes.

The flags that decide whether a diff is right

The full flag list is where the real behaviour lives, and a few of these flags change the answer rather than the presentation.

The most important is --id, which specifies the column to use as a primary key, and can be repeated for a multi-column key. Without it, daff has to infer row identity from content, and it will get that wrong as soon as a cell changes. With --id, a row whose key matches is compared against its counterpart even when every other field differs.

--ignore does the opposite, specifying a column to ignore completely, and it can be repeated. --all, --all-rows and --all-columns control pruning of unchanged rows and columns.

--act restricts the output to a certain kind of change, one of update, insert, delete or column. --padding selects the padding method for aligning columns, one of dense, sparse or smart.

For scripts, --fail-if-diff is the one that changes exit status: the README states that it returns status 0 if equal, 1 if different, 2 if problem. That gives you a diff you can use as a build gate without parsing the output.

Two comparison tolerances are provided. -w, or --ignore-whitespace, ignores changes in leading and trailing whitespace, and -i, or --ignore-case, ignores differences in case.

For output, --index includes row and column numbers from the original tables, --eol sets the separator between rows of CSV output to crlf, lf, cr or auto, and --context and --context-columns control how many rows or columns of context are shown around a change.

For SQL sources, --table compares the named table, and if the name changes between the two sides you write it as n1:n2.

daff git csv teaches git about rows and columns

The feature that sets daff apart from a general diff tool is its git integration. Running daff git csv installs daff as a diff and merge handler for *.csv files in your repository, and daff git prints instructions for doing it manually.

The README makes the claim in one line: your CSV diffs and merges will get smarter, since git will suddenly understand about rows and columns, not just lines. That is the whole mechanism. Git hands the two versions of the file to daff instead of diffing them line by line, so an inserted row does not register as a change to every line below it, and a merge conflict is computed over rows rather than over byte offsets.

This is the case where a table-aware diff is worth more than a text diff, because it is exactly the case where the text diff is misleading. Large CSV files under version control, exported from a database with a stable key column, are where a naive line diff produces a diff nobody can read.

The requirement that comes with it is the --id flag. Git has no idea which column is the primary key, so the git handler needs the same information. If your CSV has no obvious key, the git integration will produce noisy results, and you would be better off not installing it.

There is also a browser route to the same functionality. The README points to a Chrome extension, csvhub, for viewing CSV diffs on GitHub.

Five language bindings, one set of semantics

daff is packaged for several languages, and the install commands are given side by side.

sh
npm install daff -g  # node/javascript
pip install daff     # python
gem install daff     # ruby
composer require paulfitz/daff-php  # php
install.packages('daff') # R wrapper by Edwin de Jonge

The npm package exposes a bin entry, so the install gives you a command line tool as well as a library. The Python packaging registers a console script entry point named daff and installs daff.py as a script, and the setup file declares the same MIT licence and the same keywords you would expect from the JavaScript manifest.

There is a web path as well, for use in a browser.

html
<script src="https://cdn.jsdelivr.net/npm/daff@latest/lib/daff.min.js"></script>

The README notes that other translations are collected under a specific release tag on the releases page, and that a minimal demo is on the project website.

The implementation detail behind all of this is in the build. The Makefile describes itself as not so much a Makefile as a collection of scripts, and the JavaScript build runs haxe against language/js.hxml to produce lib/daff.js, then concatenates a series of files from env/js onto it, and produces a frozen web-compatible core at lib/core.js. The npm scripts run make js for both prepare and prepublishOnly, so the published JavaScript is built from source rather than committed.

The same JavaScript file is served two ways. lib/daff.js is the library, and bin/daff.js is the same content with a node shebang added and the executable bit set, which is how one codebase gives you both an importable module and a command line program.

Where daff is the wrong tool

daff is a table comparison tool, and treating it as a general diff tool is the most common mistake.

If your data has no stable identity across versions, the alignment degrades. daff matches rows by content, so a table where most rows change every export has no anchors to align on, and you end up with a diff that reports everything as an insert and a delete. The fix is not a flag, it is a key column in the data.

If your comparison is between unrelated tables rather than two versions of the same one, daff is the wrong shape of tool. Its whole model is the common ancestor, and the merge command is a three-way merge requiring a parent.

If a downstream tool expects a unified diff, the output will not satisfy it. The default output is a diff table in the tabular-diff format, not a text patch. You can render HTML, but you are not producing something a standard patch applicator accepts.

The --input-format sqlite mode and the --table flag show that the tool can read database tables, but the README does not describe writing back to a database, so a workflow that needs the merged result persisted into SQL is not covered by what is documented.

On the packaging side, the Python classifier list describes the project as Development Status :: 3 - Alpha, while the JavaScript package version reads 1.4.2. The release history explains part of the gap: the listed GitHub releases are v1.4.2 and v1.4.1, both dated 2025-05-04, and v1.3.50 from 2023-09-15. The repository was last pushed to on 2026-05-27 and is not archived, so work continues in the tree even though published release tags are sparse. Pin a version deliberately rather than tracking latest, particularly for the browser CDN script, which points at daff@latest and can change under you.

Editorial conclusion

Adopt daff when your data has columns and rows that mean something, when you need a three-way merge against a common ancestor, or when you want git to stop treating a CSV as a bag of lines. Do not adopt it for plain text diffing, for tables with no stable identity between versions, or for a workflow that needs its output consumed by a tool expecting a unified diff. Before committing, check the version you are pinning against the release list, since the npm package and the Python package are released separately from the tag history, and confirm that your tables have an identifier column you can pass with --id, because daff aligns rows by matching content and the alignment gets worse as tables drift apart.

Frequently asked questions

How do I install daff?

The README gives npm install daff -g for node and javascript, pip install daff for python, gem install daff for ruby, composer require paulfitz/daff-php for php, and install.packages('daff') for the R wrapper. A browser build is served from jsdelivr.

How do I make daff compare rows by a key column instead of by content?

Use the --id flag to specify the column to use as a primary key, and repeat it for a multi-column key. The README also offers --ignore to skip a column completely.

Can daff do a three way merge?

Yes. The merge subcommand takes a parent table along with the two versions, and the library exposes daff.compareTables3 with the ancestor table as the first argument.

What is daff git csv for?

It installs daff as a diff and merge handler for csv files in a git repository, so git works with rows and columns rather than lines. Running daff git prints the instructions for doing the same thing by hand.

How do I use daff in a build script that should fail on differences?

Use the --fail-if-diff flag. The README states that it returns status 0 if equal, 1 if different and 2 if problem.

Which file formats does daff read and write?

The supported formats are CSV, TSV, Sqlite with --input-format sqlite or the .sqlite extension, and ndjson. The --input-format and --output-format flags also accept ssv, psv, json, copy and html.

Official sources

  1. License: MIT
  2. paulfitz/daff on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/paulfitz-daff.svg)](https://hysenlabs.com/projects/paulfitz-daff)

Was this useful?