fix: dynamic FK re-add with column existence check

Previous migration hardcoded ALTER TABLE ADD CONSTRAINT for FK re-add,
but production DB may have different columns than CREATE TABLE schema
(api_keys had no user_id). Now uses _bf_add_fk() helper that checks
both source column and target column exist before adding FK. Skips
silently if either is missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mitchell R 2026-05-26 13:32:18 +02:00
parent fe9c51d3f0
commit 02b69713c3
No known key found for this signature in database

View file

@ -492,6 +492,30 @@ export const TENANT_MIGRATIONS: readonly string[] = [
// databases that already have TEXT PKs (DO NOTHING on conflict).
// gen_random_uuid() generates UUIDv4 — close enough for backfill.
// New rows already use app-generated UUIDv7 from repository.ts.
`CREATE OR REPLACE FUNCTION _bf_add_fk(
src_table text, src_col text, ref_table text, ref_col text, on_del text
) RETURNS void LANGUAGE plpgsql AS $fn$
DECLARE
col_exists boolean;
ref_exists boolean;
cname text;
BEGIN
SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = src_table AND column_name = src_col
) INTO col_exists;
SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = ref_table AND column_name = ref_col
) INTO ref_exists;
IF NOT col_exists OR NOT ref_exists THEN RETURN; END IF;
cname := src_table || '_' || src_col || '_fkey';
EXECUTE format(
'ALTER TABLE %I ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES %I(%I) ON DELETE %s',
src_table, cname, src_col, ref_table, ref_col, on_del
);
END $fn$`,
`DO $$
DECLARE
col_type text;
@ -547,65 +571,36 @@ export const TENANT_MIGRATIONS: readonly string[] = [
EXECUTE format('DROP SEQUENCE IF EXISTS %I CASCADE', r.sequence_name);
END LOOP;
-- 4. Re-add FK constraints.
ALTER TABLE sessions ADD CONSTRAINT sessions_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE api_keys ADD CONSTRAINT api_keys_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE camera_streams ADD CONSTRAINT camera_streams_camera_id_fkey
FOREIGN KEY (camera_id) REFERENCES cameras(id) ON DELETE CASCADE;
ALTER TABLE display_layouts ADD CONSTRAINT display_layouts_display_id_fkey
FOREIGN KEY (display_id) REFERENCES displays(id) ON DELETE CASCADE;
ALTER TABLE display_layouts ADD CONSTRAINT display_layouts_layout_id_fkey
FOREIGN KEY (layout_id) REFERENCES layouts(id) ON DELETE CASCADE;
ALTER TABLE layout_cells ADD CONSTRAINT layout_cells_layout_id_fkey
FOREIGN KEY (layout_id) REFERENCES layouts(id) ON DELETE CASCADE;
ALTER TABLE layout_cells ADD CONSTRAINT layout_cells_camera_id_fkey
FOREIGN KEY (camera_id) REFERENCES cameras(id) ON DELETE SET NULL;
ALTER TABLE kiosks ADD CONSTRAINT kiosks_display_id_fkey
FOREIGN KEY (display_id) REFERENCES displays(id) ON DELETE SET NULL;
ALTER TABLE kiosk_labels ADD CONSTRAINT kiosk_labels_kiosk_id_fkey
FOREIGN KEY (kiosk_id) REFERENCES kiosks(id) ON DELETE CASCADE;
ALTER TABLE kiosk_labels ADD CONSTRAINT kiosk_labels_label_id_fkey
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
ALTER TABLE camera_labels ADD CONSTRAINT camera_labels_camera_id_fkey
FOREIGN KEY (camera_id) REFERENCES cameras(id) ON DELETE CASCADE;
ALTER TABLE camera_labels ADD CONSTRAINT camera_labels_label_id_fkey
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
ALTER TABLE layout_labels ADD CONSTRAINT layout_labels_layout_id_fkey
FOREIGN KEY (layout_id) REFERENCES layouts(id) ON DELETE CASCADE;
ALTER TABLE layout_labels ADD CONSTRAINT layout_labels_label_id_fkey
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
ALTER TABLE event_log ADD CONSTRAINT event_log_source_kiosk_id_fkey
FOREIGN KEY (source_kiosk_id) REFERENCES kiosks(id) ON DELETE SET NULL;
ALTER TABLE event_log ADD CONSTRAINT event_log_source_camera_id_fkey
FOREIGN KEY (source_camera_id) REFERENCES cameras(id) ON DELETE SET NULL;
ALTER TABLE kiosk_gpio_bindings ADD CONSTRAINT kiosk_gpio_bindings_kiosk_id_fkey
FOREIGN KEY (kiosk_id) REFERENCES kiosks(id) ON DELETE CASCADE;
ALTER TABLE kiosk_logs ADD CONSTRAINT kiosk_logs_kiosk_id_fkey
FOREIGN KEY (kiosk_id) REFERENCES kiosks(id) ON DELETE CASCADE;
ALTER TABLE camera_event_subscriptions ADD CONSTRAINT camera_event_subscriptions_camera_id_fkey
FOREIGN KEY (camera_id) REFERENCES cameras(id) ON DELETE CASCADE;
ALTER TABLE camera_event_subscriptions ADD CONSTRAINT camera_event_subscriptions_subscribed_by_kiosk_id_fkey
FOREIGN KEY (subscribed_by_kiosk_id) REFERENCES kiosks(id) ON DELETE SET NULL;
ALTER TABLE displays ADD CONSTRAINT displays_default_layout_id_fkey
FOREIGN KEY (default_layout_id) REFERENCES layouts(id) ON DELETE SET NULL;
ALTER TABLE pairing_codes ADD CONSTRAINT pairing_codes_consumed_by_kiosk_id_fkey
FOREIGN KEY (consumed_by_kiosk_id) REFERENCES kiosks(id) ON DELETE SET NULL;
ALTER TABLE firmware_releases ADD CONSTRAINT firmware_releases_uploaded_by_fkey
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE firmware_rollouts ADD CONSTRAINT firmware_rollouts_release_id_fkey
FOREIGN KEY (release_id) REFERENCES firmware_releases(id) ON DELETE CASCADE;
ALTER TABLE firmware_rollouts ADD CONSTRAINT firmware_rollouts_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE os_update_releases ADD CONSTRAINT os_update_releases_uploaded_by_fkey
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE os_update_rollouts ADD CONSTRAINT os_update_rollouts_release_id_fkey
FOREIGN KEY (release_id) REFERENCES os_update_releases(id) ON DELETE CASCADE;
ALTER TABLE os_update_rollouts ADD CONSTRAINT os_update_rollouts_created_by_fkey
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE entities ADD CONSTRAINT entities_camera_id_fkey
FOREIGN KEY (camera_id) REFERENCES cameras(id) ON DELETE CASCADE;
-- 4. Re-add FK constraints (only if both table and column exist).
PERFORM _bf_add_fk('sessions', 'user_id', 'users', 'id', 'CASCADE');
PERFORM _bf_add_fk('api_keys', 'user_id', 'users', 'id', 'CASCADE');
PERFORM _bf_add_fk('camera_streams', 'camera_id', 'cameras', 'id', 'CASCADE');
PERFORM _bf_add_fk('display_layouts','display_id', 'displays', 'id', 'CASCADE');
PERFORM _bf_add_fk('display_layouts','layout_id', 'layouts', 'id', 'CASCADE');
PERFORM _bf_add_fk('layout_cells', 'layout_id', 'layouts', 'id', 'CASCADE');
PERFORM _bf_add_fk('layout_cells', 'camera_id', 'cameras', 'id', 'SET NULL');
PERFORM _bf_add_fk('kiosks', 'display_id', 'displays', 'id', 'SET NULL');
PERFORM _bf_add_fk('kiosk_labels', 'kiosk_id', 'kiosks', 'id', 'CASCADE');
PERFORM _bf_add_fk('kiosk_labels', 'label_id', 'labels', 'id', 'CASCADE');
PERFORM _bf_add_fk('camera_labels', 'camera_id', 'cameras', 'id', 'CASCADE');
PERFORM _bf_add_fk('camera_labels', 'label_id', 'labels', 'id', 'CASCADE');
PERFORM _bf_add_fk('layout_labels', 'layout_id', 'layouts', 'id', 'CASCADE');
PERFORM _bf_add_fk('layout_labels', 'label_id', 'labels', 'id', 'CASCADE');
PERFORM _bf_add_fk('event_log', 'source_kiosk_id', 'kiosks', 'id', 'SET NULL');
PERFORM _bf_add_fk('event_log', 'source_camera_id', 'cameras', 'id', 'SET NULL');
PERFORM _bf_add_fk('kiosk_gpio_bindings','kiosk_id', 'kiosks', 'id', 'CASCADE');
PERFORM _bf_add_fk('kiosk_logs', 'kiosk_id', 'kiosks', 'id', 'CASCADE');
PERFORM _bf_add_fk('camera_event_subscriptions','camera_id', 'cameras', 'id', 'CASCADE');
PERFORM _bf_add_fk('camera_event_subscriptions','subscribed_by_kiosk_id','kiosks', 'id', 'SET NULL');
PERFORM _bf_add_fk('displays', 'default_layout_id', 'layouts', 'id', 'SET NULL');
PERFORM _bf_add_fk('pairing_codes', 'consumed_by_kiosk_id', 'kiosks', 'id', 'SET NULL');
PERFORM _bf_add_fk('firmware_releases','uploaded_by', 'users', 'id', 'SET NULL');
PERFORM _bf_add_fk('firmware_rollouts','release_id', 'firmware_releases', 'id', 'CASCADE');
PERFORM _bf_add_fk('firmware_rollouts','created_by', 'users', 'id', 'SET NULL');
PERFORM _bf_add_fk('os_update_releases','uploaded_by', 'users', 'id', 'SET NULL');
PERFORM _bf_add_fk('os_update_rollouts','release_id', 'os_update_releases', 'id', 'CASCADE');
PERFORM _bf_add_fk('os_update_rollouts','created_by', 'users', 'id', 'SET NULL');
PERFORM _bf_add_fk('entities', 'camera_id', 'cameras', 'id', 'CASCADE');
RAISE NOTICE 'UUIDv7 migration: complete — all PKs and FKs are now TEXT';
END $$`,