create type group_role_v2 as enum ('MEMBER', 'GROUP_ADMIN', 'GROUP_OWNER'); alter table group_members alter column role type group_role_v2 using role::text::group_role_v2; drop type group_role; alter type group_role_v2 rename to group_role; with ranked_admins as ( select group_id, user_id, row_number() over (partition by group_id order by created_at asc, user_id asc) as rn from group_members where role = 'GROUP_ADMIN' ) update group_members gm set role = 'GROUP_OWNER' from ranked_admins ra where gm.group_id = ra.group_id and gm.user_id = ra.user_id and ra.rn = 1; create unique index group_members_owner_unique on group_members(group_id) where role = 'GROUP_OWNER'; alter table users add column last_login_at timestamptz; alter table group_members add column last_active_at timestamptz; create type group_join_policy as enum ('NOT_ACCEPTING', 'AUTO_ACCEPT', 'APPROVAL_REQUIRED'); alter table group_settings add column join_policy group_join_policy not null default 'NOT_ACCEPTING'; create type group_join_request_status as enum ('PENDING', 'APPROVED', 'DENIED', 'CANCELED'); create table group_join_requests( id bigserial primary key, group_id bigint not null references groups(id) on delete cascade, user_id bigint not null references users(id) on delete cascade, status group_join_request_status not null default 'PENDING', decided_by bigint references users(id) on delete set null, decided_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create unique index group_join_requests_pending_unique on group_join_requests(group_id, user_id) where status = 'PENDING'; create index group_join_requests_group_idx on group_join_requests(group_id); create table group_invite_links( id bigserial primary key, group_id bigint not null references groups(id) on delete cascade, created_by bigint not null references users(id) on delete cascade, token text not null unique, policy group_join_policy not null, single_use boolean not null default false, expires_at timestamptz not null, used_at timestamptz, revoked_at timestamptz, created_at timestamptz not null default now() ); create index group_invite_links_group_idx on group_invite_links(group_id); create table group_audit_log( id bigserial primary key, group_id bigint not null references groups(id) on delete cascade, actor_user_id bigint references users(id) on delete set null, actor_role group_role, event_type text not null, request_id text not null, ip inet, user_agent text, success boolean not null default true, error_code text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); create index group_audit_log_group_idx on group_audit_log(group_id, created_at desc); create index group_audit_log_request_idx on group_audit_log(request_id);