-- Step 1: Create room_access table (private room explicit access grants) CREATE TABLE IF NOT EXISTS room_access ( room UUID NOT NULL, "user" UUID NOT NULL, granted_by UUID NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (room, "user") ); CREATE INDEX IF NOT EXISTS idx_room_access_user ON room_access ("user"); -- Step 2: Create room_user_state table (per-user per-room state) CREATE TABLE IF NOT EXISTS room_user_state ( room UUID NOT NULL, "user" UUID NOT NULL, last_read_seq BIGINT, do_not_disturb BOOLEAN NOT NULL DEFAULT FALSE, dnd_start_hour SMALLINT, dnd_end_hour SMALLINT, joined_at TIMESTAMPTZ, PRIMARY KEY (room, "user") ); CREATE INDEX IF NOT EXISTS idx_room_user_state_user ON room_user_state ("user"); -- Step 3: Migrate data from room_member to room_access and room_user_state -- For private rooms (public=false), migrate member rows → room_access INSERT INTO room_access (room, "user", granted_by, granted_at) SELECT rm.room, rm."user", rm."user", COALESCE(rm.joined_at, NOW()) FROM room_member rm JOIN room r ON r.id = rm.room WHERE r.public = false; -- Migrate all room_member rows → room_user_state (state is always useful) INSERT INTO room_user_state (room, "user", last_read_seq, do_not_disturb, dnd_start_hour, dnd_end_hour, joined_at) SELECT rm.room, rm."user", rm.last_read_seq, rm.do_not_disturb, rm.dnd_start_hour, rm.dnd_end_hour, rm.joined_at FROM room_member rm ON CONFLICT (room, "user") DO NOTHING; -- Step 4: Drop room_member table DROP TABLE IF EXISTS room_member; -- Step 5: Convert sender_type enum values in room_message -- Old values: member, admin, owner, guest → new value: user -- AI/system/tool/webhook stay the same (webhook is new) UPDATE room_message SET sender_type = 'user' WHERE sender_type IN ('member', 'admin', 'owner', 'guest');