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