A migration is done when you can show that every record from the old system is in the new one with the same values, and nothing else got in. Comparing row counts is not enough: in the example below the counts differ by 33, but the truth is 37 missing customers and 4 test records that should not be there, plus 1,297 customers whose data changed on the way.
The reliable check is a compare by key: pair each record with its counterpart by an ID that both systems keep (customer number, invoice number, SKU), then sort every record into one of four groups.
The four checks
- Missing records (removed): the key is in the old export only. Usually a failed batch, a filter in the migration script or a rejected row.
- Extra records (added): the key is in the new export only. Test data, duplicates created by a rerun, or records made after the cut-over.
- Changed values: the key is in both, a value differs. The count per column tells you whether it is one bad record or a systematic mapping error.
- Repeated keys: a key that occurs more than once cannot be paired. A rerun that inserted rows twice shows up here.
Every record has to land in exactly one group, so the counts must add up: rows in the old file = unchanged + changed + removed + repeated. If they do, nothing was skipped.
An example: 250,000 customers
The old CRM and the new system each export the customer table as CSV with the same columns; the new system adds a migrated_at column. Compared by customer_id, about a second on my desktop:
calamo old_crm_customers.csv new_system_customers.csv --key customer_id \
--ignore migrated_at --tolerance 0.01 --xlsx report.xlsx
1,297 changed rows (1,297 cells)
4 added
37 removed
248,666 unchanged
A: 250,000 = 248,666 unchanged + 1,297 changed + 37 removed + 0 repeated
B: 249,967 = 248,666 unchanged + 1,297 changed + 4 added + 0 repeated
Most changed columns: phone (1,200), city (90), plan (7)
What the result says about this migration:
- phone, 1,200 changes:
0949416351became949416351. The target column is a number, so the leading zero is gone. A mapping error: fix the column type and run that part again. - city, 90 changes:
KošicebecameKoÅ¡ice. A step in between read UTF-8 as another encoding. Only names with accents are hit, which is why it is easy to miss in a sample. - plan, 7 changes: a few customers moved to Basic. A lookup table in the migration is missing a value.
- 37 removed: customers that never arrived. The Removed sheet of the report lists them for the rerun.
- 4 added: test customers left in the new system.
- balance: 70 balances differ by one cent from rounding.
--tolerance 0.01accepts that, and the report says so. Without it they would be listed too.
A report to sign off
Save report writes one Excel workbook with the sheets Summary, Changed, Added, Removed and, when there are any, Duplicates. The summary has the counts and how they add up, the key and every setting, the time of the comparison, and a fingerprint of both files: SHA-256, size and modification time. Whoever signs off the migration can see exactly which exports were compared and with which rules.

Run it in the migration script
The command line version gives an exit code: 0 when the files match, 1 when rows were added, removed or changed, 2 on an error. A migration pipeline can stop on 1 and keep the report as evidence. --json writes the full result for your own checks.
Before you compare
- Same query on both sides. The same columns, the same filter (for example no records made after the cut-over) and the same format for dates.
- A key both systems keep. If the new system makes new IDs, export the old ID with it. For order lines and similar, use a key of several columns.
- Column names. Columns are paired by name. Rename in the export query, for example
SELECT cust_no AS customer_id. - Leave out what must differ. Technical columns such as
migrated_ator new internal IDs.
What Calamo treats as equal: the same number in another format (19.90 and 19.9), and spaces at the start or end. Leading zeros are kept (00123 and 123 are different IDs), letter case counts. Each rule can be switched off. All the rules.
Free tool or Calamo Pro
The free browser tool runs the same compare on CSV files up to a few hundred MB, and you can leave columns out there too. The example above, two files of 24 and 29 MB, works in it.
Calamo Pro is the desktop app and command line for the rest of a migration: exports of several GB (4.3 GB measured), Excel and Parquet files, number tolerance, the sign-off report with fingerprints, a marked copy of the new file, and the exit code for scripts. $49 once, free for 14 days, on Windows, Linux and macOS (beta).
Questions
Can Calamo read straight from the database?
No. Export the table from each system as CSV, Excel or Parquet, then compare the two files. Use the same query on both sides, so the columns and formats match.
The column names differ between the old and the new system. What now?
Columns are paired by name. Rename them in the export query (SELECT cust_no AS customer_id) or in the header row of one file. Columns that exist in only one file are listed, not compared.
What if one ID is not unique, for example order lines?
Use a key of several columns, such as order number plus line number. Calamo also counts repeated keys and lists them, so you see when a key is not unique.
The new system writes dates in another format. Will every date show as changed?
Yes: dates are compared as text. Export them in the same format from both systems (ISO, 2026-11-06, is the safest), or leave the column out and check it separately.
Do the files leave my computer?
No. The free tool runs in your browser and Calamo Pro runs on your computer. Nothing is uploaded, which matters for customer and financial data.

