How do you validate a database migration with a small team?

Profile the source before the move and keep it as your baseline. Write down every renamed table and column, run tests built from the source profile against the target, and list each change you meant to make. Sign off when every failure matches that list. A matching row count proves the data arrived, not that it's right.

Key points

  • Validating a migration asks whether the target holds the same data as the source, except for the changes you meant to make. Health checks such as nulls, duplicates, and ranges can't answer that.
  • In DataKitchen's Jaffle Shop migration test, a 55-test health suite passed with zero failures after 483 orders were left behind. A 345-test Table Comparison suite built from the source profile failed 61 times, and every failure matched a planned change.
  • Five pieces make a migration provable: a dated source snapshot, a mapping of renamed tables and columns, reconciliation tests on the target, a written list of expected differences, and acceptance criteria tied to that list.
  • DataOps TestGen added Table Comparison tests on October 14, 2026, in open source and Enterprise. In DataKitchen's testing on three public datasets, they caught 774 of 785 planted changes.
  • Table Comparison is not a row-by-row diff. It never moves data between systems, so a failure tells you which column changed, not which row.

Most migrations get validated with a row count and some hope. The row count matches, someone says “looks fine,” and the cutover happens. Three months later a balance doesn’t tie and nobody can say whether the old system or the move caused it.

The question to answer is narrower and harder: is this the same data I started with, except for the changes I meant to make?

Why health checks pass on a bad migration

DataKitchen tested this on purpose in New in TestGen: Prove Your Data Migration Worked. We moved the Jaffle Shop demo database, split one table in two, fixed some hygiene problems, and left 483 orders behind. The standard 55-test health suite on the target found zero failures. The data looked healthy. It just wasn’t all there.

One company DataKitchen wrote about found its 6 billion-row database had only partly transferred to the cloud. A row-count test would have caught that one. It wouldn’t catch a trimmed code or a shifted date.

Five pieces a small team needs

PieceWhat it isIn the Jaffle Shop example
Source snapshotA dated profile of every source column, kept as the baselineSix tables profiled before the move, with value hashes
MappingA file of renamed, moved, or retired tables and columnsAn 11-row CSV: raw_orders.tax_paid became order_amounts.tax_amount, and so on
ReconciliationTests generated from the snapshot, run on the target345 Table Comparison tests on counts, lengths, sums, patterns, and hashes
Expected differencesThe written list of changes you meant to make483 orders quarantined, header rows removed, two strings trimmed, 13 columns retyped
Acceptance criteriaEvery failure traces to the list, and nothing else fails61 failures, every one matched to a planned change

The snapshot comes first for the same reason it does in any handover. Your legacy data has problems, and a dated record of them is what separates what you inherited from what the migration broke.

The mapping is the only part a person writes, and only when names changed. TestGen’s transfer_tests.py reads the CSV, renames every test, and writes a plain-text report of every rename and dropped test so the approver can read it. Rewriting 345 tests by hand is how a migration suite gets its own bugs.

Reconciliation is the step that gets cut when the schedule slips. Validation needs comparison: tests that check completeness, accuracy, and consistency between source and target, not either one alone. Our 345 tests ran in five seconds, so they can run after every mock load instead of once at cutover.

On cost: TestGen open source includes Table Comparison and covers one user, one connection, and one project. Enterprise is $100 per user and $100 per connection per month, per the pricing page.

What the tests can’t decide

The tests find differences. They can’t tell you which ones you meant. Your migration script and the people who know the data decide that, which is why the expected-differences list has to exist before cutover. Without it, 61 failures is a panic. With it, 61 failures is a sign-off sheet. That works the way a data contract works as a receipt: a list you can check.

Know the limits too. Table Comparison is not a row-by-row diff. It profiles the source where it lives, runs queries on the target where it lives, and compares the results, which is why it works across engines such as SQL Server to Snowflake. In DataKitchen’s testing on three public datasets with 785 planted changes, it caught 774. The misses were binary columns, type changes inside the same general type, and values swapped across a composite key. If your migration risks one of those, write a specific test for it.

Frequently asked questions

Short answers to the follow-up questions people ask next.

Why isn't a matching row count enough to validate a migration?

A row count tells you the data arrived. It doesn't tell you a value changed, a code got trimmed, a date shifted across a time zone, or two values swapped rows while the totals held. Validation needs tests on each column's counts, lengths, sums, patterns, and value hashes, compared with what the source looked like before the move.

Why do normal data quality tests pass on a broken migration?

Because they check whether data looks healthy, not whether it matches what was there before. In DataKitchen's Jaffle Shop example, 483 orders were left behind and a 55-test health suite still passed with zero failures. The migrated data had no blanks, no duplicate keys, and values in range. Nothing in a health check knows what used to be there.

How do you test a migration when tables and columns were renamed?

Keep the mapping in a file. TestGen's transfer_tests.py script reads a four-column CSV of source table, source column, target table, and target column, renames every test generated on the source, previews the import, and with --apply loads the suite onto the target. If nothing was renamed, the CSV holds only its header row and the tests move over as they are.

What should the acceptance criteria for a data migration be?

Every failing reconciliation test traces to a change on your written list, and nothing else fails. Write the list before cutover: rows quarantined, columns retyped, values trimmed, tables split. In the Jaffle Shop example all 61 failures matched planned changes, such as 483 quarantined orders and 13 retyped columns, so the sign-off was a list of explained differences, not a feeling before Go Live.

Can migration tests run across different databases?

Yes, with TestGen's Table Comparison set. Tests generated from a SQL Server profile run as SQL on Snowflake, because TestGen computes value hashes the same way on every database it supports, except SAP HANA and Salesforce Data 360. Some engine differences fail on a correct copy, such as Oracle padding CHAR columns. Deactivate those tests on the target instead of loosening them.

Try it on your own data

DataOps TestGen is free and open source. Profile a table and generate its tests this afternoon.