Files
awoooi/apps/api/migrations/asset_inventory_canonical_integrity_2026-07-14.sql
ogt 4508d19d4b
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m32s
CD Pipeline / build-and-deploy (push) Successful in 7m58s
CD Pipeline / post-deploy-checks (push) Successful in 2m25s
fix(security): repair canonical asset integrity
2026-07-14 11:54:02 +08:00

216 lines
5.8 KiB
SQL

-- 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.';