Skip to content

How It Works

splaxtr edited this page Aug 26, 2026 · 1 revision

How It Works

The pipeline

A run is a fixed sequence. Every step can stop the run, and stopping is always safer than continuing:

Step What it does Why it can stop the run
1. Collation check Reads the target's collation A wrong collation is silent: sorting and search misbehave quietly, and fixing it later means recreating the database
2. Schema read Reads both information_schemas and the target's foreign keys
3. Plan Intersects source and target columns per table A NOT NULL target column with no source and no safe default cannot be filled
4. Source-only check Lists source tables absent from the target Their data would be silently left behind
5. Pre-flight Scans source data for NULLs headed into NOT NULL columns and over-length values Better to stop before the copy than to fail halfway through it
6. Copy Truncate + binary COPY, table by table, inside one transaction
7. Sequence fixup setval on every identity sequence Without this, the first insert after migration collides
8. Verify Row counts per table, then foreign-key orphans Failure rolls the whole thing back
9. Commit Only now

Steps 6–9 share a single transaction. That is the load-bearing decision in the codebase: verification runs before the commit, not after it. Verifying afterwards would detect problems and leave them in place — a failure report sitting next to committed bad data.

Why constraint triggers are suspended — and re-checked

During the copy, foreign-key triggers are suspended (session_replication_role = replica) so table order stops mattering. That suspension is exactly why step 8 re-checks every foreign key for orphaned child rows: without it, an inconsistency in the source would enter the target while the constraints still claim to be valid.

Type mapping

Mapping is driven by the target store type: the engine asks the target what a column is and converts the incoming value to that. The table grows with target types, not with the product of source × target types.

Two mappings encode real decisions:

  • timestamp — written verbatim with no UTC conversion. A wall-clock value stays the same wall-clock value; converting here would shift every timestamp by the migrating machine's offset.
  • timestamptz — a DateTimeOffset becomes its UTC instant; a DateTime with no kind is treated as already-UTC rather than being shifted by the host's timezone, because the host's timezone is not part of the data.

Text is never case-folded or normalised — casing is data; the target's collation governs comparison, never storage.

The two projects

src/Migrator.Core/   the engine — no UI, no HTTP, no console
src/Migrator.App/    a local web app: minimal API + one HTML page

The engine reports progress through IProgress<ProgressMessage> and speaks in stable message codes, so the same engine can drive a web page today and a CLI tomorrow. The provider-specific code (SQL Server reader, PostgreSQL writer) is grouped along a visible seam that the Roadmap promotes to interfaces before a second provider lands — the full map is in docs/ARCHITECTURE.md.

Clone this wiki locally