fix(server): rebuild empty drifted tables to heal pre-rename column drift
Companion to #29. `healInitialSchemaDrift` can only ADD columns, so it skips NOT NULL-without-default columns like `federation_outbox.entity_id` / `context_id` — which on some old pre-drizzle dev DBs carry the pre-rename names `message_id` / `dm_channel_id` instead. The tables load but the outbox worker fails every tick with "no such column: federation_outbox.context_id" once the server is up. Adds healRenamedColumns() — a second pass that runs right after `healInitialSchemaDrift`. For each table whose physical column set is *missing* columns declared by the current-migration-state snapshot AND which holds zero rows, it DROPs the table and rebuilds it from the snapshot's JSON: columns, defaults, foreign keys, composite PKs, unique constraints, indexes. Key design decisions: - **Target is the current-migration-state snapshot, not the latest on disk.** The current state is determined by the highest `__drizzle_migrations.created_at` matched against `_journal.json`'s `when` timestamps (with backward walk for idx values that lack a snapshot, like the hand-written 0002). Rebuilding to a *future* snapshot would introduce columns that drizzle's migrator is about to add via ALTER TABLE ADD COLUMN, causing duplicate-column errors. Rebuilding to the *current* snapshot preserves the invariant that drizzle's pending migrations can run cleanly afterwards. - **Missing-column gate, not extra-column.** Extra columns alone don't break anything at runtime (the ORM ignores them); they're leftover from pre-drizzle manual migrations and might matter to the operator. Missing columns DO break runtime queries, so only those trigger rebuild. - **Empty-table gate.** Non-empty tables log a warning and skip — data preservation wins over heal, and this path should only ever hit a pre-drizzle dev DB that never exercised the affected tables in the first place. - **Transactional rebuild.** DROP + CREATE + index reinstatement wrap in a single `db.transaction()` so a partial rebuild rolls back. Verified against three scenarios via in-memory simulation: (A) fresh install — heal no-op, drizzle creates everything; (B) pre- drizzle dev DB with fed_outbox/fed_mutation_log rename drift — tables rebuilt to 0000 snapshot, drizzle then applies 0001–0004 successfully to reach the current target schema; (C) post-migration- correct (production-like) — heal no-op, drizzle no-op, schema unchanged. Live boot on my actual dev DB: migrations complete silently, server binds :3005, no outbox worker errors. Server tests 110/110, web 131/131, typecheck clean. No migration files changed. Deployed Pi+VM instances are unaffected (their schema matches the snapshot exactly — heal won't touch anything). Closes backlog #30.
This commit is contained in:
@@ -3,7 +3,7 @@ import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
||||
import { config } from '../config.js';
|
||||
import * as schema from './schema.js';
|
||||
import { baselineExistingInstall, healInitialSchemaDrift, ensureDefaults } from './migrate.js';
|
||||
import { baselineExistingInstall, healInitialSchemaDrift, healRenamedColumns, ensureDefaults } from './migrate.js';
|
||||
import { setWorkerId } from '../utils/snowflake.js';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
@@ -35,6 +35,13 @@ export function initDatabase() {
|
||||
// a no-op on fresh installs and correctly-migrated DBs.
|
||||
healInitialSchemaDrift(sqlite);
|
||||
|
||||
// Second-pass heal for rename/removal drift that the ADD-only pass
|
||||
// can't resolve: for tables whose column set still doesn't match
|
||||
// the 0000 snapshot and which are empty, DROP and rebuild from
|
||||
// 0000_initial.sql. Non-empty tables are skipped with a warning.
|
||||
// No-op on correctly-migrated DBs.
|
||||
healRenamedColumns(sqlite);
|
||||
|
||||
// Apply any pending Drizzle migrations
|
||||
const migrationsFolder = resolve(__dirname, '../../drizzle');
|
||||
const db = drizzle(sqlite, { schema });
|
||||
|
||||
Reference in New Issue
Block a user