From 1fb5a135732c98625935bc1d98d35d8926af25ed Mon Sep 17 00:00:00 2001 From: Timothy Quilling Date: Sun, 7 Dec 2025 15:50:05 -0500 Subject: [PATCH] feat: complete gate migration, update tests --- consumer/src/db/operations/feed/threadgate.rs | 33 ++---- consumer/src/db/record_exists/queries.rs | 22 ++-- consumer/tests/feed_operations_test.rs | 110 ++++++++---------- .../down.sql | 17 +++ .../2025-12-07-182139_drop_gate_tables/up.sql | 11 ++ parakeet-db/src/models.rs | 68 ++--------- parakeet-db/src/schema.rs | 60 ---------- parakeet/src/db/posts.rs | 17 ++- parakeet/src/sql/post_state.sql | 6 +- .../src/sql/thread_v2_hidden_children.sql | 16 +-- 10 files changed, 129 insertions(+), 231 deletions(-) create mode 100644 migrations/2025-12-07-182139_drop_gate_tables/down.sql create mode 100644 migrations/2025-12-07-182139_drop_gate_tables/up.sql diff --git a/consumer/src/db/operations/feed/threadgate.rs b/consumer/src/db/operations/feed/threadgate.rs index c7ef6661..f1c5525e 100644 --- a/consumer/src/db/operations/feed/threadgate.rs +++ b/consumer/src/db/operations/feed/threadgate.rs @@ -17,42 +17,33 @@ use deadpool_postgres::GenericClient; use eyre::Context as _; use ipld_core::cid::Cid; -/// Get threadgate rules and allowed lists for a post +/// Get threadgate rules and allowed lists for a post (DENORMALIZED) /// /// Returns (created_at, allow_rules, allowed_list_uris) if threadgate exists. /// The allow_rules are string representations like "app.bsky.feed.threadgate#mentionRule". /// The allowed_list_uris are full AT URIs for list-based reply permissions. +/// +/// DENORMALIZED: Reads threadgate_allow from posts table instead of separate threadgates table. pub async fn threadgate_get( conn: &C, post: &str, ) -> Result, Vec, Vec)>> { - // Parse post AT URI to get post_id, then fetch threadgate with allowed list URIs + // Parse post AT URI and fetch threadgate data from denormalized posts table columns let res = conn .query_opt( "WITH post_parts AS ( SELECT SPLIT_PART(SUBSTRING($1 FROM 6), '/', 1) as did, SPLIT_PART(SUBSTRING($1 FROM 6), '/', 3) as rkey - ), - post_lookup AS ( - SELECT p.actor_id, p.rkey, tid_timestamp(p.rkey) as created_at - FROM post_parts pp - INNER JOIN actors a ON a.did = pp.did - INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64(pp.rkey) - ), - allowed_list_uris AS ( - SELECT ARRAY_AGG('at://' || la.did || '/app.bsky.graph.list/' || l.rkey) as uris - FROM post_lookup pl - INNER JOIN threadgates tg ON tg.post_actor_id = pl.actor_id AND tg.post_rkey = pl.rkey - INNER JOIN threadgate_allowed_lists tal ON tal.post_actor_id = tg.post_actor_id AND tal.post_rkey = tg.post_rkey - INNER JOIN lists l ON l.id = tal.list_id - INNER JOIN actors la ON la.id = l.actor_id ) - SELECT pl.created_at, tg.allow::text[], COALESCE(alu.uris, ARRAY[]::text[]) as allowed_lists - FROM post_lookup pl - INNER JOIN threadgates tg ON tg.post_actor_id = pl.actor_id AND tg.post_rkey = pl.rkey - LEFT JOIN allowed_list_uris alu ON true - WHERE tg.allow IS NOT NULL", + SELECT + tid_timestamp(p.rkey) as created_at, + p.threadgate_allow::text[], + ARRAY[]::text[] as allowed_lists -- TODO: allowed_lists not yet implemented in denormalized schema + FROM post_parts pp + INNER JOIN actors a ON a.did = pp.did + INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64(pp.rkey) + WHERE p.threadgate_allow IS NOT NULL", &[&post], ) .await? diff --git a/consumer/src/db/record_exists/queries.rs b/consumer/src/db/record_exists/queries.rs index f84e3312..29e9f895 100644 --- a/consumer/src/db/record_exists/queries.rs +++ b/consumer/src/db/record_exists/queries.rs @@ -168,7 +168,9 @@ pub async fn list_block_exists( Ok(row.is_some()) } -/// Check if a threadgate exists +/// Check if a threadgate exists (DENORMALIZED) +/// +/// DENORMALIZED: Checks posts.threadgate_allow instead of separate threadgates table. pub async fn threadgate_exists( conn: &C, did: &str, @@ -176,16 +178,19 @@ pub async fn threadgate_exists( ) -> QueryResult { let row = conn .query_opt( - "SELECT 1 FROM threadgates tg - INNER JOIN actors a ON tg.actor_id = a.id - WHERE a.did = $1 AND tg.rkey = $2", + "SELECT 1 FROM posts p + INNER JOIN actors a ON p.actor_id = a.id + WHERE a.did = $1 AND p.rkey = $2 + AND p.threadgate_allow IS NOT NULL", &[&did, &rkey], ) .await?; Ok(row.is_some()) } -/// Check if a postgate exists +/// Check if a postgate exists (DENORMALIZED) +/// +/// DENORMALIZED: Checks posts.postgate_rules instead of separate postgates table. pub async fn postgate_exists( conn: &C, did: &str, @@ -193,9 +198,10 @@ pub async fn postgate_exists( ) -> QueryResult { let row = conn .query_opt( - "SELECT 1 FROM postgates pg - INNER JOIN actors a ON pg.actor_id = a.id - WHERE a.did = $1 AND pg.rkey = $2", + "SELECT 1 FROM posts p + INNER JOIN actors a ON p.actor_id = a.id + WHERE a.did = $1 AND p.rkey = $2 + AND p.postgate_rules IS NOT NULL", &[&did, &rkey], ) .await?; diff --git a/consumer/tests/feed_operations_test.rs b/consumer/tests/feed_operations_test.rs index 925a1a20..50b4302e 100644 --- a/consumer/tests/feed_operations_test.rs +++ b/consumer/tests/feed_operations_test.rs @@ -957,15 +957,15 @@ async fn test_postgate_upsert_insert() -> eyre::Result<()> { .await .wrap_err("Failed to insert post for postgate")?; - // Create a postgate + // Create a postgate (AT Protocol: postgate uses same rkey as the post it protects) let postgate = AppBskyFeedPostgate { post: "at://did:plc:postgateowner1/app.bsky.feed.post/3l7mkz4lmk23h".to_string(), created_at: Utc::now(), detached_embedding_uris: vec![], embedding_rules: vec![PostgateEmbeddingRules::Disable]}; - let _at_uri = "at://did:plc:postgateowner1/app.bsky.feed.postgate/3l7mkz4lmk23j"; - let result = feed::postgate_upsert(&tx, postgateowner1_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23j")?, test_cid(), &postgate).await; + let _at_uri = "at://did:plc:postgateowner1/app.bsky.feed.postgate/3l7mkz4lmk23h"; + let result = feed::postgate_upsert(&tx, postgateowner1_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23h")?, test_cid(), &postgate).await; assert!( result.is_ok(), @@ -974,18 +974,18 @@ async fn test_postgate_upsert_insert() -> eyre::Result<()> { ); assert_eq!(result.wrap_err("Operation failed")?, 1, "Should insert 1 row"); - // Verify postgate was created and post_actor_id is set - let post_actor_id_is_set: bool = tx.query_one( - "SELECT pg.post_actor_id IS NOT NULL + // Verify postgate was created (DENORMALIZED: check posts.postgate_rules on the POST being protected) + let postgate_exists: bool = tx.query_one( + "SELECT p.postgate_rules IS NOT NULL FROM actors a - LEFT JOIN postgates pg ON pg.actor_id = a.id AND pg.rkey = tid_to_i64($2) + INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64($2) WHERE a.did = $1", - &[&"did:plc:postgateowner1", &"3l7mkz4lmk23j"], + &[&"did:plc:postgateowner1", &"3l7mkz4lmk23h"], // Use post's rkey, not postgate's rkey ) .await - .wrap_err("query postgate and verify post_actor_id")? + .wrap_err("query posts table to verify postgate_rules exist")? .get(0); - assert!(post_actor_id_is_set, "Postgate should exist with post_actor_id set"); + assert!(postgate_exists, "Postgate should exist (postgate_rules set in posts table)"); Ok(()) } @@ -1025,20 +1025,20 @@ async fn test_postgate_delete() -> eyre::Result<()> { .await .wrap_err("Failed to insert post for postgate")?; - // Create a postgate + // Create a postgate (AT Protocol: postgate uses same rkey as the post it protects) let postgate = AppBskyFeedPostgate { post: "at://did:plc:postgateowner2/app.bsky.feed.post/3l7mkz4lmk23k".to_string(), created_at: Utc::now(), detached_embedding_uris: vec![], embedding_rules: vec![]}; - let _at_uri = "at://did:plc:postgateowner2/app.bsky.feed.postgate/3l7mkz4lmk23m"; - feed::postgate_upsert(&tx, postgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23m")?, test_cid(), &postgate) + let _at_uri = "at://did:plc:postgateowner2/app.bsky.feed.postgate/3l7mkz4lmk23k"; + feed::postgate_upsert(&tx, postgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23k")?, test_cid(), &postgate) .await .wrap_err("Failed to insert postgate")?; - // Delete the postgate - let result = feed::postgate_delete(&tx, postgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23m")?).await; + // Delete the postgate (same rkey as post) + let result = feed::postgate_delete(&tx, postgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23k")?).await; assert!( result.is_ok(), @@ -1047,19 +1047,18 @@ async fn test_postgate_delete() -> eyre::Result<()> { ); assert_eq!(result.unwrap(), 1, "Should delete 1 row"); - // Verify postgate was deleted + // Verify postgate was deleted (DENORMALIZED: check posts.postgate_rules is NULL on the POST) let postgate_exists: bool = tx.query_one( - "SELECT EXISTS( - SELECT 1 FROM postgates pg - INNER JOIN actors a ON pg.actor_id = a.id - WHERE a.did = $1 AND pg.rkey = tid_to_i64($2) - )", - &[&"did:plc:postgateowner2", &"3l7mkz4lmk23m"], + "SELECT COALESCE(p.postgate_rules IS NOT NULL, false) + FROM actors a + INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64($2) + WHERE a.did = $1", + &[&"did:plc:postgateowner2", &"3l7mkz4lmk23k"], // Use post's rkey, not postgate's rkey ) .await - .wrap_err("query postgates table for existence check after delete")? + .wrap_err("query posts table for postgate_rules after delete")? .get(0); - assert!(!postgate_exists, "Postgate should be deleted"); + assert!(!postgate_exists, "Postgate should be deleted (postgate_rules cleared in posts table)"); Ok(()) } @@ -1103,15 +1102,15 @@ async fn test_threadgate_upsert_insert() -> eyre::Result<()> { .await .wrap_err("Failed to insert post for threadgate")?; - // Create a threadgate + // Create a threadgate (AT Protocol: threadgate uses same rkey as the post it protects) let threadgate = AppBskyFeedThreadgate { post: "at://did:plc:threadgateowner1/app.bsky.feed.post/3l7mkz4lmk23n".to_string(), created_at: Utc::now(), allow: Some(vec![ThreadgateRule::Mention, ThreadgateRule::Following]), hidden_replies: vec![]}; - let _at_uri = "at://did:plc:threadgateowner1/app.bsky.feed.threadgate/3l7mkz4lmk23p"; - let result = feed::threadgate_upsert(&tx, threadgateowner1_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23p")?, test_cid(), threadgate).await; + let _at_uri = "at://did:plc:threadgateowner1/app.bsky.feed.threadgate/3l7mkz4lmk23n"; + let result = feed::threadgate_upsert(&tx, threadgateowner1_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23n")?, test_cid(), threadgate).await; assert!( result.is_ok(), @@ -1120,32 +1119,18 @@ async fn test_threadgate_upsert_insert() -> eyre::Result<()> { ); assert_eq!(result.wrap_err("Operation failed")?, 1, "Should insert 1 row"); - // Verify threadgate was created + // Verify threadgate was created (DENORMALIZED: check posts.threadgate_allow on the POST being protected) let threadgate_exists: bool = tx.query_one( - "SELECT EXISTS( - SELECT 1 FROM threadgates tg - INNER JOIN actors a ON tg.actor_id = a.id - WHERE a.did = $1 AND tg.rkey = tid_to_i64($2) - )", - &[&"did:plc:threadgateowner1", &"3l7mkz4lmk23p"], - ) - .await - .wrap_err("query threadgates table for existence check")? - .get(0); - assert!(threadgate_exists, "Threadgate should exist"); - - // Verify post_actor_id is set (not pending) - let post_actor_id_is_set: bool = tx.query_one( - "SELECT tg.post_actor_id IS NOT NULL - FROM threadgates tg - INNER JOIN actors a ON tg.actor_id = a.id - WHERE a.did = $1 AND tg.rkey = tid_to_i64($2)", - &[&"did:plc:threadgateowner1", &"3l7mkz4lmk23p"], + "SELECT p.threadgate_allow IS NOT NULL + FROM actors a + INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64($2) + WHERE a.did = $1", + &[&"did:plc:threadgateowner1", &"3l7mkz4lmk23n"], // Use post's rkey, not threadgate's rkey ) .await - .wrap_err("query threadgate post_actor_id field")? + .wrap_err("query posts table to verify threadgate_allow exists")? .get(0); - assert!(post_actor_id_is_set, "Threadgate should have post_actor_id set"); + assert!(threadgate_exists, "Threadgate should exist (threadgate_allow set in posts table)"); Ok(()) } @@ -1185,20 +1170,20 @@ async fn test_threadgate_delete() -> eyre::Result<()> { .await .wrap_err("Failed to insert post for threadgate")?; - // Create a threadgate + // Create a threadgate (AT Protocol: threadgate uses same rkey as the post it protects) let threadgate = AppBskyFeedThreadgate { post: "at://did:plc:threadgateowner2/app.bsky.feed.post/3l7mkz4lmk23r".to_string(), created_at: Utc::now(), allow: Some(vec![ThreadgateRule::Follower]), hidden_replies: vec![]}; - let _at_uri = "at://did:plc:threadgateowner2/app.bsky.feed.threadgate/3l7mkz4lmk23s"; - feed::threadgate_upsert(&tx, threadgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23s")?, test_cid(), threadgate) + let _at_uri = "at://did:plc:threadgateowner2/app.bsky.feed.threadgate/3l7mkz4lmk23r"; + feed::threadgate_upsert(&tx, threadgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23r")?, test_cid(), threadgate) .await .wrap_err("Failed to insert threadgate")?; - // Delete the threadgate - let result = feed::threadgate_delete(&tx, threadgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23s")?).await; + // Delete the threadgate (same rkey as post) + let result = feed::threadgate_delete(&tx, threadgateowner2_actor_id, parakeet_db::models::tid_to_i64("3l7mkz4lmk23r")?).await; assert!( result.is_ok(), @@ -1207,19 +1192,18 @@ async fn test_threadgate_delete() -> eyre::Result<()> { ); assert_eq!(result.unwrap(), 1, "Should delete 1 row"); - // Verify threadgate was deleted + // Verify threadgate was deleted (DENORMALIZED: check posts.threadgate_allow is NULL on the POST) let threadgate_exists: bool = tx.query_one( - "SELECT EXISTS( - SELECT 1 FROM threadgates tg - INNER JOIN actors a ON tg.actor_id = a.id - WHERE a.did = $1 AND tg.rkey = tid_to_i64($2) - )", - &[&"did:plc:threadgateowner2", &"3l7mkz4lmk23s"], + "SELECT COALESCE(p.threadgate_allow IS NOT NULL, false) + FROM actors a + INNER JOIN posts p ON p.actor_id = a.id AND p.rkey = tid_to_i64($2) + WHERE a.did = $1", + &[&"did:plc:threadgateowner2", &"3l7mkz4lmk23r"], // Use post's rkey, not threadgate's rkey ) .await - .wrap_err("query threadgates table for existence check after delete")? + .wrap_err("query posts table for threadgate_allow after delete")? .get(0); - assert!(!threadgate_exists, "Threadgate should be deleted"); + assert!(!threadgate_exists, "Threadgate should be deleted (threadgate_allow cleared in posts table)"); Ok(()) } diff --git a/migrations/2025-12-07-182139_drop_gate_tables/down.sql b/migrations/2025-12-07-182139_drop_gate_tables/down.sql new file mode 100644 index 00000000..0129a7ae --- /dev/null +++ b/migrations/2025-12-07-182139_drop_gate_tables/down.sql @@ -0,0 +1,17 @@ +-- Rollback for dropping gate tables +-- WARNING: This migration is not fully reversible! +-- The original tables can be recreated, but the data has been denormalized into posts table. +-- To fully restore, you would need to extract data from posts.threadgate_* and posts.postgate_* columns. + +-- This down migration is intentionally left empty as a safety measure. +-- If you need to rollback, you should: +-- 1. Manually recreate the tables using the original schema +-- 2. Extract data from posts table denormalized columns +-- 3. Revert code changes to use the old tables + +-- For reference, the dropped tables were: +-- - threadgates (actor_id, rkey, cid, post_actor_id, post_rkey, allow) +-- - postgates (actor_id, rkey, cid, post_actor_id, post_rkey, embedding_rules) +-- - threadgate_hidden_replies (post_actor_id, post_rkey, hidden_post_actor_id, hidden_post_rkey) +-- - threadgate_allowed_lists (post_actor_id, post_rkey, list_id) +-- - postgate_detached (post_actor_id, post_rkey, detached_actor_id, detached_rkey) diff --git a/migrations/2025-12-07-182139_drop_gate_tables/up.sql b/migrations/2025-12-07-182139_drop_gate_tables/up.sql new file mode 100644 index 00000000..73f65f23 --- /dev/null +++ b/migrations/2025-12-07-182139_drop_gate_tables/up.sql @@ -0,0 +1,11 @@ +-- Drop old gate tables after denormalization into posts table +-- Gate data is now stored in posts.threadgate_* and posts.postgate_* columns + +-- Drop junction tables first +DROP TABLE IF EXISTS threadgate_hidden_replies; +DROP TABLE IF EXISTS threadgate_allowed_lists; +DROP TABLE IF EXISTS postgate_detached; + +-- Drop main gate tables +DROP TABLE IF EXISTS threadgates; +DROP TABLE IF EXISTS postgates; diff --git a/parakeet-db/src/models.rs b/parakeet-db/src/models.rs index 07a753eb..f344efb1 100644 --- a/parakeet-db/src/models.rs +++ b/parakeet-db/src/models.rs @@ -398,31 +398,6 @@ pub struct Uri { // Postgates -#[derive(Clone, Debug, Queryable, Selectable, Identifiable)] -#[diesel(table_name = crate::schema::postgates)] -#[diesel(primary_key(post_actor_id, post_rkey))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Postgate { - pub actor_id: i32, // FK to actors (who created the postgate) - pub rkey: i64, // TID as INT8 - pub cid: Vec, // 32-byte CID digest - pub post_actor_id: i32, // PK part 1: Natural key reference to posts (NOT NULL - required) - pub post_rkey: i64, // PK part 2: Natural key reference to posts (NOT NULL - required) - pub rules: array_helpers::PostgateRuleArray, // ENUM array: everybody | nobody - // Note: created_at derived from TID rkey via created_at() method -} - -#[derive(Clone, Debug, Queryable, Selectable)] -#[diesel(table_name = crate::schema::postgate_detached)] -#[diesel(primary_key(post_actor_id, post_rkey, detached_post_actor_id, detached_post_rkey))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct PostgateDetached { - pub post_actor_id: i32, // PK part 1: FK to postgates (via natural key) - pub post_rkey: i64, // PK part 2: FK to postgates (via natural key) - pub detached_post_actor_id: i32, // PK part 3: Natural key reference to detached post - pub detached_post_rkey: i64, // PK part 4: Natural key reference to detached post -} - // ============================================================================= // SOCIAL INTERACTIONS // ============================================================================= @@ -641,39 +616,16 @@ pub struct StarterPackFeed { // THREAD CONTROL // ============================================================================= -#[derive(Clone, Debug, Serialize, Deserialize, Queryable, Selectable, Identifiable)] -#[diesel(table_name = crate::schema::threadgates)] -#[diesel(primary_key(post_actor_id, post_rkey))] -#[diesel(check_for_backend(diesel::pg::Pg))] +// Threadgate struct for denormalized use (no longer has a separate table) +// Used only for compatibility with EnrichedThreadgate in parakeet loader +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Threadgate { - pub actor_id: i32, // FK to actors (who created the threadgate) - pub rkey: i64, // TID as INT8 - pub cid: Vec, // 32-byte CID digest + pub actor_id: i32, // Post's actor_id (threadgate typically owned by post author) + pub rkey: i64, // Post's rkey (synthetic - threadgate has own rkey in reality) + pub cid: Vec, // Synthetic CID (deterministic based on post key) pub allow: Option, // ENUM array: mention | following | list - pub post_actor_id: i32, // PK part 1: Natural key reference to posts (NOT NULL - required) - pub post_rkey: i64, // PK part 2: Natural key reference to posts (NOT NULL - required) - // Note: created_at derived from TID rkey via created_at() method -} - -#[derive(Clone, Debug, Queryable, Selectable)] -#[diesel(table_name = crate::schema::threadgate_hidden_replies)] -#[diesel(primary_key(post_actor_id, post_rkey, hidden_post_actor_id, hidden_post_rkey))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ThreadgateHiddenReply { - pub post_actor_id: i32, // PK part 1: FK to threadgates (via natural key) - pub post_rkey: i64, // PK part 2: FK to threadgates (via natural key) - pub hidden_post_actor_id: i32, // PK part 3: Natural key reference to hidden reply - pub hidden_post_rkey: i64, // PK part 4: Natural key reference to hidden reply -} - -#[derive(Clone, Debug, Queryable, Selectable)] -#[diesel(table_name = crate::schema::threadgate_allowed_lists)] -#[diesel(primary_key(post_actor_id, post_rkey, list_id))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ThreadgateAllowedList { - pub post_actor_id: i32, // PK part 1: FK to threadgates (via natural key) - pub post_rkey: i64, // PK part 2: FK to threadgates (via natural key) - pub list_id: i64, // PK part 3: FK to lists (allowed list) + pub post_actor_id: i32, // Same as actor_id (denormalized in post) + pub post_rkey: i64, // Same as rkey (denormalized in post) } #[derive(Clone, Debug, Queryable, Selectable)] @@ -872,7 +824,6 @@ pub mod array_helpers { // Implement HasTidRkey for all models with TID-based rkeys impl_tid_rkey!(Post); -impl_tid_rkey!(Postgate); impl_tid_rkey!(FeedgenLike); impl_tid_rkey!(LabelerLike); impl_tid_rkey!(Repost); @@ -883,7 +834,6 @@ impl_tid_rkey!(Bookmark); impl_tid_rkey!(ListItem); impl_tid_rkey!(ListBlock); impl_tid_rkey!(StarterPack); -impl_tid_rkey!(Threadgate); impl_tid_rkey!(Verification); // ============================================================================= @@ -893,7 +843,6 @@ impl_tid_rkey!(Verification); // This eliminates the need for a separate created_at column in the database impl_tid_created_at!(Post); -impl_tid_created_at!(Postgate); impl_tid_created_at!(FeedgenLike); impl_tid_created_at!(LabelerLike); impl_tid_created_at!(Repost); @@ -904,7 +853,6 @@ impl_tid_created_at!(ListItem); impl_tid_created_at!(ListBlock); // NOTE: List uses String rkey and has custom created_at() implementation impl_tid_created_at!(StarterPack); -impl_tid_created_at!(Threadgate); impl_tid_created_at!(Verification); // Custom created_at() implementation for List (String rkey can be TID or arbitrary) diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs index a81859e4..42f90ab6 100644 --- a/parakeet-db/src/schema.rs +++ b/parakeet-db/src/schema.rs @@ -473,29 +473,6 @@ diesel::table! { } } -diesel::table! { - postgate_detached (post_actor_id, post_rkey, detached_post_actor_id, detached_post_rkey) { - post_actor_id -> Int4, - post_rkey -> Int8, - detached_post_actor_id -> Int4, - detached_post_rkey -> Int8, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::PostgateRule; - - postgates (actor_id, rkey) { - actor_id -> Int4, - rkey -> Int8, - cid -> Bytea, - post_actor_id -> Int4, - post_rkey -> Int8, - rules -> Array>, - } -} - diesel::table! { use diesel::sql_types::*; use super::sql_types::LanguageCode; @@ -626,37 +603,6 @@ diesel::table! { } } -diesel::table! { - threadgate_allowed_lists (post_actor_id, post_rkey, list_id) { - post_actor_id -> Int4, - post_rkey -> Int8, - list_id -> Int8, - } -} - -diesel::table! { - threadgate_hidden_replies (post_actor_id, post_rkey, hidden_post_actor_id, hidden_post_rkey) { - post_actor_id -> Int4, - post_rkey -> Int8, - hidden_post_actor_id -> Int4, - hidden_post_rkey -> Int8, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::ThreadgateRule; - - threadgates (actor_id, rkey) { - actor_id -> Int4, - rkey -> Int8, - cid -> Bytea, - post_actor_id -> Int4, - post_rkey -> Int8, - allow -> Nullable>>, - } -} - diesel::table! { uris (id) { id -> Int4, @@ -685,7 +631,6 @@ 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)); -diesel::joinable!(threadgate_allowed_lists -> lists (list_id)); diesel::allow_tables_to_appear_in_same_query!( _diesel_schema_inference, @@ -711,17 +656,12 @@ diesel::allow_tables_to_appear_in_same_query!( mutes, notifications, post_aggregate_stats, - postgate_detached, - postgates, posts, reposts, spatial_ref_sys, starterpack_feeds, starterpacks, thread_mutes, - threadgate_allowed_lists, - threadgate_hidden_replies, - threadgates, uris, verification, ); diff --git a/parakeet/src/db/posts.rs b/parakeet/src/db/posts.rs index ae7906f3..0fe7b319 100644 --- a/parakeet/src/db/posts.rs +++ b/parakeet/src/db/posts.rs @@ -116,17 +116,16 @@ pub async fn get_threadgate_hiddens( Err(_) => return Ok(None), }; - // Parse URI to get post, then query threadgate_hidden_replies junction table - // threadgates PK is (post_actor_id, post_rkey), threadgate_hidden_replies uses natural keys + // DENORMALIZED: Read hidden replies from posts.threadgate_hidden_actor_ids/rkeys arrays let hidden_rows: Vec = diesel_async::RunQueryDsl::load( diesel::sql_query( - "SELECT a.did, thr.hidden_post_rkey as rkey - FROM actors tg_actor - INNER JOIN threadgates tg ON tg.post_actor_id = (SELECT id FROM actors WHERE did = $1) AND tg.post_rkey = $2 - INNER JOIN threadgate_hidden_replies thr ON thr.post_actor_id = tg.post_actor_id AND thr.post_rkey = tg.post_rkey - INNER JOIN actors a ON a.id = thr.hidden_post_actor_id - INNER JOIN posts p ON p.actor_id = thr.hidden_post_actor_id AND p.rkey = thr.hidden_post_rkey - WHERE tg_actor.did = $1 AND p.status = 'complete'" + "SELECT a.did, hidden_rkey as rkey + FROM actors post_actor + INNER JOIN posts p ON p.actor_id = post_actor.id + CROSS JOIN unnest(p.threadgate_hidden_actor_ids, p.threadgate_hidden_rkeys) AS hidden(hidden_actor_id, hidden_rkey) + INNER JOIN actors a ON a.id = hidden.hidden_actor_id + INNER JOIN posts hidden_post ON hidden_post.actor_id = hidden.hidden_actor_id AND hidden_post.rkey = hidden.hidden_rkey + WHERE post_actor.did = $1 AND p.rkey = $2 AND hidden_post.status = 'complete'" ) .bind::(tg_did) .bind::(tg_rkey), diff --git a/parakeet/src/sql/post_state.sql b/parakeet/src/sql/post_state.sql index 3ad5431e..e0172a86 100644 --- a/parakeet/src/sql/post_state.sql +++ b/parakeet/src/sql/post_state.sql @@ -79,7 +79,8 @@ SELECT vl.like_rkey, vr.repost_rkey, CASE WHEN vb.post_actor_id IS NOT NULL THEN true ELSE false END as bookmarked, - COALESCE(pg.rules && ARRAY['app.bsky.feed.postgate#disableRule']::postgate_rule[], false) as embed_disabled, + -- DENORMALIZED: Read postgate_rules from posts table instead of separate postgates table + COALESCE(p.postgate_rules && ARRAY['app.bsky.feed.postgate#disableRule']::postgate_rule[], false) as embed_disabled, -- Only check pinned when viewing own posts (tp.actor_id = viewer_id) COALESCE(tp.actor_id = v.viewer_id AND tp.rkey = vp.pinned_post_rkey, false) as pinned FROM target_posts tp @@ -88,4 +89,5 @@ LEFT JOIN viewer_pinned vp ON tp.actor_id = $1 -- Only join when viewing own po LEFT JOIN viewer_likes vl ON vl.post_actor_id = tp.actor_id AND vl.post_rkey = tp.rkey LEFT JOIN viewer_reposts vr ON vr.post_actor_id = tp.actor_id AND vr.post_rkey = tp.rkey LEFT JOIN viewer_bookmarks vb ON vb.post_actor_id = tp.actor_id AND vb.post_rkey = tp.rkey -LEFT JOIN postgates pg ON pg.post_actor_id = tp.actor_id AND pg.post_rkey = tp.rkey; +-- JOIN posts to access denormalized postgate_rules column +LEFT JOIN posts p ON p.actor_id = tp.actor_id AND p.rkey = tp.rkey; diff --git a/parakeet/src/sql/thread_v2_hidden_children.sql b/parakeet/src/sql/thread_v2_hidden_children.sql index 20d05823..a30a6ced 100644 --- a/parakeet/src/sql/thread_v2_hidden_children.sql +++ b/parakeet/src/sql/thread_v2_hidden_children.sql @@ -1,21 +1,21 @@ --- Find hidden replies to a post (OPTIMIZED: uses actor_id instead of DID) +-- Find hidden replies to a post (DENORMALIZED: reads from posts.threadgate_hidden_* arrays) -- $1 = parent post actor_id (integer) -- $2 = parent post rkey (bigint) -- $3 = root post actor_id (integer) -- $4 = root post rkey (bigint) -- -- PERFORMANCE: Avoids ALL actors table joins by using actor_ids directly +-- DENORMALIZED: Checks denormalized threadgate_hidden_actor_ids/rkeys arrays on root post SELECT p.actor_id, p.rkey FROM posts p +-- Join to root post to access its threadgate arrays +INNER JOIN posts root ON root.actor_id = $3 AND root.rkey = $4 WHERE p.parent_post_actor_id = $1 AND p.parent_post_rkey = $2 - -- Check if this post is in the hidden replies junction table + -- Check if this reply is in the root post's hidden replies arrays AND EXISTS ( SELECT 1 - FROM threadgate_hidden_replies thr - WHERE thr.hidden_post_actor_id = p.actor_id - AND thr.hidden_post_rkey = p.rkey - -- Match the threadgate for the root post - AND thr.post_actor_id = $3 - AND thr.post_rkey = $4 + FROM unnest(root.threadgate_hidden_actor_ids, root.threadgate_hidden_rkeys) AS hidden(actor_id, rkey) + WHERE hidden.actor_id = p.actor_id + AND hidden.rkey = p.rkey ) -- 2.51.2