From 108123fb86ce10aea0ae52b86562d78baa8af134 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Tue, 26 May 2026 13:26:46 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20dynamic=20FK=20drop=20in=20UUIDv7=20migr?= =?UTF-8?q?ation=20=E2=80=94=20handle=20all=20constraints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- server/src/shared/db/migrations-pg.ts | 197 ++++++++++++-------------- 1 file changed, 94 insertions(+), 103 deletions(-) diff --git a/server/src/shared/db/migrations-pg.ts b/server/src/shared/db/migrations-pg.ts index b197a4a..a26f7f1 100644 --- a/server/src/shared/db/migrations-pg.ts +++ b/server/src/shared/db/migrations-pg.ts @@ -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 $$`,