Independent engineering project / Heterogeneous Database Migration Verifier

Heterogeneous Database Migration Verifier

A Java migration verifier comparing H2 and SQLite records across decimal, time, Unicode and null-handling differences. Controlled fixtures expose where normalization can conceal data corruption.

Contribution
Independent design, implementation and evaluation
Languages
Java / PowerShell
Engineering focus
Row-level reconciliation / Silent corruption taxonomy / Cutover gating

Scope. Self-directed modernization study measured in a local or simulated environment; not a customer engagement, and not evidence of production migration outcomes.

Implementation & evaluation

The project README is reproduced below, with links to the implementation, design records and operating instructions. Scenario narratives describe an independent project, not a customer deployment.

Read on GitHub

migration-verifier

Silent data corruption during a database migration is loss of injectivity -- and the same criterion decides whether the verifier you are using to look for it can see anything at all.

A migration moves 29 rows from H2 to SQLite. Nothing goes wrong. Every row comes back different.

account  source 10001            (String)     target 10001            (Integer)
amount   source 10.0000          (BigDecimal) target 10               (Integer)
active   source true             (Boolean)    target 1                (Integer)
seen     source 2024-01-15 09:00 (Timestamp)  target 1705309200000    (Long)

Three columns differ on every row of a migration in which nothing went wrong. The signal-to-noise ratio of a naive comparison is not poor -- it is zero, because the noise is total. So you write canonicalisation rules to normalise the noise away. Each rule you add makes the verifier quieter. Some of those rules are also deleting the evidence, and the verifier cannot tell you which, because a rule that hides a defect and a rule that removes noise produce the same output: silence.

This project measures which rules are which, and derives the criterion that separates them.

The result

A rule is safe if and only if it is injective -- if it never maps two distinct inputs to the same output. '0000007' -> 7 is not injective, so it is corruption. 0.1 -> 0.1000 is injective, so it is representation. That single test, applied to the verifier's own rules, predicts every blindfold measured here:

rule injective can it blind the verifier?
numeric, boolean, identifier yes never, in any measured configuration
trim, nfc, casefold, temporal no yes -- casefold costs 24 of 26 detections, silently

The converse does not hold and is not claimed: two non-injective rules never blinded anything here, which is a fact about this corpus, not a theorem.

What is measured

Everything in docs/results.md is generated by running the code against real H2 and SQLite JDBC drivers. Eleven predictions were registered before the runs; three held and eight were contradicted, and the contradictions are the interesting part. Highlights:

  • The rule lattice has no gradient. Seven rules, 128 subsets. 120 of them false-flag all 27 clean rows; 8 flag none. Nothing in between. The 8 that work are exactly the subsets containing {numeric, boolean, temporal, identifier}. This is why verifier tuning gets abandoned: the first three correct fixes produce byte-identical output, so it looks like nothing you do helps.
  • A set-level check catches what no row comparison can. COUNT(DISTINCT name) returns 26 on the source and 27 on the target across rows that are byte-identical one at a time -- the target's collation folds a smaller alphabet, so a UNIQUE(name) constraint silently weakens. Then three of the five defective migrators defeat that check by compensating error, their own damage merging a pair and cancelling the divergence back to 26 = 26.
  • Copy migration and dual-write are not the same problem. Identical rule set: precision 1.00 verifying a copy, 0.07 verifying a dual write, because the write path binds the timestamp as a string and the temporal rule stops recognising it.
  • The verifier certifies a column whose meaning is machine-dependent. 2024-03-31 02:30:00 does not exist in Europe/London. The target stores it as epoch milliseconds with no zone. Reading it back in four zones gives four different wall clocks. The verifier reports zero differences, correctly, because the bytes agree.
  • The cutover gate. "The verifier reported no differences" is consistent with a correct migration and with a verifier that cannot detect anything. The gate plants known defects and requires the verifier to be observed catching its own controls before its silence is allowed to mean anything. The perfect-precision configuration -- the one anybody would ship -- fails that gate.

Running it

Requires a JDK 21 and Maven. No network at runtime; no database server (H2 is in-memory, SQLite is a temp file).

.\demo.ps1     # regenerate docs/results.md and print the headline numbers
.\test.ps1     # full verification: build, 121 tests, report freshness, determinism, mutation

Or directly:

$env:JAVA_HOME = '<jdk-21>'
mvn compile
mvn exec:java          # writes docs/results.md
mvn test               # 121 tests

The surefire JVM is pinned to -Duser.timezone=UTC in pom.xml. That is not a flaky-test workaround -- it is one of the findings expressed as build configuration, and the reason is documented at the setting.

Layout

file what it is
Engine.java the two engines behind one interface; every type-behaviour claim in the report is a live probe, not a citation
Hazard.java the eleven-mechanism taxonomy, two of which were measured and dismissed
Corpus.java 29 rows, one per hazard instance, each labelled with ground truth
Migrator.java one faithful migrator and five defective ones, each declaring by construction which rows it damages
Rule.java column-scoped canonicalisation rules, each knowing whether it is injective
Comparison.java four verifier strategies from row-count to full canonicalisation
DualWrite.java, Backfill.java the two live-cutover mechanics, measured separately
CutoverGate.java the operational artifact: GO, NO_GO_CORRUPTION, NO_GO_BLIND, NO_GO_UNUSABLE
Experiments.java the report as code -- eleven sections, eleven registered predictions

Reading order

  1. docs/results.md -- the measurements
  2. docs/portfolio/01-the-problem.md -- why this is hard
  3. docs/portfolio/03-the-injectivity-criterion.md -- the idea
  4. docs/portfolio/04-what-the-tests-caught.md -- four defects the tests found in this code
  5. docs/known-limitations.md -- what this does not show

Source & documentation

Continue into the implementation.

The source repository contains setup instructions, design decisions, evaluation guidance and the project's stated limitations.

Browse supporting documentation 11 documents / 5 decision records