-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
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.
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.
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— aDateTimeOffsetbecomes its UTC instant; aDateTimewith 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.
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.
SQL Data Migrator — MIT licensed. The wiki mirrors the repo's docs/; when they disagree, docs/ wins.