fix: dynamic FK drop in UUIDv7 migration — handle all constraints

Previous migration hard-coded FK constraint names and missed
firmware_releases.uploaded_by, firmware_rollouts.created_by/release_id,
os_update_releases.uploaded_by, os_update_rollouts.created_by/release_id,
pairing_codes.consumed_by_kiosk_id, entities.camera_id.

Now uses information_schema to dynamically drop ALL FK constraints
before type conversion, and dynamically finds ALL integer id/*_id
columns to convert. No more missed FKs.

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

View file

@ -495,8 +495,8 @@ export const TENANT_MIGRATIONS: readonly string[] = [
`DO $$
DECLARE
col_type text;
r record;
BEGIN
-- Only run if users.id is still integer (proxy for "needs migration").
SELECT data_type INTO col_type
FROM information_schema.columns
WHERE table_schema = current_schema()
@ -509,112 +509,103 @@ export const TENANT_MIGRATIONS: readonly string[] = [
RAISE NOTICE 'UUIDv7 migration: converting INTEGER PKs to TEXT...';
-- 1. Drop all FK constraints first (PG won't let us alter referenced types).
-- sessions users
ALTER TABLE sessions DROP CONSTRAINT IF EXISTS sessions_user_id_fkey;
-- api_keys users
ALTER TABLE api_keys DROP CONSTRAINT IF EXISTS api_keys_user_id_fkey;
-- cameras no FK to other tables with integer PK
-- camera_streams cameras
ALTER TABLE camera_streams DROP CONSTRAINT IF EXISTS camera_streams_camera_id_fkey;
-- layouts no FK from layout.id
-- display_layouts displays, layouts
ALTER TABLE display_layouts DROP CONSTRAINT IF EXISTS display_layouts_display_id_fkey;
ALTER TABLE display_layouts DROP CONSTRAINT IF EXISTS display_layouts_layout_id_fkey;
-- layout_cells layouts, cameras
ALTER TABLE layout_cells DROP CONSTRAINT IF EXISTS layout_cells_layout_id_fkey;
ALTER TABLE layout_cells DROP CONSTRAINT IF EXISTS layout_cells_camera_id_fkey;
-- kiosks displays
ALTER TABLE kiosks DROP CONSTRAINT IF EXISTS kiosks_display_id_fkey;
-- labels standalone
-- kiosk_labels kiosks, labels
ALTER TABLE kiosk_labels DROP CONSTRAINT IF EXISTS kiosk_labels_kiosk_id_fkey;
ALTER TABLE kiosk_labels DROP CONSTRAINT IF EXISTS kiosk_labels_label_id_fkey;
-- camera_labels cameras, labels
ALTER TABLE camera_labels DROP CONSTRAINT IF EXISTS camera_labels_camera_id_fkey;
ALTER TABLE camera_labels DROP CONSTRAINT IF EXISTS camera_labels_label_id_fkey;
-- layout_labels layouts, labels
ALTER TABLE layout_labels DROP CONSTRAINT IF EXISTS layout_labels_layout_id_fkey;
ALTER TABLE layout_labels DROP CONSTRAINT IF EXISTS layout_labels_label_id_fkey;
-- event_log kiosks, cameras
ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_source_kiosk_id_fkey;
ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_source_camera_id_fkey;
-- kiosk_gpio_bindings kiosks
ALTER TABLE kiosk_gpio_bindings DROP CONSTRAINT IF EXISTS kiosk_gpio_bindings_kiosk_id_fkey;
-- kiosk_logs kiosks
ALTER TABLE kiosk_logs DROP CONSTRAINT IF EXISTS kiosk_logs_kiosk_id_fkey;
-- camera_event_subscriptions cameras, kiosks
ALTER TABLE camera_event_subscriptions DROP CONSTRAINT IF EXISTS camera_event_subscriptions_camera_id_fkey;
ALTER TABLE camera_event_subscriptions DROP CONSTRAINT IF EXISTS camera_event_subscriptions_subscribed_by_kiosk_id_fkey;
-- entities standalone
-- audit_log standalone
-- cloud_accounts standalone (already TEXT PK)
-- 1. Drop ALL foreign key constraints in current schema dynamically.
FOR r IN
SELECT tc.constraint_name, tc.table_name
FROM information_schema.table_constraints tc
WHERE tc.table_schema = current_schema()
AND tc.constraint_type = 'FOREIGN KEY'
LOOP
EXECUTE format('ALTER TABLE %I DROP CONSTRAINT IF EXISTS %I', r.table_name, r.constraint_name);
END LOOP;
-- 2. Convert PK columns: add TEXT column, backfill, swap.
-- Helper: for each table, ALTER COLUMN TYPE works if data is castable.
-- Integer TEXT cast is safe.
ALTER TABLE users ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE api_keys ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE displays ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE cameras ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE camera_streams ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE layouts ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE layout_cells ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE kiosks ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE labels ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE event_log ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE entities ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE kiosk_gpio_bindings ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE audit_log ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE kiosk_logs ALTER COLUMN id TYPE TEXT USING id::TEXT;
ALTER TABLE camera_event_subscriptions ALTER COLUMN id TYPE TEXT USING id::TEXT;
-- 2. Convert every integer/bigint column that is a PK or FK to TEXT.
FOR r IN
SELECT c.table_name, c.column_name
FROM information_schema.columns c
WHERE c.table_schema = current_schema()
AND c.data_type IN ('integer', 'bigint')
AND (
c.column_name = 'id'
OR c.column_name LIKE '%_id'
)
AND c.table_name NOT IN ('schema_migrations')
LOOP
EXECUTE format('ALTER TABLE %I ALTER COLUMN %I TYPE TEXT USING %I::TEXT',
r.table_name, r.column_name, r.column_name);
-- Drop any leftover default (sequences from old SERIAL columns).
EXECUTE format('ALTER TABLE %I ALTER COLUMN %I DROP DEFAULT', r.table_name, r.column_name);
END LOOP;
-- 3. Convert FK columns to TEXT too.
ALTER TABLE sessions ALTER COLUMN user_id TYPE TEXT USING user_id::TEXT;
ALTER TABLE api_keys ALTER COLUMN user_id TYPE TEXT USING user_id::TEXT;
ALTER TABLE camera_streams ALTER COLUMN camera_id TYPE TEXT USING camera_id::TEXT;
ALTER TABLE display_layouts ALTER COLUMN display_id TYPE TEXT USING display_id::TEXT;
ALTER TABLE display_layouts ALTER COLUMN layout_id TYPE TEXT USING layout_id::TEXT;
ALTER TABLE layout_cells ALTER COLUMN layout_id TYPE TEXT USING layout_id::TEXT;
ALTER TABLE layout_cells ALTER COLUMN camera_id TYPE TEXT USING camera_id::TEXT;
ALTER TABLE kiosks ALTER COLUMN display_id TYPE TEXT USING display_id::TEXT;
ALTER TABLE kiosk_labels ALTER COLUMN kiosk_id TYPE TEXT USING kiosk_id::TEXT;
ALTER TABLE kiosk_labels ALTER COLUMN label_id TYPE TEXT USING label_id::TEXT;
ALTER TABLE camera_labels ALTER COLUMN camera_id TYPE TEXT USING camera_id::TEXT;
ALTER TABLE camera_labels ALTER COLUMN label_id TYPE TEXT USING label_id::TEXT;
ALTER TABLE layout_labels ALTER COLUMN layout_id TYPE TEXT USING layout_id::TEXT;
ALTER TABLE layout_labels ALTER COLUMN label_id TYPE TEXT USING label_id::TEXT;
ALTER TABLE event_log ALTER COLUMN source_kiosk_id TYPE TEXT USING source_kiosk_id::TEXT;
ALTER TABLE event_log ALTER COLUMN source_camera_id TYPE TEXT USING source_camera_id::TEXT;
ALTER TABLE kiosk_gpio_bindings ALTER COLUMN kiosk_id TYPE TEXT USING kiosk_id::TEXT;
ALTER TABLE kiosk_logs ALTER COLUMN kiosk_id TYPE TEXT USING kiosk_id::TEXT;
ALTER TABLE camera_event_subscriptions ALTER COLUMN camera_id TYPE TEXT USING camera_id::TEXT;
ALTER TABLE camera_event_subscriptions ALTER COLUMN subscribed_by_kiosk_id TYPE TEXT USING subscribed_by_kiosk_id::TEXT;
-- displays.default_layout_id
ALTER TABLE displays ALTER COLUMN default_layout_id TYPE TEXT USING default_layout_id::TEXT;
-- 3. Drop orphan sequences (leftover from SERIAL columns).
FOR r IN
SELECT sequence_name
FROM information_schema.sequences
WHERE sequence_schema = current_schema()
AND sequence_name LIKE '%_id_seq'
LOOP
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 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;
RAISE NOTICE 'UUIDv7 migration: complete — all PKs and FKs are now TEXT';
END $$`,