From 173b2d21dde16789353f8a69102dc222942e7775 Mon Sep 17 00:00:00 2001 From: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Date: Sun, 12 Jul 2026 21:47:18 -0400 Subject: [PATCH] perf(db): index thread reply keyset pagination Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: coder 0 Signed-off-by: coder 0 --- crates/buzz-db/src/migration.rs | 21 ++++++++++++++++++- .../0032_thread_metadata_keyset_index.sql | 21 +++++++++++++++++++ schema/schema.sql | 2 ++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 migrations/0032_thread_metadata_keyset_index.sql diff --git a/crates/buzz-db/src/migration.rs b/crates/buzz-db/src/migration.rs index be87faa1ac..021af5d223 100644 --- a/crates/buzz-db/src/migration.rs +++ b/crates/buzz-db/src/migration.rs @@ -625,7 +625,7 @@ mod tests { let mut migrations: Vec<_> = MIGRATOR.iter().collect(); migrations.sort_by_key(|migration| migration.version); - assert_eq!(migrations.len(), 31); + assert_eq!(migrations.len(), 32); assert_eq!(migrations[0].version, 1); assert_eq!(&*migrations[0].description, "initial schema"); assert!(migrations[0] @@ -1057,6 +1057,25 @@ mod tests { .as_str() .contains("error_code")); assert!(include_str!("../../../schema/schema.sql").contains("error_code TEXT")); + + // Thread subtree pagination uses the full tenant/root/keyset tuple. Keep + // the covering index additive so brownfield migration checksums remain + // stable. + assert_eq!(migrations[31].version, 32); + assert!(migrations[31] + .sql + .as_str() + .contains("CREATE INDEX IF NOT EXISTS idx_thread_metadata_root_keyset")); + assert!(migrations[31] + .sql + .as_str() + .contains("(community_id, root_event_id, event_created_at, event_id)")); + assert!(!migrations[0] + .sql + .as_str() + .contains("idx_thread_metadata_root_keyset")); + assert!(include_str!("../../../schema/schema.sql") + .contains("CREATE INDEX idx_thread_metadata_root_keyset")); } #[test] diff --git a/migrations/0032_thread_metadata_keyset_index.sql b/migrations/0032_thread_metadata_keyset_index.sql new file mode 100644 index 0000000000..9142f0bccf --- /dev/null +++ b/migrations/0032_thread_metadata_keyset_index.sql @@ -0,0 +1,21 @@ +-- ── Thread reply keyset pagination index ───────────────────────────────────── +-- Thread subtree reads filter by tenant and root, then page in ascending +-- (event_created_at, event_id) order. Include the keyset columns so PostgreSQL +-- can satisfy both the filter and ordering from one index scan. +-- +-- Lock note: built without CONCURRENTLY because sqlx runs each migration inside +-- a transaction and CREATE INDEX CONCURRENTLY cannot run in one. This takes a +-- SHARE lock on `thread_metadata` (blocking writes, not reads) for the duration +-- of the build. `thread_metadata` is on the event insertion path and may be +-- large on brownfield databases, so operators should pre-build it by hand before +-- deploying this migration: +-- +-- CREATE INDEX CONCURRENTLY idx_thread_metadata_root_keyset +-- ON thread_metadata (community_id, root_event_id, event_created_at, event_id); +-- +-- IF NOT EXISTS then makes this migration a no-op on that database. +-- +-- Additive migration: previously applied files must not change checksum. + +CREATE INDEX IF NOT EXISTS idx_thread_metadata_root_keyset + ON thread_metadata (community_id, root_event_id, event_created_at, event_id); diff --git a/schema/schema.sql b/schema/schema.sql index 9ef7bc0a4b..44bd90bb18 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -531,6 +531,8 @@ CREATE TABLE thread_metadata ( CREATE INDEX idx_thread_metadata_parent ON thread_metadata (community_id, parent_event_id); CREATE INDEX idx_thread_metadata_root ON thread_metadata (community_id, root_event_id); +CREATE INDEX idx_thread_metadata_root_keyset + ON thread_metadata (community_id, root_event_id, event_created_at, event_id); CREATE INDEX idx_thread_metadata_channel_depth ON thread_metadata (community_id, channel_id, depth, event_created_at); CREATE INDEX idx_thread_metadata_event_id ON thread_metadata (community_id, event_id);