fix(db): repair fresh migration chain
CI and Deploy / test (pull_request) Successful in 5m19s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 20:47:36 +02:00
parent 191de69c48
commit 74a1e0d845
15 changed files with 297 additions and 35 deletions
+32 -9
View File
@@ -1,6 +1,6 @@
# Database ownership and startup order
> 2026-07-19. Which component creates which table, in what order, and why a clean MariaDB install used
> Updated 2026-08-15. Which component creates which table, in what order, and why a clean MariaDB install used
> to fail. Read this before adding a table or touching `StartupInitializationExtensions`.
## The problem this document exists to prevent
@@ -22,28 +22,35 @@ provider.
## Startup order
`InitializeJobTrackerAsync` runs exactly this sequence:
`InitializeJobTrackerAsync` runs this provider-aware sequence:
```
1. Connect
2. ReconcileSchema() ← pass 1: repair existing schema, create reconciler-owned tables
3. Database.Migrate() ← create every migration-owned table
4. ReconcileSchema() ← pass 2: everything pass 1 had to skip
2. ReconcileSchema() ← repair legacy schema/create prerequisites
3a. SQLite: apply one migration, reconcile, repeat
3b. MariaDB: apply the complete migration chain
4. ReconcileSchema() ← create/repair everything skipped before migrations
5. Seed admin, start services
```
### Why the reconciler runs twice
### Why migration sequencing differs by provider
Neither position alone works:
Neither a single reconciliation position nor one shared provider sequence works:
- **Pass 1 must come first.** A legacy database has hand-added columns and Identity tables that
predate the migrations; without repairing them (and stamping the legacy migration id into
`__EFMigrationsHistory`) `Migrate()` collides with them. `AddCareerProfileRelationalChildren` also
adds children that reference `CareerProfiles`, a **reconciler-owned** table — so it must exist
before migrations run.
- **Pass 2 must come after.** On a brand-new database the migration-owned tables do not exist during
- **The final pass must come after.** On a brand-new database the migration-owned tables do not exist during
pass 1, so every reconciler table that references one (FK into `JobApplications`) is skipped, as
are the index and `AUTO_INCREMENT` repairs.
- **SQLite reconciles between migrations.** Historical SQLite table rebuilds read the current model
shape, including columns that were originally supplied by reconciliation. The per-migration pass
establishes that shape before a later rebuild reads it.
- **MariaDB does not reconcile between migrations.** Its ALTER operations do not use SQLite table
rebuilds, and an intermediate pass could create a later migration's column early and cause a
duplicate-column failure. It applies the chain first and uses the shared final repair pass.
Every statement in `ReconcileSchema` is existence-guarded, so the second pass is a no-op scan on an
already-correct database. Two consequences worth knowing:
@@ -59,7 +66,9 @@ already-correct database. Two consequences worth knowing:
Created by EF migrations, never by the reconciler:
`Companies`, `JobApplications`, `Jobs`, `Correspondences`, `Attachments`, `JobEvents`,
`RuleSettings`, and the ASP.NET Identity tables.
`RuleSettings`, and the ASP.NET Identity tables. Two compatibility migrations use guarded
`CREATE TABLE IF NOT EXISTS` bootstraps for `AspNetUsers` and `AiInteractions` so standalone EF
tooling can traverse the historical chain; normal application startup makes those statements no-ops.
The reconciler may **repair** these (add a missing column, add an index, fix a non-`AUTO_INCREMENT`
primary key) and may seed the default `RuleSettings` row — but it must never `CREATE TABLE` them.
@@ -133,6 +142,12 @@ dotnet run --project JobTrackerApi/JobTrackerApi.csproj
Create the empty schema/database itself (`CREATE DATABASE jobtracker;`); the application builds
everything inside it.
Standalone EF tooling is also supported for a blank SQLite database. The historical initial
migration now supplies the stable JobApplication columns required by later SQLite rebuilds, and
guarded compatibility bootstraps provide the reconciler-owned source tables used by later additive
migrations. Application startup may subsequently reconcile the remaining Identity and auxiliary
tables without losing rows.
## Production upgrade
Deploy and restart. The reconciler is idempotent and additive:
@@ -159,3 +174,11 @@ All four scenarios, 2026-07-19, against MariaDB 11 and SQLite:
Column types on MariaDB spot-checked: `int AUTO_INCREMENT` primary keys, `varchar(255)` owner keys,
`datetime(6)` timestamps, `tinyint(1)` booleans, and every composite index inside the key limit.
On 2026-08-15 the current 29-migration chain was additionally verified against a blank standalone
SQLite database, an older populated SQLite checkpoint, and a disposable MariaDB 11.8 database.
Standalone SQLite migration and retry both reached the latest migration; populated title/date and
reconciler-owned owner/summary data survived. Starting the application over that EF-only database
served `/health` successfully. Fresh MariaDB startup and restart both served `/health` with 29
migrations and 49 tables; provider-sensitive ID, owner, decimal and timestamp column types were
spot-checked. No production database was changed.