diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs --- a/parakeet-db/src/schema.rs +++ b/parakeet-db/src/schema.rs @@ -276,6 +276,8 @@ post_rkeys -> Nullable>>, repost_rkeys -> Nullable>>, labels -> Nullable>>, + labeler_like_actor_ids -> Nullable>>, + labeler_like_rkeys -> Nullable>>, } } diff --git a/parakeet/src/db.rs b/parakeet/src/db.rs --- a/parakeet/src/db.rs +++ b/parakeet/src/db.rs @@ -38,7 +38,7 @@ pub use feedgens::{get_actor_feedgens, get_all_feedgen_uris, get_feedgen_service_did}; pub use graph::{ get_actor_followers, get_actor_follows, get_actor_lists, get_followed_by_batch, - get_following_batch, get_list_id_by_uri, get_list_items, get_mutual_followers, + get_following_batch, get_list_items, get_mutual_followers, get_user_blocks, get_user_list_blocks, get_user_list_mutes, get_user_mutes, }; pub use likes::{get_actor_likes, get_like_state, get_like_states, get_post_likes}; diff --git a/parakeet/src/db/feeds.rs b/parakeet/src/db/feeds.rs --- a/parakeet/src/db/feeds.rs +++ b/parakeet/src/db/feeds.rs @@ -552,43 +552,43 @@ micros << 10 }); - // Step 1: Get list_id from (list_owner_actor_id, list_rkey_text) - // This is a simple lookup on lists table - no JOIN needed + // Step 1: Verify list exists (lists now use natural keys: actor_id, rkey) #[derive(QueryableByName)] - struct ListIdRow { - #[diesel(sql_type = diesel::sql_types::BigInt)] - id: i64, + struct ListExists { + #[diesel(sql_type = diesel::sql_types::Integer)] + exists: i32, } - let list_id_result = diesel::sql_query( - "SELECT id FROM lists WHERE actor_id = $1 AND rkey = $2" + let list_exists = diesel::sql_query( + "SELECT 1 as exists FROM lists WHERE actor_id = $1 AND rkey = $2 LIMIT 1" ) .bind::(list_owner_actor_id) .bind::(list_rkey_text) - .get_result::(conn) + .get_result::(conn) .await; - let list_id = match list_id_result { - Ok(row) => row.id, - Err(_) => return Ok(Vec::new()), // List not found - }; + if list_exists.is_err() { + return Ok(Vec::new()); // List not found + } // Step 2: Query posts by list members (0 actors JOINs!) // Pure actor_id operations - avoids decompressing actors table + // Note: list_items now uses natural keys (list_owner_actor_id, list_rkey) instead of list_id FK let results = diesel::sql_query( "SELECT p.actor_id, p.rkey FROM posts p WHERE p.actor_id IN ( SELECT li.subject_actor_id FROM list_items li - WHERE li.list_id = $1 + WHERE li.list_owner_actor_id = $1 AND li.list_rkey = $2 ) AND p.status = 'complete' - AND ($2::bigint IS NULL OR p.rkey < $2) + AND ($3::bigint IS NULL OR p.rkey < $3) ORDER BY p.rkey DESC - LIMIT $3" + LIMIT $4" ) - .bind::(list_id) + .bind::(list_owner_actor_id) + .bind::(list_rkey_text) .bind::, _>(cursor_rkey) .bind::(i64::from(limit)) .load::(conn) diff --git a/parakeet/src/db/graph.rs b/parakeet/src/db/graph.rs --- a/parakeet/src/db/graph.rs +++ b/parakeet/src/db/graph.rs @@ -44,31 +44,9 @@ }) } -/// Get list ID by DID and rkey -/// -/// Returns the internal list ID -pub async fn get_list_id_by_uri( - conn: &mut AsyncPgConnection, - list_owner_actor_id: i32, - rkey: &str, -) -> QueryResult { - #[derive(QueryableByName)] - struct ListIdRow { - #[diesel(sql_type = diesel::sql_types::BigInt)] - id: i64, - } - - diesel::sql_query( - "SELECT l.id FROM lists l - WHERE l.actor_id = $1 - AND l.rkey::text = $2" - ) - .bind::(list_owner_actor_id) - .bind::(rkey) - .get_result::(conn) - .await - .map(|r| r.id) -} +/// REMOVED: Lists no longer have numeric IDs +/// Lists now use natural keys (actor_id, rkey) as their primary key. +/// If you need to verify a list exists, check (actor_id, rkey) directly. /// Get blocked accounts for a user with cursor pagination /// diff --git a/consumer/src/db/operations/feed.rs b/consumer/src/db/operations/feed.rs --- a/consumer/src/db/operations/feed.rs +++ b/consumer/src/db/operations/feed.rs @@ -30,7 +30,7 @@ // Re-export all public functions for external use pub use feedgen::{feedgen_delete, feedgen_upsert, decrement_feedgen_like_count}; -pub use helpers::{ensure_list_id, ensure_list_natural_key, get_actor_id}; +pub use helpers::{ensure_list_natural_key, get_actor_id}; pub use like::{like_delete, like_insert, LikeSubject}; pub use post::{post_delete, post_insert}; pub use post_update::{PostUpdate, PostUpdateTarget, PostUpdateReturning, PostUpdateResult, LikeArrayOp, RecordDetachedOp}; diff --git a/consumer/src/db/operations/feed/helpers.rs b/consumer/src/db/operations/feed/helpers.rs --- a/consumer/src/db/operations/feed/helpers.rs +++ b/consumer/src/db/operations/feed/helpers.rs @@ -138,15 +138,6 @@ Ok(((actor_id, rkey.to_string()), was_created)) } -/// Get list ID by AT URI, creating a stub if it doesn't exist (returns just ID) -/// -/// This is a convenience wrapper around `get_list_id` that returns just the list ID. -/// **DEPRECATED**: Use `ensure_list_natural_key` instead for new code. -pub async fn ensure_list_id(conn: &C, at_uri: &str) -> Result { - let (list_id, _) = get_list_id(conn, at_uri).await?; - Ok(list_id) -} - /// Get list natural key by AT URI, creating a stub if it doesn't exist /// /// Returns (Option, Option) tuple. Both are None if at_uri is empty/missing. @@ -183,65 +174,6 @@ .await?; Ok((Some(actor_id), Some(rkey.to_string()))) -} - -/// Get list ID by AT URI, creating a stub if it doesn't exist -/// -/// This function: -/// 1. Extracts owner DID and rkey from AT URI -/// 2. Gets/creates owner actor_id -/// 3. Looks up existing list or creates stub if not found (status='stub') -/// 4. Returns (list_id, was_created) where was_created indicates if a new stub was created -/// -/// Uses advisory locks to prevent concurrent transactions from racing on the same list URI. -/// -/// For stubs, owner_actor_id is set equal to actor_id, and list_type/name are NULL. -/// The stub uses a zero CID as placeholder until the actual list is fetched. -/// The actual list data comes from the full list record when fetched. -pub(super) async fn get_list_id(conn: &C, at_uri: &str) -> Result<(i64, bool)> { - // Acquire advisory lock on list URI to prevent concurrent access races - let (table_id, key_id) = crate::database_writer::locking::table_record_lock("lists", at_uri); - crate::database_writer::locking::acquire_lock(conn, table_id, key_id).await?; - - // Extract owner DID and rkey from AT URI - let did = parakeet_db::at_uri_util::extract_did(at_uri) - .ok_or_else(|| eyre::eyre!("Invalid AT URI: missing DID in {}", at_uri))?; - let rkey = parakeet_db::at_uri_util::extract_rkey(at_uri) - .ok_or_else(|| eyre::eyre!("Invalid AT URI: missing rkey in {}", at_uri))?; - - // Lists can use both TID rkeys (like "3jzfcijpj2z2a") and arbitrary string rkeys (like "nfb", "bblock") - // We support both now that lists.rkey is text - - // Get/create actor_id for owner - let (actor_id, _, _) = get_actor_id(conn, did).await?; - - // Use CTE to SELECT first, then conditionally INSERT only if not found - // This prevents unnecessary sequence consumption when list stub already exists - // Still uses ON CONFLICT for race condition safety between concurrent transactions - // For stubs, owner_actor_id equals actor_id, list_type and name are NULL - // Use zero CID as placeholder (will be updated when real list is fetched) - // Note: created_at is derived from TID rkey if present, or set to epoch for non-TID rkeys - let zero_cid = vec![0_u8; 32]; - let row = conn - .query_one( - "WITH existing AS ( - SELECT id FROM lists WHERE actor_id = $1 AND rkey = $2 - ), - inserted AS ( - INSERT INTO lists (actor_id, rkey, cid, owner_actor_id, status) - SELECT $1, $2, $3, $1, 'stub'::record_status - WHERE NOT EXISTS (SELECT 1 FROM existing) - ON CONFLICT (actor_id, rkey) DO UPDATE SET actor_id = EXCLUDED.actor_id - RETURNING id - ) - SELECT - COALESCE((SELECT id FROM existing), (SELECT id FROM inserted)) as id, - (SELECT id FROM existing) IS NULL as was_created", - &[&actor_id, &rkey, &zero_cid], - ) - .await?; - - Ok((row.get(0), row.get(1))) } /// Get a repost natural key by actor_id and rkey