fix(db): repair fresh migration chain
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user