fix(install): defer events.hero_photo_id FK to break circular reference (#484)
Real root cause behind MrGabri's fresh-Postgres install crash, surfaced by his second log dump after #488 silenced the FATAL noise: Initial setup failed: error: alter table "events" add constraint "events_hero_photo_id_foreign" foreign key ("hero_photo_id") references "photos" ("id") on delete SET NULL - relation "photos" does not exist initializeDatabase() in src/database/db.js declared the FK inline at events createTable (line 89), but the photos table is created later in the same function (line 203). On Postgres this is a hard error — the referenced table must exist at FK-declaration time. SQLite silently tolerated it because its FK enforcement is lazy and the inline declaration just became a column with no FK metadata. Why no existing Postgres install hit it: initializeDatabase only runs the createTable block on `if (!hasEventsTable)`. Once a deployment has the events table from any prior run, the path is skipped. So the bug only ever fires on a truly fresh Postgres install — which is exactly MrGabri's scenario, and which our smoke suite never exercises (it runs against a long-lived dev stack). Fix: - events createTable: drop the inline FK; column declared as a plain integer with an explainer comment. - After both tables exist (post photos createTable): db.schema .alterTable('events').foreign('hero_photo_id').references... Wrapped in a try/catch that swallows "already exists" so re-runs on installs that previously got into a half-state don't fail boot. Verified by docker compose down -v + up against the dev stack — no FK error, all migrations apply, FK present in pg_constraint with the expected definition.
This commit is contained in:
@@ -86,7 +86,16 @@ async function initializeDatabase() {
|
||||
table.boolean('disable_right_click').defaultTo(false);
|
||||
table.boolean('watermark_downloads').defaultTo(false);
|
||||
table.text('watermark_text');
|
||||
table.integer('hero_photo_id').references('id').inTable('photos').onDelete('SET NULL');
|
||||
// events.hero_photo_id → photos.id is a forward reference (the
|
||||
// photos table is created later in this same function). Postgres
|
||||
// rejects FK declarations that reference a non-existent table at
|
||||
// CREATE TABLE time, so the constraint is added below as an
|
||||
// ALTER TABLE *after* the photos table exists. SQLite previously
|
||||
// tolerated the inline declaration because its FK enforcement is
|
||||
// lazy — the inline form silently became a column with no FK
|
||||
// metadata. Both backends now go through the same code path.
|
||||
// (#484, MrGabri's reproduction.)
|
||||
table.integer('hero_photo_id');
|
||||
table.boolean('require_password').defaultTo(true);
|
||||
});
|
||||
} else {
|
||||
@@ -213,6 +222,25 @@ async function initializeDatabase() {
|
||||
table.integer('view_count').defaultTo(0);
|
||||
table.integer('download_count').defaultTo(0);
|
||||
});
|
||||
|
||||
// Deferred FK: events.hero_photo_id → photos.id. See the comment
|
||||
// on the events createTable above for why this can't be inline.
|
||||
// Wrapped in try/catch so a re-run path or an SQLite install that
|
||||
// already accepted the inline (no-op) declaration doesn't fail
|
||||
// boot when the constraint already exists in some shape.
|
||||
try {
|
||||
await db.schema.alterTable('events', (table) => {
|
||||
table.foreign('hero_photo_id')
|
||||
.references('id').inTable('photos')
|
||||
.onDelete('SET NULL');
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err?.message || '';
|
||||
if (!/already exists|duplicate|exists/i.test(msg)) {
|
||||
throw err;
|
||||
}
|
||||
// Constraint already in place — fine, carry on.
|
||||
}
|
||||
}
|
||||
|
||||
// Access logs table
|
||||
|
||||
Reference in New Issue
Block a user