-- Migration: Shared Vehicle Config (car-config-centric architecture) -- Converts per-user vehicle records to shared vehicle configs with junction table. -- IMPORTANT: Take a DB backup before running. Execute inside a transaction. BEGIN; -- 1. Create user_vehicles junction table CREATE TABLE user_vehicles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, vehicle_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 2. For each VIN, pick the canonical record (most recently updated) -- and migrate all userId-vehicleId pairs into the junction table INSERT INTO user_vehicles (user_id, vehicle_id, created_at, last_accessed_at) SELECT v.user_id, canonical.id, v.created_at, v.updated_at FROM vehicles v JOIN ( SELECT DISTINCT ON (vin) id, vin FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST ) canonical ON canonical.vin = v.vin; -- 3. Delete categories on NON-canonical vehicles that already exist on the -- canonical vehicle (same name+source), so the UPDATE in step 4 won't -- hit the unique constraint. WITH canonical AS ( SELECT DISTINCT ON (vin) id AS cid, vin FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST ) DELETE FROM categories WHERE id IN ( SELECT c_dup.id FROM categories c_dup JOIN vehicles v ON c_dup.vehicle_id = v.id JOIN canonical can ON can.vin = v.vin WHERE v.id != can.cid AND EXISTS ( SELECT 1 FROM categories c_can WHERE c_can.vehicle_id = can.cid AND c_can.name = c_dup.name AND c_can.source = c_dup.source ) ); -- 4. Move remaining categories from non-canonical vehicles to canonical WITH canonical AS ( SELECT DISTINCT ON (vin) id AS cid, vin FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST ) UPDATE categories c SET vehicle_id = can.cid FROM vehicles v JOIN canonical can ON can.vin = v.vin WHERE c.vehicle_id = v.id AND v.id != can.cid; -- 5. Delete parts on non-canonical vehicles that would conflict after move -- (parts don't have a unique constraint, but let's keep data clean by -- removing duplicates — same oemCode+categoryId on canonical vehicle) WITH canonical AS ( SELECT DISTINCT ON (vin) id AS cid, vin FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST ) DELETE FROM parts WHERE id IN ( SELECT p_dup.id FROM parts p_dup JOIN vehicles v ON p_dup.vehicle_id = v.id JOIN canonical can ON can.vin = v.vin WHERE v.id != can.cid AND EXISTS ( SELECT 1 FROM parts p_can WHERE p_can.vehicle_id = can.cid AND p_can.category_id = p_dup.category_id AND p_can.oem_code = p_dup.oem_code ) ); -- 6. Move remaining parts from non-canonical vehicles to canonical WITH canonical AS ( SELECT DISTINCT ON (vin) id AS cid, vin FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST ) UPDATE parts p SET vehicle_id = can.cid FROM vehicles v JOIN canonical can ON can.vin = v.vin WHERE p.vehicle_id = v.id AND v.id != can.cid; -- 7. Delete non-canonical vehicle records (duplicates) DELETE FROM vehicles WHERE id NOT IN ( SELECT DISTINCT ON (vin) id FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST ); -- 8. Drop old user-specific indexes and column DROP INDEX IF EXISTS vehicles_user_vin_idx; DROP INDEX IF EXISTS vehicles_user_id_idx; ALTER TABLE vehicles DROP COLUMN user_id; -- 9. Add unique index on VIN (one record per VIN globally) CREATE UNIQUE INDEX vehicles_vin_unique_idx ON vehicles (vin); -- 10. Add FK and indexes on junction table ALTER TABLE user_vehicles ADD CONSTRAINT user_vehicles_vehicle_id_fkey FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE; CREATE UNIQUE INDEX user_vehicles_user_vehicle_idx ON user_vehicles (user_id, vehicle_id); CREATE INDEX user_vehicles_user_id_idx ON user_vehicles (user_id); -- 11. Drop unused vehicle_categories table DROP TABLE IF EXISTS vehicle_categories; -- Also drop the old vin-only index if it exists (replaced by unique) DROP INDEX IF EXISTS vehicles_vin_idx; COMMIT;