perf(db): composite index (account_id, name) + hide archived groups
Two related follow-ups for the 3 000+ groups-per-account scale path: 1. New B-tree index on whatsapp_groups (account_id, name) (migration 0014). Covers the groups list page's `WHERE account_id=? ORDER BY name ASC LIMIT 200` query so PG streams pre-sorted from the index instead of pulling all rows then sorting. The unique (account_id, wa_group_jid) was the only prior B-tree on this table; it backed the WHERE prefix but not the ORDER BY. 2. listGroupsForAccount now filters `is_archived = false` in both the search and the no-search branch. Soft-archived groups (set when group-sync sees them disappear from the live participant list, or when an operator unpairs the account) used to leak into the wizard picker, letting operators pick a group the bot can no longer reach. Archived rows still exist in DB so reminders that target them keep working; a re-pair flips them back via the on-conflict upsert. README "Deferred" entry for the composite index removed (it's shipped). Search-as-you-type in the wizard picker stays deferred. 482 web + 88 bot tests still green; typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c906a9fa3a
commit
ea7d07b2c8
@ -200,9 +200,5 @@ Set `NO_SUDO=1` if your user is in the docker group (recommended).
|
|||||||
groups per account the picker still loads the alphabetical
|
groups per account the picker still loads the alphabetical
|
||||||
top-200; operators with >200 groups need to use the list page's
|
top-200; operators with >200 groups need to use the list page's
|
||||||
search to find anything past 'L'.
|
search to find anything past 'L'.
|
||||||
- **Composite index on `(account_id, name)`** for the groups list
|
|
||||||
page's `ORDER BY name LIMIT 200` query — currently a sort + limit;
|
|
||||||
the GIN trigram on `name` plus the unique on `(account_id,
|
|
||||||
wa_group_jid)` already cover most cases.
|
|
||||||
- **Self-service password reset** (email link, etc.) — out of scope
|
- **Self-service password reset** (email link, etc.) — out of scope
|
||||||
for v1; admins use the Users page.
|
for v1; admins use the Users page.
|
||||||
|
|||||||
@ -82,11 +82,19 @@ export async function listGroupsForAccount(operatorId: string, accountId: string
|
|||||||
const account = await getAccount(operatorId, accountId);
|
const account = await getAccount(operatorId, accountId);
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
const trimmed = (q ?? "").trim();
|
const trimmed = (q ?? "").trim();
|
||||||
|
// Hide archived groups from the picker by default. They're rows
|
||||||
|
// that disappeared from the live participant list (group deleted,
|
||||||
|
// bot kicked, etc.) but still have reminder_targets pointing at
|
||||||
|
// them — see the soft-archive flow in apps/bot/src/whatsapp/
|
||||||
|
// group-sync.ts. Surfacing archived rows here would let an
|
||||||
|
// operator pick a group the bot can't actually reach.
|
||||||
const rows = trimmed
|
const rows = trimmed
|
||||||
? await db.execute(sql`
|
? await db.execute(sql`
|
||||||
SELECT id, account_id, wa_group_jid, name, participant_count, is_archived, last_synced_at
|
SELECT id, account_id, wa_group_jid, name, participant_count, is_archived, last_synced_at
|
||||||
FROM whatsapp_groups
|
FROM whatsapp_groups
|
||||||
WHERE account_id = ${accountId} AND name % ${trimmed}
|
WHERE account_id = ${accountId}
|
||||||
|
AND is_archived = false
|
||||||
|
AND name % ${trimmed}
|
||||||
ORDER BY similarity(name, ${trimmed}) DESC
|
ORDER BY similarity(name, ${trimmed}) DESC
|
||||||
LIMIT 50
|
LIMIT 50
|
||||||
`)
|
`)
|
||||||
@ -94,6 +102,7 @@ export async function listGroupsForAccount(operatorId: string, accountId: string
|
|||||||
SELECT id, account_id, wa_group_jid, name, participant_count, is_archived, last_synced_at
|
SELECT id, account_id, wa_group_jid, name, participant_count, is_archived, last_synced_at
|
||||||
FROM whatsapp_groups
|
FROM whatsapp_groups
|
||||||
WHERE account_id = ${accountId}
|
WHERE account_id = ${accountId}
|
||||||
|
AND is_archived = false
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
LIMIT 200
|
LIMIT 200
|
||||||
`);
|
`);
|
||||||
|
|||||||
1
packages/db/migrations/0014_lame_puck.sql
Normal file
1
packages/db/migrations/0014_lame_puck.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX IF NOT EXISTS "whatsapp_groups_account_name_idx" ON "whatsapp_groups" USING btree ("account_id","name");
|
||||||
1093
packages/db/migrations/meta/0014_snapshot.json
Normal file
1093
packages/db/migrations/meta/0014_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -99,6 +99,13 @@
|
|||||||
"when": 1778464004000,
|
"when": 1778464004000,
|
||||||
"tag": "0013_tricky_yellowjacket",
|
"tag": "0013_tricky_yellowjacket",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778464005000,
|
||||||
|
"tag": "0014_lame_puck",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
jsonb,
|
jsonb,
|
||||||
primaryKey,
|
primaryKey,
|
||||||
uniqueIndex,
|
uniqueIndex,
|
||||||
|
index,
|
||||||
inet,
|
inet,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
@ -80,6 +81,16 @@ export const whatsappGroups = pgTable(
|
|||||||
},
|
},
|
||||||
(t) => ({
|
(t) => ({
|
||||||
accountJidUnique: uniqueIndex("whatsapp_groups_account_jid_uq").on(t.accountId, t.waGroupJid),
|
accountJidUnique: uniqueIndex("whatsapp_groups_account_jid_uq").on(t.accountId, t.waGroupJid),
|
||||||
|
// Backs `WHERE account_id=? ORDER BY name ASC LIMIT 200` on the
|
||||||
|
// groups list page. Without this, PG falls back to the unique
|
||||||
|
// (account_id, wa_group_jid) index for the WHERE clause and then
|
||||||
|
// does an explicit sort on `name` — fine at small scale, slow
|
||||||
|
// when an operator has 3 000+ groups. Drizzle import is `index`,
|
||||||
|
// declared in this same file's import block.
|
||||||
|
accountNameIdx: index("whatsapp_groups_account_name_idx").on(
|
||||||
|
t.accountId,
|
||||||
|
t.name,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user