diff --git a/keyserver/src/creators/invite-link-creator.js b/keyserver/src/creators/invite-link-creator.js
--- a/keyserver/src/creators/invite-link-creator.js
+++ b/keyserver/src/creators/invite-link-creator.js
@@ -30,9 +30,9 @@
     threadPermissions.MANAGE_INVITE_LINKS,
   );
   const existingPrimaryLinksPromise = fetchPrimaryInviteLinks(viewer);
-  const fetchThreadInfoPromise = fetchServerThreadInfos(
-    SQL`t.id = ${request.communityID}`,
-  );
+  const fetchThreadInfoPromise = fetchServerThreadInfos({
+    threadID: request.communityID,
+  });
   const [hasPermission, existingPrimaryLinks, { threadInfos }] =
     await Promise.all([
       permissionPromise,
diff --git a/keyserver/src/creators/message-creator.js b/keyserver/src/creators/message-creator.js
--- a/keyserver/src/creators/message-creator.js
+++ b/keyserver/src/creators/message-creator.js
@@ -268,7 +268,7 @@
 
   if (updatedThreads.length > 0) {
     const [{ threadInfos: serverThreadInfos }] = await Promise.all([
-      fetchServerThreadInfos(SQL`t.id IN (${updatedThreads})`),
+      fetchServerThreadInfos({ threadIDs: new Set(updatedThreads) }),
       dbQuery(updateThreads),
       dbQuery(updateMemberships),
     ]);
diff --git a/keyserver/src/creators/update-creator.js b/keyserver/src/creators/update-creator.js
--- a/keyserver/src/creators/update-creator.js
+++ b/keyserver/src/creators/update-creator.js
@@ -440,10 +440,9 @@
   const promises = {};
 
   if (!viewerInfo.threadInfos && threadIDsNeedingFetch.size > 0) {
-    promises.threadResult = fetchThreadInfos(
-      viewer,
-      SQL`t.id IN (${[...threadIDsNeedingFetch]})`,
-    );
+    promises.threadResult = fetchThreadInfos(viewer, {
+      threadIDs: threadIDsNeedingFetch,
+    });
   }
 
   let calendarQuery: ?CalendarQuery = viewerInfo.calendarQuery
diff --git a/keyserver/src/deleters/thread-deleters.js b/keyserver/src/deleters/thread-deleters.js
--- a/keyserver/src/deleters/thread-deleters.js
+++ b/keyserver/src/deleters/thread-deleters.js
@@ -54,7 +54,7 @@
   const threadIDs = await fetchContainedThreadIDs(threadID);
 
   const [{ threadInfos: serverThreadInfos }] = await Promise.all([
-    fetchServerThreadInfos(SQL`t.id IN (${threadIDs})`),
+    fetchServerThreadInfos({ threadIDs: new Set(threadID) }),
     rescindPushNotifs(
       SQL`n.thread IN (${threadIDs})`,
       SQL`IF(m.thread IN (${threadIDs}), NULL, m.thread)`,
diff --git a/keyserver/src/fetchers/thread-fetchers.js b/keyserver/src/fetchers/thread-fetchers.js
--- a/keyserver/src/fetchers/thread-fetchers.js
+++ b/keyserver/src/fetchers/thread-fetchers.js
@@ -19,18 +19,53 @@
 import { ServerError } from 'lib/utils/errors.js';
 
 import { getUploadURL } from './upload-fetchers.js';
-import { dbQuery, SQL } from '../database/database.js';
+import { dbQuery, SQL, mergeAndConditions } from '../database/database.js';
 import type { SQLStatementType } from '../database/types.js';
 import type { Viewer } from '../session/viewer.js';
 
+type FetchThreadInfosFilter = $Shape<{
+  +threadID: string,
+  +threadIDs: $ReadOnlySet<string>,
+  +parentThreadID: string,
+  +sourceMessageID: string,
+}>;
+function constructWhereClause(
+  filter: FetchThreadInfosFilter,
+): SQLStatementType {
+  const conditions = [];
+
+  if (filter.threadID) {
+    conditions.push(SQL`t.id = ${filter.threadID}`);
+  }
+
+  if (filter.threadIDs) {
+    conditions.push(SQL`t.id IN (${[...filter.threadIDs]})`);
+  }
+
+  if (filter.parentThreadID) {
+    conditions.push(SQL`t.parent_thread_id = ${filter.parentThreadID}`);
+  }
+
+  if (filter.sourceMessageID) {
+    conditions.push(SQL`t.source_message = ${filter.sourceMessageID}`);
+  }
+
+  if (conditions.length === 0) {
+    return SQL``;
+  }
+
+  const clause = mergeAndConditions(conditions);
+  return SQL`WHERE `.append(clause);
+}
+
 type FetchServerThreadInfosResult = {
   +threadInfos: { +[id: string]: ServerThreadInfo },
 };
 
 async function fetchServerThreadInfos(
-  condition?: SQLStatementType,
+  filter?: FetchThreadInfosFilter,
 ): Promise<FetchServerThreadInfosResult> {
-  const whereClause = condition ? SQL`WHERE `.append(condition) : '';
+  const whereClause = filter ? constructWhereClause(filter) : '';
 
   const rolesQuery = SQL`
     SELECT t.id, t.default_role, r.id AS role, r.name, r.permissions
@@ -156,9 +191,9 @@
 
 async function fetchThreadInfos(
   viewer: Viewer,
-  condition?: SQLStatementType,
+  filter?: FetchThreadInfosFilter,
 ): Promise<FetchThreadInfosResult> {
-  const serverResult = await fetchServerThreadInfos(condition);
+  const serverResult = await fetchServerThreadInfos(filter);
   return rawThreadInfosFromServerThreadInfos(viewer, serverResult);
 }
 
@@ -230,9 +265,9 @@
   if (!parentThreadID) {
     return { containingThreadID: null, community: null, depth: 0 };
   }
-  const parentThreadInfos = await fetchServerThreadInfos(
-    SQL`t.id = ${parentThreadID}`,
-  );
+  const parentThreadInfos = await fetchServerThreadInfos({
+    threadID: parentThreadID,
+  });
   const parentThreadInfo = parentThreadInfos.threadInfos[parentThreadID];
   if (!parentThreadInfo) {
     throw new ServerError('invalid_parameters');
@@ -276,7 +311,7 @@
   message: RawMessageInfo | MessageInfo,
 ): Promise<?ServerThreadInfo> {
   const threadID = message.threadID;
-  const threads = await fetchServerThreadInfos(SQL`t.id = ${threadID}`);
+  const threads = await fetchServerThreadInfos({ threadID });
   return threads.threadInfos[threadID];
 }
 
diff --git a/keyserver/src/fetchers/thread-permission-fetchers.js b/keyserver/src/fetchers/thread-permission-fetchers.js
--- a/keyserver/src/fetchers/thread-permission-fetchers.js
+++ b/keyserver/src/fetchers/thread-permission-fetchers.js
@@ -153,7 +153,7 @@
   }
 
   const [{ threadInfos }, userInfos] = await Promise.all([
-    fetchThreadInfos(viewer, SQL`t.id IN (${[...threadIDs]})`),
+    fetchThreadInfos(viewer, { threadIDs: new Set(threadIDs) }),
     fetchKnownUserInfos(viewer),
   ]);
 
diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js
--- a/keyserver/src/push/send.js
+++ b/keyserver/src/push/send.js
@@ -495,9 +495,7 @@
 
   const promises = {};
   // These threadInfos won't have currentUser set
-  promises.threadResult = fetchServerThreadInfos(
-    SQL`t.id IN (${[...threadIDs]})`,
-  );
+  promises.threadResult = fetchServerThreadInfos({ threadIDs });
   if (threadWithChangedNamesToMessages.size > 0) {
     const typesThatAffectName = [
       messageTypes.CHANGE_SETTINGS,
diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js
--- a/keyserver/src/responders/message-responders.js
+++ b/keyserver/src/responders/message-responders.js
@@ -43,7 +43,6 @@
 } from 'lib/utils/validation-utils.js';
 
 import createMessages from '../creators/message-creator.js';
-import { SQL } from '../database/database.js';
 import {
   fetchMessageInfos,
   fetchMessageInfoForLocalID,
@@ -303,7 +302,7 @@
 
   const [serverThreadInfos, hasPermission, targetMessageUserInfos] =
     await Promise.all([
-      fetchServerThreadInfos(SQL`t.id = ${threadID}`),
+      fetchServerThreadInfos({ threadID }),
       checkThreadPermission(
         viewer,
         threadID,
@@ -393,11 +392,12 @@
 
   const [serverThreadInfos, hasPermission, rawSidebarThreadInfos] =
     await Promise.all([
-      fetchServerThreadInfos(SQL`t.id = ${threadID}`),
+      fetchServerThreadInfos({ threadID }),
       checkThreadPermission(viewer, threadID, threadPermissions.EDIT_MESSAGE),
-      fetchServerThreadInfos(
-        SQL`t.parent_thread_id = ${threadID} AND t.source_message = ${targetMessageID}`,
-      ),
+      fetchServerThreadInfos({
+        parentThreadID: threadID,
+        sourceMessageID: targetMessageID,
+      }),
     ]);
 
   const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID];
diff --git a/keyserver/src/scripts/add-thread-ancestry.js b/keyserver/src/scripts/add-thread-ancestry.js
deleted file mode 100644
--- a/keyserver/src/scripts/add-thread-ancestry.js
+++ /dev/null
@@ -1,84 +0,0 @@
-// @flow
-
-import {
-  getContainingThreadID,
-  getCommunity,
-} from 'lib/shared/thread-utils.js';
-import type { ServerThreadInfo } from 'lib/types/thread-types.js';
-
-import { main } from './utils.js';
-import { dbQuery, SQL } from '../database/database.js';
-import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
-
-async function addColumnAndIndexes() {
-  await dbQuery(SQL`
-    ALTER TABLE threads
-      ADD containing_thread_id BIGINT(20) NULL AFTER parent_thread_id,
-      ADD community BIGINT(20) NULL AFTER containing_thread_id,
-      ADD depth INT UNSIGNED NOT NULL DEFAULT 0 AFTER community,
-      ADD INDEX parent_thread_id (parent_thread_id),
-      ADD INDEX community (community),
-      ADD INDEX containing_thread_id (containing_thread_id);
-  `);
-}
-
-async function setColumn() {
-  const stack = [[null, SQL`t.parent_thread_id IS NULL`]];
-
-  while (stack.length > 0) {
-    const [parentThreadInfo, predicate] = stack.shift();
-    const { threadInfos } = await fetchServerThreadInfos(predicate);
-    const updatedThreadInfos = await setColumnForLayer(
-      parentThreadInfo,
-      threadInfos,
-    );
-    for (const threadInfo of updatedThreadInfos) {
-      stack.push([threadInfo, SQL`t.parent_thread_id = ${threadInfo.id}`]);
-    }
-  }
-}
-
-async function setColumnForLayer(
-  parentThreadInfo: ?ServerThreadInfo,
-  threadInfos: { +[id: string]: ServerThreadInfo },
-): Promise<ServerThreadInfo[]> {
-  const updatedThreadInfos = [];
-  for (const threadID in threadInfos) {
-    const threadInfo = threadInfos[threadID];
-    const containingThreadID = getContainingThreadID(
-      parentThreadInfo,
-      threadInfo.type,
-    );
-    const community = getCommunity(parentThreadInfo);
-    if (!containingThreadID && !community) {
-      console.log(
-        `containingThreadID and community are null for ${threadID}, ` +
-          'skipping...',
-      );
-      updatedThreadInfos.push(threadInfo);
-      continue;
-    }
-    const depth = parentThreadInfo ? parentThreadInfo.depth + 1 : 0;
-    console.log(
-      `setting containingThreadID to ${containingThreadID ?? 'null'}, ` +
-        `community to ${community ?? 'null'}, and ` +
-        `depth to ${depth} for ${threadID}`,
-    );
-    await dbQuery(SQL`
-      UPDATE threads
-      SET containing_thread_id = ${containingThreadID},
-        community = ${community},
-        depth = ${depth}
-      WHERE id = ${threadID}
-    `);
-    updatedThreadInfos.push({
-      ...threadInfo,
-      containingThreadID,
-      community,
-      depth,
-    });
-  }
-  return updatedThreadInfos;
-}
-
-main([addColumnAndIndexes, setColumn]);
diff --git a/keyserver/src/scripts/soft-launch-migration.js b/keyserver/src/scripts/soft-launch-migration.js
deleted file mode 100644
--- a/keyserver/src/scripts/soft-launch-migration.js
+++ /dev/null
@@ -1,430 +0,0 @@
-// @flow
-
-import invariant from 'invariant';
-
-import ashoat from 'lib/facts/ashoat.js';
-import bots from 'lib/facts/bots.js';
-import genesis from 'lib/facts/genesis.js';
-import testers from 'lib/facts/testers.js';
-import { messageTypes } from 'lib/types/message-types-enum.js';
-import { threadTypes, type ThreadType } from 'lib/types/thread-types-enum.js';
-
-import { main } from './utils.js';
-import createMessages from '../creators/message-creator.js';
-import { createThread } from '../creators/thread-creator.js';
-import { dbQuery, SQL } from '../database/database.js';
-import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
-import { fetchAllUserIDs } from '../fetchers/user-fetchers.js';
-import { createScriptViewer } from '../session/scripts.js';
-import type { Viewer } from '../session/viewer.js';
-import {
-  recalculateThreadPermissions,
-  commitMembershipChangeset,
-  saveMemberships,
-} from '../updaters/thread-permission-updaters.js';
-import { updateThread } from '../updaters/thread-updaters.js';
-
-const batchSize = 10;
-const createThreadOptions = { forceAddMembers: true };
-const updateThreadOptions = {
-  forceUpdateRoot: true,
-  silenceMessages: true,
-  ignorePermissions: true,
-};
-const convertUnadminnedToCommunities = ['311733', '421638'];
-const convertToAnnouncementCommunities = ['375310'];
-const convertToAnnouncementSubthreads = ['82649'];
-const threadsWithMissingParent = ['534395'];
-const personalThreadsWithMissingMembers = [
-  '82161',
-  '103111',
-  '210609',
-  '227049',
-];
-const excludeFromTestersThread = new Set([
-  '1402',
-  '39227',
-  '156159',
-  '526973',
-  '740732',
-]);
-
-async function createGenesisCommunity() {
-  const genesisThreadInfos = await fetchServerThreadInfos(
-    SQL`t.id = ${genesis.id}`,
-  );
-  const genesisThreadInfo = genesisThreadInfos.threadInfos[genesis.id];
-  if (genesisThreadInfo && genesisThreadInfo.type === threadTypes.GENESIS) {
-    return;
-  } else if (genesisThreadInfo) {
-    await updateGenesisCommunityType();
-    return;
-  }
-
-  console.log('creating GENESIS community');
-
-  const idInsertQuery = SQL`
-    INSERT INTO ids(id, table_name)
-    VALUES ${[[genesis.id, 'threads']]}
-  `;
-  await dbQuery(idInsertQuery);
-
-  const ashoatViewer = createScriptViewer(ashoat.id);
-  const allUserIDs = await fetchAllUserIDs();
-  const nonAshoatUserIDs = allUserIDs.filter(id => id !== ashoat.id);
-
-  await createThread(
-    ashoatViewer,
-    {
-      id: genesis.id,
-      type: threadTypes.GENESIS,
-      name: genesis.name,
-      description: genesis.description,
-      initialMemberIDs: nonAshoatUserIDs,
-    },
-    createThreadOptions,
-  );
-
-  await createMessages(
-    ashoatViewer,
-    genesis.introMessages.map(message => ({
-      type: messageTypes.TEXT,
-      threadID: genesis.id,
-      creatorID: ashoat.id,
-      time: Date.now(),
-      text: message,
-    })),
-  );
-
-  console.log('creating testers thread');
-
-  const testerUserIDs = nonAshoatUserIDs.filter(
-    userID => !excludeFromTestersThread.has(userID),
-  );
-  const { newThreadID } = await createThread(
-    ashoatViewer,
-    {
-      type: threadTypes.COMMUNITY_SECRET_SUBTHREAD,
-      name: testers.name,
-      description: testers.description,
-      initialMemberIDs: testerUserIDs,
-    },
-    createThreadOptions,
-  );
-  invariant(
-    newThreadID,
-    'newThreadID for tester thread creation should be set',
-  );
-
-  await createMessages(
-    ashoatViewer,
-    testers.introMessages.map(message => ({
-      type: messageTypes.TEXT,
-      threadID: newThreadID,
-      creatorID: ashoat.id,
-      time: Date.now(),
-      text: message,
-    })),
-  );
-}
-
-async function updateGenesisCommunityType() {
-  console.log('updating GENESIS community to GENESIS type');
-
-  const ashoatViewer = createScriptViewer(ashoat.id);
-  await updateThread(
-    ashoatViewer,
-    {
-      threadID: genesis.id,
-      changes: {
-        type: threadTypes.GENESIS,
-      },
-    },
-    updateThreadOptions,
-  );
-}
-
-async function convertExistingCommunities() {
-  const communityQuery = SQL`
-    SELECT t.id, t.name
-    FROM threads t
-    LEFT JOIN roles r ON r.thread = t.id
-    LEFT JOIN memberships m ON m.thread = t.id
-    WHERE t.type = ${threadTypes.COMMUNITY_SECRET_SUBTHREAD}
-      AND t.parent_thread_id IS NULL
-    GROUP BY t.id
-    HAVING COUNT(DISTINCT r.id) > 1 AND COUNT(DISTINCT m.user) > 2
-  `;
-  const [convertToCommunity] = await dbQuery(communityQuery);
-
-  const botViewer = createScriptViewer(bots.commbot.userID);
-  await convertThreads(
-    botViewer,
-    convertToCommunity,
-    threadTypes.COMMUNITY_ROOT,
-  );
-}
-
-async function convertThreads(
-  viewer: Viewer,
-  threads: Array<{ +id: number, +name: string }>,
-  type: ThreadType,
-) {
-  while (threads.length > 0) {
-    const batch = threads.splice(0, batchSize);
-    await Promise.all(
-      batch.map(async thread => {
-        console.log(`converting ${JSON.stringify(thread)} to ${type}`);
-        return await updateThread(
-          viewer,
-          {
-            threadID: thread.id.toString(),
-            changes: { type },
-          },
-          updateThreadOptions,
-        );
-      }),
-    );
-  }
-}
-
-async function convertUnadminnedCommunities() {
-  const communityQuery = SQL`
-    SELECT id, name
-    FROM threads
-    WHERE id IN (${convertUnadminnedToCommunities}) AND
-      type = ${threadTypes.COMMUNITY_SECRET_SUBTHREAD}
-  `;
-  const [convertToCommunity] = await dbQuery(communityQuery);
-
-  // We use ashoat here to make sure he becomes the admin of these communities
-  const ashoatViewer = createScriptViewer(ashoat.id);
-  await convertThreads(
-    ashoatViewer,
-    convertToCommunity,
-    threadTypes.COMMUNITY_ROOT,
-  );
-}
-
-async function convertAnnouncementCommunities() {
-  const announcementCommunityQuery = SQL`
-    SELECT id, name
-    FROM threads
-    WHERE id IN (${convertToAnnouncementCommunities}) AND
-      type != ${threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT}
-  `;
-  const [convertToAnnouncementCommunity] = await dbQuery(
-    announcementCommunityQuery,
-  );
-
-  const botViewer = createScriptViewer(bots.commbot.userID);
-  await convertThreads(
-    botViewer,
-    convertToAnnouncementCommunity,
-    threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT,
-  );
-}
-
-async function convertAnnouncementSubthreads() {
-  const announcementSubthreadQuery = SQL`
-    SELECT id, name
-    FROM threads
-    WHERE id IN (${convertToAnnouncementSubthreads}) AND
-      type != ${threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD}
-  `;
-  const [convertToAnnouncementSubthread] = await dbQuery(
-    announcementSubthreadQuery,
-  );
-
-  const botViewer = createScriptViewer(bots.commbot.userID);
-  await convertThreads(
-    botViewer,
-    convertToAnnouncementSubthread,
-    threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD,
-  );
-}
-
-async function fixThreadsWithMissingParent() {
-  const threadsWithMissingParentQuery = SQL`
-    SELECT id, name
-    FROM threads
-    WHERE id IN (${threadsWithMissingParent}) AND
-      type != ${threadTypes.COMMUNITY_SECRET_SUBTHREAD}
-  `;
-  const [threadsWithMissingParentResult] = await dbQuery(
-    threadsWithMissingParentQuery,
-  );
-
-  const botViewer = createScriptViewer(bots.commbot.userID);
-  while (threadsWithMissingParentResult.length > 0) {
-    const batch = threadsWithMissingParentResult.splice(0, batchSize);
-    await Promise.all(
-      batch.map(async thread => {
-        console.log(`fixing ${JSON.stringify(thread)} with missing parent`);
-        return await updateThread(
-          botViewer,
-          {
-            threadID: thread.id.toString(),
-            changes: {
-              parentThreadID: null,
-              type: threadTypes.COMMUNITY_SECRET_SUBTHREAD,
-            },
-          },
-          updateThreadOptions,
-        );
-      }),
-    );
-  }
-}
-
-async function fixPersonalThreadsWithMissingMembers() {
-  const missingMembersQuery = SQL`
-    SELECT thread, user
-    FROM memberships
-    WHERE thread IN (${personalThreadsWithMissingMembers}) AND role <= 0
-  `;
-  const [missingMembers] = await dbQuery(missingMembersQuery);
-
-  const botViewer = createScriptViewer(bots.commbot.userID);
-  for (const row of missingMembers) {
-    console.log(`fixing ${JSON.stringify(row)} with missing member`);
-    await updateThread(
-      botViewer,
-      {
-        threadID: row.thread.toString(),
-        changes: {
-          newMemberIDs: [row.user.toString()],
-        },
-      },
-      updateThreadOptions,
-    );
-  }
-}
-
-async function moveThreadsToGenesis() {
-  const noParentQuery = SQL`
-    SELECT id, name
-    FROM threads
-    WHERE type != ${threadTypes.COMMUNITY_ROOT}
-      AND type != ${threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT}
-      AND type != ${threadTypes.GENESIS}
-      AND parent_thread_id IS NULL
-  `;
-  const [noParentThreads] = await dbQuery(noParentQuery);
-
-  const botViewer = createScriptViewer(bots.commbot.userID);
-  while (noParentThreads.length > 0) {
-    const batch = noParentThreads.splice(0, batchSize);
-    await Promise.all(
-      batch.map(async thread => {
-        console.log(`processing ${JSON.stringify(thread)}`);
-        return await updateThread(
-          botViewer,
-          {
-            threadID: thread.id.toString(),
-            changes: {
-              parentThreadID: genesis.id,
-            },
-          },
-          updateThreadOptions,
-        );
-      }),
-    );
-  }
-
-  const childQuery = SQL`
-    SELECT id, name
-    FROM threads
-    WHERE type != ${threadTypes.COMMUNITY_ROOT}
-      AND type != ${threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT}
-      AND type != ${threadTypes.GENESIS}
-      AND parent_thread_id IS NOT NULL
-      AND parent_thread_id != ${genesis.id}
-  `;
-  const [childThreads] = await dbQuery(childQuery);
-
-  for (const childThread of childThreads) {
-    // We go one by one because the changes in a parent thread can affect a
-    // child thread. If the child thread update starts at the same time as an
-    // update for its parent thread, a race can cause incorrect results for the
-    // child thread (in particular for the permissions on the memberships table)
-    console.log(`processing ${JSON.stringify(childThread)}`);
-    await updateThread(
-      botViewer,
-      {
-        threadID: childThread.id.toString(),
-        changes: {},
-      },
-      updateThreadOptions,
-    );
-  }
-}
-
-async function clearMembershipPermissions() {
-  const membershipPermissionQuery = SQL`
-    SELECT DISTINCT thread
-    FROM memberships
-    WHERE JSON_EXTRACT(permissions, '$.membership') IS NOT NULL
-  `;
-  const [membershipPermissionResult] = await dbQuery(membershipPermissionQuery);
-  if (membershipPermissionResult.length === 0) {
-    return;
-  }
-
-  const botViewer = createScriptViewer(bots.commbot.userID);
-  for (const row of membershipPermissionResult) {
-    const threadID = row.thread.toString();
-    console.log(`clearing membership permissions for ${threadID}`);
-    const changeset = await recalculateThreadPermissions(threadID);
-    await commitMembershipChangeset(botViewer, changeset);
-  }
-
-  console.log('clearing -1 rows...');
-  const emptyMembershipDeletionQuery = SQL`
-    DELETE FROM memberships
-    WHERE role = -1 AND permissions IS NULL
-  `;
-  await dbQuery(emptyMembershipDeletionQuery);
-
-  await createMembershipsForFormerMembers();
-}
-
-async function createMembershipsForFormerMembers() {
-  const [result] = await dbQuery(SQL`
-    SELECT DISTINCT thread, user
-    FROM messages m
-    WHERE NOT EXISTS (
-      SELECT thread, user FROM memberships mm
-      WHERE m.thread = mm.thread AND m.user = mm.user
-    )
-  `);
-
-  const rowsToSave = [];
-  for (const row of result) {
-    rowsToSave.push({
-      operation: 'save',
-      userID: row.user.toString(),
-      threadID: row.thread.toString(),
-      userNeedsFullThreadDetails: false,
-      intent: 'none',
-      permissions: null,
-      permissionsForChildren: null,
-      role: '-1',
-      oldRole: '-1',
-    });
-  }
-
-  await saveMemberships(rowsToSave);
-}
-
-main([
-  createGenesisCommunity,
-  convertExistingCommunities,
-  convertUnadminnedCommunities,
-  convertAnnouncementCommunities,
-  convertAnnouncementSubthreads,
-  fixThreadsWithMissingParent,
-  fixPersonalThreadsWithMissingMembers,
-  moveThreadsToGenesis,
-  clearMembershipPermissions,
-]);
diff --git a/keyserver/src/socket/session-utils.js b/keyserver/src/socket/session-utils.js
--- a/keyserver/src/socket/session-utils.js
+++ b/keyserver/src/socket/session-utils.js
@@ -41,7 +41,6 @@
 import { createOlmSession } from '../creators/olm-session-creator.js';
 import { saveOneTimeKeys } from '../creators/one-time-keys-creator.js';
 import createReport from '../creators/report-creator.js';
-import { SQL } from '../database/database.js';
 import {
   fetchEntryInfos,
   fetchEntryInfosByID,
@@ -424,7 +423,7 @@
     fetchAllEntries = false,
     fetchAllUserInfos = false,
     fetchUserInfo = false;
-  const threadIDsToFetch = [],
+  const threadIDsToFetch = new Set(),
     entryIDsToFetch = [],
     userIDsToFetch = [];
   for (const key of invalidKeys) {
@@ -438,7 +437,7 @@
       fetchUserInfo = true;
     } else if (key.startsWith('threadInfo|')) {
       const [, threadID] = key.split('|');
-      threadIDsToFetch.push(threadID);
+      threadIDsToFetch.add(threadID);
     } else if (key.startsWith('entryInfo|')) {
       const [, entryID] = key.split('|');
       entryIDsToFetch.push(entryID);
@@ -451,11 +450,10 @@
   const fetchPromises = {};
   if (fetchAllThreads) {
     fetchPromises.threadsResult = fetchThreadInfos(viewer);
-  } else if (threadIDsToFetch.length > 0) {
-    fetchPromises.threadsResult = fetchThreadInfos(
-      viewer,
-      SQL`t.id IN (${threadIDsToFetch})`,
-    );
+  } else if (threadIDsToFetch.size > 0) {
+    fetchPromises.threadsResult = fetchThreadInfos(viewer, {
+      threadIDs: threadIDsToFetch,
+    });
   }
   if (fetchAllEntries) {
     fetchPromises.entriesResult = fetchEntryInfos(viewer, [calendarQuery]);
diff --git a/keyserver/src/updaters/thread-permission-updaters.js b/keyserver/src/updaters/thread-permission-updaters.js
--- a/keyserver/src/updaters/thread-permission-updaters.js
+++ b/keyserver/src/updaters/thread-permission-updaters.js
@@ -1052,9 +1052,9 @@
     rescindPushNotifsForMemberDeletion(toRescindPushNotifs),
   ]);
 
-  const serverThreadInfoFetchResult = await fetchServerThreadInfos(
-    SQL`t.id IN (${[...changedThreadIDs]})`,
-  );
+  const serverThreadInfoFetchResult = await fetchServerThreadInfos({
+    threadIDs: changedThreadIDs,
+  });
   const { threadInfos: serverThreadInfos } = serverThreadInfoFetchResult;
 
   const time = Date.now();
diff --git a/keyserver/src/updaters/thread-updaters.js b/keyserver/src/updaters/thread-updaters.js
--- a/keyserver/src/updaters/thread-updaters.js
+++ b/keyserver/src/updaters/thread-updaters.js
@@ -77,7 +77,7 @@
       request.threadID,
       threadPermissions.CHANGE_ROLE,
     ),
-    fetchThreadInfos(viewer, SQL`t.id = ${request.threadID}`),
+    fetchThreadInfos(viewer, { threadID: request.threadID }),
   ]);
   if (memberIDs.length === 0) {
     throw new ServerError('invalid_parameters');
@@ -237,7 +237,7 @@
   }
 
   const [fetchThreadResult, hasPermission] = await Promise.all([
-    fetchThreadInfos(viewer, SQL`t.id = ${request.threadID}`),
+    fetchThreadInfos(viewer, { threadID: request.threadID }),
     checkThreadPermission(
       viewer,
       request.threadID,
@@ -376,9 +376,9 @@
     throw new ServerError('invalid_parameters');
   }
 
-  validationPromises.serverThreadInfos = fetchServerThreadInfos(
-    SQL`t.id = ${request.threadID}`,
-  );
+  validationPromises.serverThreadInfos = fetchServerThreadInfos({
+    threadID: request.threadID,
+  });
 
   validationPromises.hasNecessaryPermissions = (async () => {
     if (ignorePermissions) {
@@ -895,7 +895,7 @@
   }
 
   const [{ threadInfos: serverThreadInfos }] = await Promise.all([
-    fetchServerThreadInfos(SQL`t.id = ${threadID}`),
+    fetchServerThreadInfos({ threadID }),
     dbQuery(togglePinQuery),
     dbQuery(updateThreadQuery),
   ]);
diff --git a/keyserver/src/updaters/user-subscription-updaters.js b/keyserver/src/updaters/user-subscription-updaters.js
--- a/keyserver/src/updaters/user-subscription-updaters.js
+++ b/keyserver/src/updaters/user-subscription-updaters.js
@@ -21,10 +21,9 @@
     throw new ServerError('not_logged_in');
   }
 
-  const { threadInfos } = await fetchThreadInfos(
-    viewer,
-    SQL`t.id = ${update.threadID}`,
-  );
+  const { threadInfos } = await fetchThreadInfos(viewer, {
+    threadID: update.threadID,
+  });
   const threadInfo = threadInfos[update.threadID];
   if (!viewerIsMember(threadInfo)) {
     throw new ServerError('not_member');