From 5c1c3a9ef266fc2c7214e96bf2e659843bba5d98 Mon Sep 17 00:00:00 2001 From: Timothy Quilling Date: Sun, 7 Dec 2025 21:13:57 -0500 Subject: [PATCH] feat: denormalize strategy; lists --- .../down.sql | 60 +++++++++++++++++++ .../up.sql | 52 ++++++++++++++++ parakeet-db/src/composite_types.rs | 17 +----- parakeet-db/src/models.rs | 48 ++++----------- parakeet-db/src/schema.rs | 50 ++++++---------- 5 files changed, 146 insertions(+), 81 deletions(-) create mode 100644 migrations/2025-12-08-015357_denormalize_list_moderation/down.sql create mode 100644 migrations/2025-12-08-015357_denormalize_list_moderation/up.sql diff --git a/migrations/2025-12-08-015357_denormalize_list_moderation/down.sql b/migrations/2025-12-08-015357_denormalize_list_moderation/down.sql new file mode 100644 index 00000000..24db2072 --- /dev/null +++ b/migrations/2025-12-08-015357_denormalize_list_moderation/down.sql @@ -0,0 +1,60 @@ +-- Rollback Phase 5: Restore thread_mutes, list_mutes, list_blocks tables + +-- Recreate thread_mutes table +CREATE TABLE thread_mutes ( + actor_id integer NOT NULL, + root_post_actor_id integer NOT NULL, + root_post_rkey bigint NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (actor_id, root_post_actor_id, root_post_rkey) +); + +-- Recreate list_mutes table +CREATE TABLE list_mutes ( + actor_id integer NOT NULL, + list_id bigint NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (actor_id, list_id), + FOREIGN KEY (list_id) REFERENCES lists(id) ON DELETE CASCADE +); + +-- Recreate list_blocks table +CREATE TABLE list_blocks ( + id bigserial PRIMARY KEY, + actor_id integer NOT NULL, + rkey bigint NOT NULL, + cid bytea NOT NULL, + list_id bigint NOT NULL, + UNIQUE (actor_id, rkey), + FOREIGN KEY (list_id) REFERENCES lists(id) ON DELETE CASCADE +); + +CREATE INDEX idx_list_blocks_actor ON list_blocks(actor_id); +CREATE INDEX idx_list_blocks_list ON list_blocks(list_id); + +-- Restore thread_mutes from array +INSERT INTO thread_mutes (actor_id, root_post_actor_id, root_post_rkey, created_at) +SELECT a.id, (tm).root_post_actor_id, (tm).root_post_rkey, (tm).created_at +FROM actors a, unnest(a.thread_mutes) AS tm +WHERE a.thread_mutes IS NOT NULL; + +-- Restore list_mutes from array +INSERT INTO list_mutes (actor_id, list_id, created_at) +SELECT a.id, (lm).list_id, (lm).created_at +FROM actors a, unnest(a.list_mutes) AS lm +WHERE a.list_mutes IS NOT NULL; + +-- Restore list_blocks from array +INSERT INTO list_blocks (actor_id, list_id, rkey, cid) +SELECT a.id, (lb).list_id, (lb).rkey, (lb).cid +FROM actors a, unnest(a.list_blocks) AS lb +WHERE a.list_blocks IS NOT NULL; + +-- Drop arrays from actors table +ALTER TABLE actors + DROP COLUMN thread_mutes, + DROP COLUMN list_mutes, + DROP COLUMN list_blocks; + +-- Note: Composite types (thread_mute_record, list_mute_record, list_block_record) +-- are not dropped here - they belong to Phase 1 migration diff --git a/migrations/2025-12-08-015357_denormalize_list_moderation/up.sql b/migrations/2025-12-08-015357_denormalize_list_moderation/up.sql new file mode 100644 index 00000000..f2406cda --- /dev/null +++ b/migrations/2025-12-08-015357_denormalize_list_moderation/up.sql @@ -0,0 +1,52 @@ +-- Phase 5: Denormalize list moderation into arrays on actors table +-- Migrates thread_mutes, list_mutes, list_blocks to arrays +-- Note: Composite types (thread_mute_record, list_mute_record, list_block_record) +-- were already created in Phase 1 migration + +-- Add moderation arrays to actors table +ALTER TABLE actors + ADD COLUMN thread_mutes thread_mute_record[], + ADD COLUMN list_mutes list_mute_record[], + ADD COLUMN list_blocks list_block_record[]; + +-- Backfill thread_mutes array from thread_mutes table +UPDATE actors a +SET thread_mutes = ( + SELECT ARRAY_AGG( + ROW(tm.root_post_actor_id, tm.root_post_rkey, tm.created_at)::thread_mute_record + ORDER BY tm.created_at DESC + ) + FROM thread_mutes tm + WHERE tm.actor_id = a.id +); + +-- Backfill list_mutes array from list_mutes table +-- Note: Converting list_id to natural keys (list_actor_id, list_rkey) +UPDATE actors a +SET list_mutes = ( + SELECT ARRAY_AGG( + ROW(l.actor_id, l.rkey, lm.created_at)::list_mute_record + ORDER BY lm.created_at DESC + ) + FROM list_mutes lm + JOIN lists l ON lm.list_id = l.id + WHERE lm.actor_id = a.id +); + +-- Backfill list_blocks array from list_blocks table +-- Note: Converting list_id to natural keys (list_actor_id, list_rkey) +UPDATE actors a +SET list_blocks = ( + SELECT ARRAY_AGG( + ROW(l.actor_id, l.rkey, lb.rkey, lb.cid)::list_block_record + ORDER BY lb.rkey DESC + ) + FROM list_blocks lb + JOIN lists l ON lb.list_id = l.id + WHERE lb.actor_id = a.id +); + +-- Drop old tables (foreign key constraints will be dropped automatically) +DROP TABLE thread_mutes; +DROP TABLE list_mutes; +DROP TABLE list_blocks; diff --git a/parakeet-db/src/composite_types.rs b/parakeet-db/src/composite_types.rs index 51b41d26..db6da8f2 100644 --- a/parakeet-db/src/composite_types.rs +++ b/parakeet-db/src/composite_types.rs @@ -27,8 +27,9 @@ use crate::schema::sql_types::{ PostExtEmbed, PostVideoEmbed, PostImageEmbed, PostFacetEmbed, PostVideoCaption, LabelerDefRecord, FollowRecord, MuteRecord, BlockRecord, BookmarkRecord, // Phase 4: User preferences - // Note: Other composite types will be added when used in Phase 5-7: - // ThreadMuteRecord, ListMuteRecord, ListBlockRecord, PostLabel, ActorLabel, + ThreadMuteRecord, ListMuteRecord, ListBlockRecord, // Phase 5: List moderation + // Note: Other composite types will be added when used in Phase 7: + // PostLabel, ActorLabel, }; // Placeholder SQL types for composite types not yet used in tables @@ -38,18 +39,6 @@ mod placeholder_sql_types { use diesel::query_builder::QueryId; use diesel::sql_types::SqlType; - #[derive(QueryId, SqlType)] - #[diesel(postgres_type(name = "thread_mute_record"))] - pub struct ThreadMuteRecord; - - #[derive(QueryId, SqlType)] - #[diesel(postgres_type(name = "list_mute_record"))] - pub struct ListMuteRecord; - - #[derive(QueryId, SqlType)] - #[diesel(postgres_type(name = "list_block_record"))] - pub struct ListBlockRecord; - #[derive(QueryId, SqlType)] #[diesel(postgres_type(name = "post_label"))] pub struct PostLabel; diff --git a/parakeet-db/src/models.rs b/parakeet-db/src/models.rs index 2c2584a4..a9ce428f 100644 --- a/parakeet-db/src/models.rs +++ b/parakeet-db/src/models.rs @@ -168,6 +168,11 @@ pub struct Actor { pub mutes: Option>>, // Muted actors pub blocks: Option>>, // Blocked actors pub bookmarks: Option>>, // Bookmarked posts + + // List moderation arrays (from thread_mutes, list_mutes, list_blocks tables, denormalized) + pub thread_mutes: Option>>, // Muted threads + pub list_mutes: Option>>, // Muted lists + pub list_blocks: Option>>, // List blocks } // AllowlistEntry model removed - allowlist table dropped in favor of actors.sync_state @@ -522,28 +527,12 @@ pub struct ListItem { // Note: created_at derived from TID rkey via created_at() method } -#[derive(Debug, Queryable, Selectable, Identifiable)] -#[diesel(table_name = crate::schema::list_blocks)] -#[diesel(primary_key(id))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ListBlock { - pub id: i64, // PK: Surrogate key - pub actor_id: i32, // FK to actors - pub rkey: i64, // TID as INT8 - pub cid: Vec, // 32-byte CID digest - pub list_id: i64, // FK to lists - // Note: created_at derived from TID rkey via created_at() method -} - -#[derive(Clone, Debug, Queryable, Selectable)] -#[diesel(table_name = crate::schema::list_mutes)] -#[diesel(primary_key(actor_id, list_id))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ListMute { - pub actor_id: i32, // PK: FK to actors - pub list_id: i64, // PK: FK to lists - pub created_at: DateTime, // Off-protocol: has own timestamp -} +// Note: ListBlock, ListMute, ThreadMute models removed +// List moderation data is now stored as arrays on actors table: +// - thread_mutes: thread_mute_record[] +// - list_mutes: list_mute_record[] +// - list_blocks: list_block_record[] +// Composite types are defined in composite_types.rs // ============================================================================= // STARTER PACKS @@ -594,17 +583,6 @@ pub struct Threadgate { pub post_rkey: i64, // Same as rkey (denormalized in post) } -#[derive(Clone, Debug, Queryable, Selectable)] -#[diesel(table_name = crate::schema::thread_mutes)] -#[diesel(primary_key(actor_id, root_post_actor_id, root_post_rkey))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ThreadMute { - pub actor_id: i32, // PK: FK to actors - pub root_post_actor_id: i32, // PK: FK to posts (root post actor) - pub root_post_rkey: i64, // PK: FK to posts (root post rkey) - pub created_at: DateTime, // Off-protocol: has own timestamp -} - // ============================================================================= // MODERATION & LABELS // ============================================================================= @@ -772,7 +750,7 @@ impl_tid_rkey!(Block); impl_tid_rkey!(Bookmark); // NOTE: FeedGen and List intentionally excluded - use arbitrary String rkeys, not TIDs impl_tid_rkey!(ListItem); -impl_tid_rkey!(ListBlock); +// impl_tid_rkey!(ListBlock); // Removed - list_blocks table dropped, now list_block_record[] arrays impl_tid_rkey!(StarterPack); impl_tid_rkey!(Verification); @@ -790,7 +768,7 @@ impl_tid_created_at!(Repost); impl_tid_created_at!(Block); impl_tid_created_at!(Bookmark); impl_tid_created_at!(ListItem); -impl_tid_created_at!(ListBlock); +// impl_tid_created_at!(ListBlock); // Removed - list_blocks table dropped, now list_block_record[] arrays // NOTE: List uses String rkey and has custom created_at() implementation impl_tid_created_at!(StarterPack); impl_tid_created_at!(Verification); diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs index 641bc2c8..f0283bcd 100644 --- a/parakeet-db/src/schema.rs +++ b/parakeet-db/src/schema.rs @@ -73,6 +73,14 @@ pub mod sql_types { #[diesel(postgres_type(name = "language_code"))] pub struct LanguageCode; + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "list_block_record"))] + pub struct ListBlockRecord; + + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "list_mute_record"))] + pub struct ListMuteRecord; + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "list_type"))] pub struct ListType; @@ -145,6 +153,10 @@ pub mod sql_types { #[diesel(postgres_type(name = "subject_type"))] pub struct SubjectType; + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "thread_mute_record"))] + pub struct ThreadMuteRecord; + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "threadgate_rule"))] pub struct ThreadgateRule; @@ -191,6 +203,9 @@ diesel::table! { use super::sql_types::MuteRecord; use super::sql_types::BlockRecord; use super::sql_types::BookmarkRecord; + use super::sql_types::ThreadMuteRecord; + use super::sql_types::ListMuteRecord; + use super::sql_types::ListBlockRecord; actors (id) { id -> Int4, @@ -246,6 +261,9 @@ diesel::table! { mutes -> Nullable>>, blocks -> Nullable>>, bookmarks -> Nullable>>, + thread_mutes -> Nullable>>, + list_mutes -> Nullable>>, + list_blocks -> Nullable>>, } } @@ -356,16 +374,6 @@ diesel::table! { } } -diesel::table! { - list_blocks (id) { - id -> Int8, - actor_id -> Int4, - rkey -> Int8, - cid -> Bytea, - list_id -> Int8, - } -} - diesel::table! { list_items (actor_id, rkey) { actor_id -> Int4, @@ -376,14 +384,6 @@ diesel::table! { } } -diesel::table! { - list_mutes (actor_id, list_id) { - actor_id -> Int4, - list_id -> Int8, - created_at -> Timestamptz, - } -} - diesel::table! { use diesel::sql_types::*; use super::sql_types::ListType; @@ -558,15 +558,6 @@ diesel::table! { } } -diesel::table! { - thread_mutes (actor_id, root_post_actor_id, root_post_rkey) { - actor_id -> Int4, - root_post_actor_id -> Int4, - root_post_rkey -> Int8, - created_at -> Timestamptz, - } -} - diesel::table! { uris (id) { id -> Int4, @@ -589,9 +580,7 @@ diesel::table! { } diesel::joinable!(actors -> starterpacks (profile_joined_sp_id)); -diesel::joinable!(list_blocks -> lists (list_id)); diesel::joinable!(list_items -> lists (list_id)); -diesel::joinable!(list_mutes -> lists (list_id)); diesel::joinable!(starterpack_feeds -> feedgens (feed_id)); diesel::joinable!(starterpack_feeds -> starterpacks (starterpack_id)); diesel::joinable!(starterpacks -> lists (list_id)); @@ -607,9 +596,7 @@ diesel::allow_tables_to_appear_in_same_query!( handle_resolution_queue, jetstream_cursors, labels, - list_blocks, list_items, - list_mutes, lists, notifications, post_aggregate_stats, @@ -618,7 +605,6 @@ diesel::allow_tables_to_appear_in_same_query!( spatial_ref_sys, starterpack_feeds, starterpacks, - thread_mutes, uris, verification, ); -- 2.51.2