-- Tenant/user hierarchy alignment. Additive and safe to run repeatedly. -- New business flows use tenant_members, platform_role, tenant_id and -- created_by. Legacy role/project fields remain for compatibility only. ALTER TABLE users ADD COLUMN IF NOT EXISTS platform_role TEXT NOT NULL DEFAULT 'platform_user'; UPDATE users SET platform_role = CASE WHEN role = 'admin' OR COALESCE(protected, 0) = 1 THEN 'platform_admin' ELSE 'platform_user' END WHERE platform_role IS NULL OR platform_role NOT IN ('platform_admin', 'platform_user'); CREATE INDEX IF NOT EXISTS idx_users_platform_role ON users(platform_role, status, deleted_at); ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default'; ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS deleted_by TEXT; UPDATE fine_tune_tasks SET tenant_id = COALESCE(NULLIF(tenant_id, ''), payload::json->>'tenant_id', 'default'), created_by = COALESCE(NULLIF(created_by, ''), payload::json->>'created_by') WHERE tenant_id IS NULL OR tenant_id = '' OR created_by IS NULL OR created_by = ''; CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_tenant_active ON fine_tune_tasks(tenant_id, status, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_creator ON fine_tune_tasks(created_by, deleted_at, create_time DESC); INSERT INTO tenants (id, name, code, status, quota, create_time) VALUES ('default', '默认租户', 'default', 'active', '{}', NOW()::text) ON CONFLICT (id) DO UPDATE SET status = CASE WHEN COALESCE(tenants.status, 'active') = 'deleted' THEN 'active' ELSE tenants.status END, deleted_at = CASE WHEN COALESCE(tenants.status, 'active') = 'deleted' THEN NULL ELSE tenants.deleted_at END, deleted_by = CASE WHEN COALESCE(tenants.status, 'active') = 'deleted' THEN NULL ELSE tenants.deleted_by END; -- Make the tenant owner relationship explicit and repair older tenant rows. INSERT INTO tenant_members (tenant_id, user_id, role, status, joined_at) SELECT t.id, t.owner_user_id, 'owner', 'active', COALESCE(t.create_time, NOW()::text) FROM tenants t JOIN users u ON u.id = t.owner_user_id WHERE t.owner_user_id IS NOT NULL AND COALESCE(t.status, 'active') = 'active' ON CONFLICT (tenant_id, user_id) DO UPDATE SET role = 'owner', status = 'active'; -- Existing account creation used admin as a tenant owner. Platform role and -- tenant role are separate, so keep only explicit tenant owners as owners. UPDATE tenant_members tm SET role = 'member' FROM users u WHERE tm.user_id = u.id AND tm.role = 'owner' AND (u.role <> 'admin' AND COALESCE(u.protected, 0) = 0) AND NOT EXISTS ( SELECT 1 FROM tenants t WHERE t.id = tm.tenant_id AND t.owner_user_id = tm.user_id ); -- Project is no longer a business isolation boundary. Keep historical columns -- readable, but make tenant-scoped queries the only supported new path. CREATE INDEX IF NOT EXISTS idx_datasets_tenant_active ON datasets(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_models_tenant_active ON models(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_trained_models_tenant_active ON trained_models(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_eval_tasks_tenant_active ON eval_tasks(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_compare_tasks_tenant_active ON compare_tasks(tenant_id, deleted_at, create_time DESC);