-- Repair duplicate canonical asset keys without deleting historical rows. -- Safety: bounded quarantine, transactional index rebuild, independent verifier. SET lock_timeout = '10s'; SET statement_timeout = '90s'; SET enable_indexscan = 'off'; SET enable_indexonlyscan = 'off'; SET enable_bitmapscan = 'off'; CREATE TEMP TABLE asset_inventory_integrity_repair_map ON COMMIT DROP AS WITH ranked_assets AS ( SELECT asset_id, asset_key AS original_asset_key, FIRST_VALUE(asset_id) OVER ( PARTITION BY asset_key ORDER BY first_seen_at ASC, asset_id ASC ) AS canonical_asset_id, ROW_NUMBER() OVER ( PARTITION BY asset_key ORDER BY first_seen_at ASC, asset_id ASC ) AS duplicate_rank FROM asset_inventory ) SELECT asset_id, original_asset_key, canonical_asset_id FROM ranked_assets WHERE duplicate_rank > 1; DO $$ DECLARE repair_row_count BIGINT; BEGIN SELECT COUNT(*) INTO repair_row_count FROM asset_inventory_integrity_repair_map; IF repair_row_count > 100 THEN RAISE EXCEPTION 'asset_inventory_integrity_repair_bound_exceeded: % rows', repair_row_count; END IF; END $$; UPDATE asset_inventory AS duplicate_asset SET asset_key = duplicate_asset.asset_key || '/__integrity_quarantine__/' || duplicate_asset.asset_id::TEXT, lifecycle_state = 'deprecated', decommissioned_at = COALESCE(duplicate_asset.decommissioned_at, NOW()), metadata = COALESCE(duplicate_asset.metadata, '{}'::JSONB) || JSONB_BUILD_OBJECT( 'integrity_state', 'quarantined_duplicate', 'integrity_canonical_asset_id', repair.canonical_asset_id, 'integrity_repair', 'asset_inventory_canonical_integrity_2026-07-14' ), tags = CASE WHEN 'integrity:quarantined_duplicate' = ANY(duplicate_asset.tags) THEN duplicate_asset.tags ELSE ARRAY_APPEND( duplicate_asset.tags, 'integrity:quarantined_duplicate' ) END, updated_at = NOW() FROM asset_inventory_integrity_repair_map AS repair WHERE duplicate_asset.asset_id = repair.asset_id; -- Rebuild the unique index after every formerly unindexed duplicate has a -- distinct quarantine key. The outer CD transaction rolls back on any error. REINDEX INDEX asset_inventory_asset_key_key; DO $$ DECLARE duplicate_group_count BIGINT; repaired_row_count BIGINT; expected_repair_count BIGINT; index_healthy BOOLEAN; BEGIN SELECT COUNT(*) INTO duplicate_group_count FROM ( SELECT asset_key FROM asset_inventory GROUP BY asset_key HAVING COUNT(*) > 1 ) AS duplicate_groups; SELECT COUNT(*) INTO expected_repair_count FROM asset_inventory_integrity_repair_map; SELECT COUNT(*) INTO repaired_row_count FROM asset_inventory AS repaired JOIN asset_inventory_integrity_repair_map AS repair ON repair.asset_id = repaired.asset_id WHERE repaired.lifecycle_state = 'deprecated' AND repaired.asset_key = repair.original_asset_key || '/__integrity_quarantine__/' || repaired.asset_id::TEXT AND repaired.metadata ->> 'integrity_state' = 'quarantined_duplicate'; SELECT index_row.indisvalid AND index_row.indisready AND index_row.indislive AND index_row.indisunique INTO index_healthy FROM pg_index AS index_row WHERE index_row.indexrelid = 'asset_inventory_asset_key_key'::REGCLASS; IF duplicate_group_count <> 0 THEN RAISE EXCEPTION 'asset_inventory_duplicate_groups_remain: %', duplicate_group_count; END IF; IF repaired_row_count <> expected_repair_count THEN RAISE EXCEPTION 'asset_inventory_quarantine_verifier_failed: expected %, got %', expected_repair_count, repaired_row_count; END IF; IF index_healthy IS DISTINCT FROM TRUE THEN RAISE EXCEPTION 'asset_inventory_unique_index_not_healthy'; END IF; END $$; -- Exercise the exact conflict-update path that failed before the repair while -- preserving every application value. WITH probe AS ( SELECT asset_key, asset_type, host, namespace, name, metadata, tags, environment, lifecycle_state, first_seen_at, last_seen_at FROM asset_inventory ORDER BY asset_id LIMIT 1 ) INSERT INTO asset_inventory ( asset_key, asset_type, host, namespace, name, metadata, tags, environment, lifecycle_state, first_seen_at, last_seen_at ) SELECT asset_key, asset_type, host, namespace, name, metadata, tags, environment, lifecycle_state, first_seen_at, last_seen_at FROM probe ON CONFLICT (asset_key) DO UPDATE SET last_seen_at = asset_inventory.last_seen_at, updated_at = asset_inventory.updated_at; INSERT INTO automation_operation_log ( operation_type, actor, status, input, output, tags ) SELECT 'asset_discovered', 'gitea_cd_asset_integrity_reconciler', 'success', '{"work_item_id":"AIA-P0-006","migration":"asset_inventory_canonical_integrity_2026-07-14"}'::JSONB, JSONB_BUILD_OBJECT( 'duplicate_group_count_before', COUNT(DISTINCT original_asset_key), 'rows_quarantined', COUNT(*), 'duplicate_group_count_after', 0, 'index_rebuilt', TRUE, 'canonical_upsert_verified', TRUE, 'data_rows_deleted', 0 ), ARRAY[ 'asset_control_plane', 'integrity_repair', 'controlled_apply', 'post_verified', 'no_delete' ] FROM asset_inventory_integrity_repair_map; COMMENT ON INDEX asset_inventory_asset_key_key IS 'Canonical asset identity unique index; transactionally rebuilt and verified by Gitea CD.';