diff --git a/parakeet/src/db/actors.rs b/parakeet/src/db/actors.rs index 13d07ce8..1ddedde6 100644 --- a/parakeet/src/db/actors.rs +++ b/parakeet/src/db/actors.rs @@ -5,7 +5,6 @@ use diesel::sql_types::Text; use diesel_async::{AsyncPgConnection, RunQueryDsl}; use parakeet_db::{schema, types}; -/// Resolved actor information from unified resolution #[derive(Debug, Clone)] pub struct ResolvedActor { pub actor_id: i32, @@ -14,28 +13,7 @@ pub struct ResolvedActor { pub status: types::ActorStatus, } -/// Unified actor resolution with intelligent caching -/// -/// Resolves an actor identifier (handle, DID, or actor_id) to full actor data. -/// -/// Priority order for lookups: -/// 1. If numeric actor_id: Check actor_data cache (reverse lookup) -/// 2. If handle/DID: Check actor_ids cache (forward lookup) -/// 3. Database query (handles both handle and DID) -/// 4. If handle not found: Optional Slingshot resolution -/// -/// Populates both forward and reverse caches on database hits. -/// -/// # Arguments -/// * `conn` - Database connection -/// * `actor_identifier` - Can be a handle, DID, or numeric actor_id (as string) -/// * `cache` - Optional id_cache for faster lookups -/// * `slingshot_resolver` - Optional closure to resolve handles via Slingshot -/// -/// # Returns -/// * `Ok(Some(ResolvedActor))` - Actor found and active -/// * `Ok(None)` - Actor not found or not active -/// * `Err(...)` - Database error +/// Resolve actor identifier (handle, DID, or actor_id) using cache and database pub async fn resolve_actor( conn: &mut AsyncPgConnection, actor_identifier: &str, @@ -60,11 +38,9 @@ where status: types::ActorStatus, } - // Step 1: Try to parse as actor_id (numeric) and check reverse cache if let Ok(actor_id) = actor_identifier.parse::() { if let Some(cache) = cache { if let Some(data) = cache.get_actor_data(actor_id).await { - // Cache hit! Query database only for status let status: Option = schema::actors::table .select(schema::actors::status) .filter(schema::actors::id.eq(actor_id)) @@ -81,17 +57,14 @@ where status, })); } - // Actor exists in cache but not active - invalidate cache cache.invalidate_actor_data(actor_id).await; return Ok(None); } } } - // Step 2: Check forward cache (DID or handle) if let Some(cache) = cache { if let Some(cached_actor) = cache.get_actor_id(actor_identifier).await { - // Cache hit! Query database only for full data let actor: Option = diesel::sql_query( "SELECT id, did, handle, status FROM actors @@ -104,7 +77,6 @@ where .optional()?; if let Some(actor) = actor { - // Populate reverse cache cache.set_actor_data( actor.id, parakeet_db::id_cache::CachedActorData { @@ -120,17 +92,14 @@ where status: actor.status, })); } - // Actor exists in cache but not active - invalidate cache cache.invalidate_actor(actor_identifier).await; return Ok(None); } } - // Step 3: Database query (handles both handle and DID) let is_did = actor_identifier.starts_with("did:"); let actor: Option = if is_did { - // Query by DID diesel::sql_query( "SELECT id, did, handle, status FROM actors @@ -142,7 +111,6 @@ where .await .optional()? } else { - // Query by handle diesel::sql_query( "SELECT id, did, handle, status FROM actors @@ -156,16 +124,13 @@ where }; if let Some(actor) = actor { - // Populate both caches if let Some(cache) = cache { - // Forward cache: DID → actor_id cache.set_actor_id_with_allowlist( actor.did.clone(), actor.id, - false, // Parakeet doesn't track allowlist + false, ).await; - // Forward cache: handle → actor_id (if handle exists) if let Some(ref handle) = actor.handle { cache.set_actor_id_with_allowlist( handle.clone(), @@ -174,7 +139,6 @@ where ).await; } - // Reverse cache: actor_id → (did, handle) cache.set_actor_data( actor.id, parakeet_db::id_cache::CachedActorData { @@ -192,18 +156,14 @@ where })); } - // Step 4: If it's a handle and not found, try Slingshot if !is_did { if let Some(resolver) = slingshot_resolver { if let Some(resolved_did) = resolver(actor_identifier.to_string()).await { - // Recursively resolve by DID (will hit cache or DB) - // Box::pin required for recursive async fn return Box::pin(resolve_actor(conn, &resolved_did, cache, None:: futures::future::Ready>>)).await; } } } - // Not found Ok(None) } @@ -221,16 +181,7 @@ pub async fn get_actor_status( .optional() } -/// Batch lookup actor IDs from DIDs -/// -/// Returns HashMap mapping DID to internal actor ID -/// Only returns entries for actors with status = 'active' -/// -/// If `cache` is provided, will: -/// 1. Check cache for all DIDs first -/// 2. Query database only for cache misses -/// 3. Populate cache with new results -/// 4. Return combined results (cache hits + DB results) +/// Batch lookup actor IDs from DIDs using cache and database pub async fn get_actor_ids_by_dids( conn: &mut AsyncPgConnection, dids: &[String], @@ -250,7 +201,6 @@ pub async fn get_actor_ids_by_dids( return Ok(std::collections::HashMap::new()); } - // Check cache first if provided let (mut result, dids_to_query) = if let Some(cache) = cache { let cached = cache.get_actor_ids(dids).await; let misses: Vec = dids @@ -258,7 +208,6 @@ pub async fn get_actor_ids_by_dids( .filter(|did| !cached.contains_key(*did)) .cloned() .collect(); - // Extract just actor_ids for the return value let cached_ids: std::collections::HashMap = cached .into_iter() .map(|(did, actor)| (did, actor.actor_id)) @@ -268,7 +217,6 @@ pub async fn get_actor_ids_by_dids( (std::collections::HashMap::new(), dids.to_vec()) }; - // If all DIDs were in cache, return early if dids_to_query.is_empty() { return Ok(result); } @@ -285,21 +233,17 @@ pub async fn get_actor_ids_by_dids( .load(conn) .await?; - // Convert results let db_results: std::collections::HashMap = results .into_iter() .map(|row| (row.did, row.actor_id)) .collect(); - // Populate cache with new results if cache is provided - // Note: Parakeet doesn't track allowlist status, so we default to false if let Some(cache) = cache { for (did, actor_id) in &db_results { cache.set_actor_id_with_allowlist(did.clone(), *actor_id, false).await; } } - // Merge cache hits with database results result.extend(db_results); Ok(result) diff --git a/parakeet/src/hydration/posts/mod.rs b/parakeet/src/hydration/posts/mod.rs index 756980e8..7b5e88d8 100644 --- a/parakeet/src/hydration/posts/mod.rs +++ b/parakeet/src/hydration/posts/mod.rs @@ -8,7 +8,6 @@ use lexica::app_bsky::feed::{PostView, PostViewerState, ThreadgateView}; use builders::{build_postview, build_threadgate_view, HydratePostsRet}; -// Re-export RawFeedItem for public use pub use feed::RawFeedItem; use crate::hydration::StatefulHydrator; @@ -16,7 +15,6 @@ use crate::loaders::EnrichedThreadgate; impl StatefulHydrator<'_> { /// Convert denormalized post labels to full Label models - /// This resolves labeler_actor_id to DID and constructs URIs async fn convert_post_labels( &self, post_uri: &str, @@ -26,13 +24,9 @@ impl StatefulHydrator<'_> { return vec![]; } - // Collect unique labeler actor IDs let labeler_ids: Vec = labels.iter().map(|l| l.labeler_actor_id).collect(); - - // Batch resolve labeler_actor_id → DID using IdCache let labeler_data = self.loaders.post_state.id_cache().get_actor_data_many(&labeler_ids).await; - // Convert each label labels.iter().filter_map(|record| { let labeler_did = labeler_data.get(&record.labeler_actor_id)?.did.clone(); @@ -40,11 +34,11 @@ impl StatefulHydrator<'_> { labeler_actor_id: record.labeler_actor_id, label: record.label.clone(), uri: post_uri.to_string(), - self_label: false, // Post labels are never self-labels - cid: None, // Not stored in denormalized structure + self_label: false, + cid: None, negated: record.negated, expires: record.expires, - sig: None, // Not stored in denormalized structure + sig: None, created_at: record.created_at, labeler: labeler_did, }) @@ -88,7 +82,6 @@ impl StatefulHydrator<'_> { threadgates .into_iter() .filter_map(|threadgate| { - // Look up post DID from map let post_did = post_did_map.get(&(threadgate.actor_id, threadgate.rkey))?; let this_lists = match &threadgate.allowed_lists { @@ -99,7 +92,6 @@ impl StatefulHydrator<'_> { None => Vec::new(), }; - // Construct threadgate URI for the HashMap key let encoded_rkey = parakeet_db::tid_util::encode_tid(threadgate.rkey); let threadgate_uri = format!("at://{}/app.bsky.feed.threadgate/{}", post_did, encoded_rkey); @@ -111,56 +103,19 @@ impl StatefulHydrator<'_> { .collect() } - pub async fn hydrate_post(&self, post: String) -> Option { - let (post, threadgate, stats) = self.loaders.posts.load(post).await?; - let viewer = self.get_post_viewer_state(&post.post); - let embed = self.hydrate_embed_from_post(&post).await; - let author = self.hydrate_profile_basic(post.did.clone()).await?; - let threadgate = self.hydrate_threadgate(threadgate, &post.did).await; - // OPTIMIZED: Use inline labels from PostLoader instead of separate query - let labels = if let Some(ref label_records) = post.post.labels { - let unwrapped: Vec<_> = label_records.iter().filter_map(|opt| opt.as_ref()).cloned().collect(); - self.convert_post_labels(&post.at_uri, &unwrapped).await - } else { - vec![] - }; - - Some(build_postview(( - post, author, labels, embed, threadgate, viewer, stats, - ), self.loaders.post_state.id_cache())) + /// Hydrate a single post + pub async fn hydrate_post(&self, uri: String) -> Option { + self.hydrate_posts(vec![uri.clone()]).await.remove(&uri) } - /// Hydrate a single post for embed (quote post) - optimized version that skips viewer states - /// - /// See `hydrate_posts_for_embed` for details on what's skipped and why. - pub async fn hydrate_post_for_embed(&self, post_uri: String) -> Option { - let (post, _threadgate, stats) = self.loaders.posts.load(post_uri).await?; - let embed = self.hydrate_embed_from_post(&post).await; - let author = self.hydrate_profile_basic(post.did.clone()).await?; - // OPTIMIZED: Use inline labels from PostLoader instead of separate query - let labels = if let Some(ref label_records) = post.post.labels { - let unwrapped: Vec<_> = label_records.iter().filter_map(|opt| opt.as_ref()).cloned().collect(); - self.convert_post_labels(&post.at_uri, &unwrapped).await - } else { - vec![] - }; - - Some(build_postview(( - post, - author, - labels, - embed, - None, // No threadgate for embeds - None, // No viewer state for embeds - stats, - ), self.loaders.post_state.id_cache())) + pub async fn hydrate_post_for_embed(&self, uri: String) -> Option { + self.hydrate_post(uri).await } pub(super) async fn hydrate_posts_inner( &self, posts: Vec, ) -> HashMap { - // Load posts (now includes stats via gRPC - no separate PostStatsLoader needed!) let load_start = std::time::Instant::now(); let posts_with_stats = self.loaders.posts.load_many(posts).await; let load_time = load_start.elapsed().as_secs_f64() * 1000.0; @@ -169,7 +124,6 @@ impl StatefulHydrator<'_> { tracing::warn!(" → Slow load stats and posts: {:.1} ms", load_time); } - // Extract stats from posts into separate map let stats: HashMap = posts_with_stats .iter() .filter_map(|(uri, (_, _, stats_opt))| { @@ -177,16 +131,14 @@ impl StatefulHydrator<'_> { }) .collect(); - // Extract data for hydration: author IDs and create actor_id→DID mapping let mut actor_id_to_did: HashMap = HashMap::new(); let mut post_key_to_did: HashMap<(i32, i64), String> = HashMap::new(); let (author_ids, post_uris_with_keys): (Vec<_>, Vec<(String, i32, i64, String)>) = posts_with_stats .values() .map(|(post, _, _)| { - let author_id = post.post.actor_id; // Use actor_id for loading - actor_id_to_did.insert(author_id, post.did.clone()); // Map actor_id → DID - post_key_to_did.insert((post.post.actor_id, post.post.rkey), post.did.clone()); // Map (actor_id, rkey) → DID for threadgates - // (uri, actor_id, rkey, post_author_did) + let author_id = post.post.actor_id; + actor_id_to_did.insert(author_id, post.did.clone()); + post_key_to_did.insert((post.post.actor_id, post.post.rkey), post.did.clone()); let uri_with_keys = (post.at_uri.clone(), post.post.actor_id, post.post.rkey, post.did.clone()); (author_id, uri_with_keys) }) @@ -197,28 +149,23 @@ impl StatefulHydrator<'_> { .filter_map(|(_, threadgate, _)| threadgate.clone()) .collect::>(); - // Parallelize: hydrate all independent data concurrently (except embeds which needs post data) let hydrate_start = std::time::Instant::now(); - // Capture counts before moving let author_count = author_ids.len(); let post_uri_count = post_uris_with_keys.len(); let threadgate_count = threadgates_to_hydrate.len(); - // Extract just URIs for labels (doesn't need natural keys or author DIDs) let post_uris: Vec = post_uris_with_keys.iter().map(|(uri, _, _, _)| uri.clone()).collect(); - // Time each operation individually to identify bottlenecks let profiles_future = async move { let start = std::time::Instant::now(); - let profiles_by_id = self.hydrate_profiles_by_id(author_ids).await; // Use by_id variant + let profiles_by_id = self.hydrate_profiles_by_id(author_ids).await; let elapsed = start.elapsed().as_secs_f64() * 1000.0; tracing::info!(" → Profiles: {:.1} ms ({} actors)", elapsed, author_count); if elapsed > 20.0 { tracing::warn!(" → Slow profiles: {:.1} ms", elapsed); } - // Convert HashMap to HashMap profiles_by_id .into_iter() .filter_map(|(actor_id, profile)| { @@ -264,7 +211,6 @@ impl StatefulHydrator<'_> { threadgates_future ); - // Build embeds from post data (no DB query needed since PostLoader already fetched embed fields) let embeds_start = std::time::Instant::now(); let mut embeds = self.hydrate_embeds_from_posts(&posts_with_stats).await; let embeds_time = embeds_start.elapsed().as_secs_f64() * 1000.0; @@ -291,7 +237,6 @@ impl StatefulHydrator<'_> { let author = authors.get(&post.did)?.clone(); let embed = embeds.remove(&uri); let threadgate = threadgate.and_then(|tg| { - // Construct threadgate URI from natural keys for lookup let encoded_rkey = parakeet_db::tid_util::encode_tid(tg.rkey); let threadgate_uri = format!("at://{}/app.bsky.feed.threadgate/{}", post.did, encoded_rkey); threadgates.get(&threadgate_uri).cloned() @@ -316,122 +261,14 @@ impl StatefulHydrator<'_> { .collect() } - /// Hydrate posts for embeds (quote posts) - optimized version that skips expensive viewer-specific data - /// - /// This is a faster version of `hydrate_posts` specifically for quoted posts in embeds. - /// It skips: - /// - Viewer states (likes, reposts, bookmarks) - not shown in embeds - /// - Threadgates - not relevant for embedded posts - /// - /// It still loads: - /// - Post data and stats (like/repost counts) - required by RecordView schema - /// - Author profiles - required by RecordView schema - /// - Labels - needed for moderation - /// - Nested embeds - needed if the quoted post has images/videos - /// - /// Expected performance: Saves 40-80ms per quote post by skipping viewer states query pub async fn hydrate_posts_for_embed(&self, posts: Vec) -> HashMap { - if posts.is_empty() { - return HashMap::new(); - } - - // Load posts (now includes stats via gRPC - no separate PostStatsLoader needed!) - let load_start = std::time::Instant::now(); - let posts_with_stats = self.loaders.posts.load_many(posts).await; - let load_time = load_start.elapsed().as_secs_f64() * 1000.0; - tracing::debug!(" → [Embed] Load posts + stats: {:.1} ms ({} posts)", load_time, posts_with_stats.len()); - - // Extract stats from posts into separate map - let stats: HashMap = posts_with_stats - .iter() - .filter_map(|(uri, (_, _, stats_opt))| { - stats_opt.map(|stats| (uri.clone(), stats)) - }) - .collect(); - - // Extract author IDs and create mapping to DIDs - let mut actor_id_to_did: HashMap = HashMap::new(); - let (author_ids, post_uris) = posts_with_stats - .values() - .map(|(post, _, _)| { - actor_id_to_did.insert(post.post.actor_id, post.did.clone()); - (post.post.actor_id, post.at_uri.clone()) - }) - .unzip::<_, _, Vec<_>, Vec<_>>(); - - let author_count = author_ids.len(); - let post_uri_count = post_uris.len(); - - // Parallelize: hydrate authors and labels (skip viewer states and threadgates!) - let hydrate_start = std::time::Instant::now(); - - let profiles_future = async move { - let start = std::time::Instant::now(); - let profiles_by_id = self.hydrate_profiles_by_id(author_ids).await; // Use by_id variant - let elapsed = start.elapsed().as_secs_f64() * 1000.0; - tracing::debug!(" → [Embed] Profiles: {:.1} ms ({} actors)", elapsed, author_count); - - // Convert HashMap to HashMap - profiles_by_id - .into_iter() - .filter_map(|(actor_id, profile)| { - actor_id_to_did.get(&actor_id).map(|did| (did.clone(), profile)) - }) - .collect::>() - }; - - let labels_future = async { - let start = std::time::Instant::now(); - let result = self.get_label_many_by_actor_ids(&post_uris).await; - let elapsed = start.elapsed().as_secs_f64() * 1000.0; - tracing::debug!(" → [Embed] Labels: {:.1} ms ({} posts)", elapsed, post_uri_count); - result - }; - - let (authors, mut post_labels) = tokio::join!( - profiles_future, - labels_future - ); - - let hydrate_time = hydrate_start.elapsed().as_secs_f64() * 1000.0; - tracing::debug!(" → [Embed] Hydration (authors/labels): {:.1} ms", hydrate_time); - - // Build embeds from post data - let embeds_start = std::time::Instant::now(); - let mut embeds = self.hydrate_embeds_from_posts(&posts_with_stats).await; - let embeds_time = embeds_start.elapsed().as_secs_f64() * 1000.0; - tracing::debug!(" → [Embed] Embeds: {:.1} ms", embeds_time); - - // Build PostViews without viewer states or threadgates - posts_with_stats - .into_iter() - .filter_map(|(uri, (post, _threadgate, _stats))| { - let author = authors.get(&post.did)?.clone(); - let embed = embeds.remove(&uri); - let labels = post_labels.remove(&uri).unwrap_or_default(); - let post_stats = stats.get(&uri).copied(); - - Some(( - uri, - build_postview(( - post, - author, - labels, - embed, - None, // No threadgate for embeds - None, // No viewer state for embeds - post_stats, - ), self.loaders.post_state.id_cache()), - )) - }) - .collect() + self.hydrate_posts(posts).await } fn get_post_viewer_state(&self, post: ¶keet_db::models::Post) -> Option { let viewer_cache = self.viewer_cache.as_ref()?; let viewer_did = self.current_actor.as_ref()?; - // Check if viewer liked this post (array lookup in Rust) let like_rkey = post.like_actor_ids .as_ref() .and_then(|actor_ids| { @@ -441,7 +278,6 @@ impl StatefulHydrator<'_> { post.like_rkeys.as_ref()?.get(pos)?.as_ref().copied() }); - // Check if viewer reposted this post (array lookup in Rust) let repost_rkey = post.repost_actor_ids .as_ref() .and_then(|actor_ids| { @@ -451,12 +287,10 @@ impl StatefulHydrator<'_> { post.repost_rkeys.as_ref()?.get(pos)?.as_ref().copied() }); - // Check if viewer bookmarked this post (cached array lookup) let bookmarked = viewer_cache.bookmarks .iter() .any(|b| b.post_actor_id == post.actor_id && b.post_rkey == post.rkey); - // Check if embed is disabled (postgate rule check in Rust) let embed_disabled = post.postgate_rules .as_ref() .map(|rules| { @@ -466,13 +300,11 @@ impl StatefulHydrator<'_> { }) .unwrap_or(false); - // Check if pinned (only applies to viewer's own posts) let pinned = post.actor_id == viewer_cache.actor_id && viewer_cache.pinned_post_rkey == Some(post.rkey); let is_me = viewer_cache.actor_id == post.actor_id; - // Build viewer state (same as build_viewer, but inlined to avoid data structure conversion) let repost = repost_rkey.map(|rkey| { let encoded_rkey = parakeet_db::tid_util::encode_tid(rkey); format!("at://{viewer_did}/app.bsky.feed.repost/{encoded_rkey}") @@ -486,9 +318,9 @@ impl StatefulHydrator<'_> { repost, like, bookmarked, - thread_muted: false, // todo when we have thread mutes + thread_muted: false, reply_disabled: false, - embedding_disabled: embed_disabled && !is_me, // poster can always bypass embed disabled + embedding_disabled: embed_disabled && !is_me, pinned, }) } diff --git a/parakeet/tests/db_feedgens_test.rs b/parakeet/tests/db_feedgens_test.rs index 216b31ce..68b484bb 100644 --- a/parakeet/tests/db_feedgens_test.rs +++ b/parakeet/tests/db_feedgens_test.rs @@ -68,6 +68,7 @@ async fn test_get_feedgen_service_did() -> eyre::Result<()> { let result = parakeet::db::get_feedgen_service_did( &mut conn, "at://did:plc:test/app.bsky.feed.generator/abc123", + None, ) .await; diff --git a/parakeet/tests/db_graph_test.rs b/parakeet/tests/db_graph_test.rs index 2a67298c..2d8aef97 100644 --- a/parakeet/tests/db_graph_test.rs +++ b/parakeet/tests/db_graph_test.rs @@ -250,28 +250,6 @@ async fn test_get_mutual_followers_with_cursor() -> eyre::Result<()> { Ok(()) } -// ============================================================================ -// LIST QUERY TESTS -// ============================================================================ - -#[tokio::test] -async fn test_get_list_id_by_uri() -> eyre::Result<()> { - common::ensure_test_db_ready().await; - let pool = common::test_diesel_pool(); - let mut conn = pool.get().await.wrap_err("Failed to get connection")?; - - let result = parakeet::db::get_list_id_by_uri(&mut conn, 1, "abc123").await; - - // Query should execute without SQL syntax errors (NotFound is ok) - assert!( - result.is_ok() || result.is_err(), - "Query should execute: {:?}", - result - ); - - Ok(()) -} - #[tokio::test] async fn test_get_actor_lists() -> eyre::Result<()> { common::ensure_test_db_ready().await; diff --git a/parakeet/tests/db_states_test.rs b/parakeet/tests/db_states_test.rs index 4193075a..22d8b637 100644 --- a/parakeet/tests/db_states_test.rs +++ b/parakeet/tests/db_states_test.rs @@ -28,48 +28,6 @@ async fn test_get_profile_states() -> eyre::Result<()> { Ok(()) } -// ============================================================================ -// POST STATE TESTS -// ============================================================================ - -#[tokio::test] -async fn test_get_post_state() -> eyre::Result<()> { - common::ensure_test_db_ready().await; - let pool = common::test_diesel_pool(); - let mut conn = pool.get().await.wrap_err("Failed to get connection")?; - - // Use dummy IDs for SQL syntax validation (doesn't need to exist) - let _result = parakeet::db::get_post_state( - &mut conn, - 999, // viewer_id - 888, // subject_actor_id - 1234567890, // subject_rkey - ) - .await - .wrap_err("SQL syntax error in get_post_state")?; - - Ok(()) -} - -#[tokio::test] -async fn test_get_post_states() -> eyre::Result<()> { - common::ensure_test_db_ready().await; - let pool = common::test_diesel_pool(); - let mut conn = pool.get().await.wrap_err("Failed to get connection")?; - - // get_post_states expects (uri, actor_id, rkey) tuples - let subjects = vec![ - ("at://did:plc:test/app.bsky.feed.post/3ktpjpifdsr2d".to_string(), 888, 1234567890), - ]; - - // Use dummy viewer_id for SQL syntax validation (doesn't need to exist) - let _result = parakeet::db::get_post_states(&mut conn, 999, &subjects) - .await - .wrap_err("SQL syntax error in get_post_states")?; - - Ok(()) -} - // ============================================================================ // LIST STATE TESTS // ============================================================================ diff --git a/parakeet/tests/sql/db_module_test.rs b/parakeet/tests/sql/db_module_test.rs index bb2668ce..c5dc07c6 100644 --- a/parakeet/tests/sql/db_module_test.rs +++ b/parakeet/tests/sql/db_module_test.rs @@ -81,55 +81,6 @@ async fn test_get_profile_states_batch() -> eyre::Result<()> { Ok(()) } -// ============================================================================ -// POST STATE TESTS (External SQL: post_state.sql) -// ============================================================================ - -#[tokio::test] -async fn test_get_post_state_empty() -> eyre::Result<()> { - common::ensure_test_db_ready().await; - let pool = common::test_diesel_pool(); - let mut conn = pool.get().await - .wrap_err("Failed to get connection")?; - - // Use dummy IDs for SQL syntax validation (doesn't need to exist) - let result = db::get_post_state( - &mut conn, - 999, // viewer_id - 888, // subject_actor_id - 1234567890 // subject_rkey - ).await; - - let result = result.wrap_err("SQL query failed")?; - - Ok(()) -} - -#[tokio::test] -async fn test_get_post_states_empty() -> eyre::Result<()> { - common::ensure_test_db_ready().await; - let pool = common::test_diesel_pool(); - let mut conn = pool.get().await - .wrap_err("Failed to get connection")?; - - // get_post_states expects (uri, actor_id, rkey) tuples - let subjects = vec![ - ("at://did:plc:author/app.bsky.feed.post/3ktpjpifdsr2d".to_string(), 888, 1234567890), - ("at://did:plc:author/app.bsky.feed.post/3kvqomklhtx2d".to_string(), 888, 1234567891), - ]; - - // Use dummy viewer_id for SQL syntax validation (doesn't need to exist) - let result = db::get_post_states( - &mut conn, - 999, - &subjects - ).await; - - let result = result.wrap_err("SQL query failed")?; - - Ok(()) -} - // ============================================================================ // LIST STATE TESTS (External SQL: list_states.sql) // ============================================================================ @@ -805,7 +756,8 @@ async fn test_get_feedgen_service_did_nonexistent() -> eyre::Result<()> { // Query for a non-existent feedgen (should still validate SQL) let result = db::get_feedgen_service_did( &mut conn, - "at://did:plc:nonexistent/app.bsky.feed.generator/testfeed" + "at://did:plc:nonexistent/app.bsky.feed.generator/testfeed", + None, ).await; // Should fail with NotFound (no rows), but SQL should be valid @@ -1258,26 +1210,6 @@ async fn test_get_user_mutes_with_cursor() -> eyre::Result<()> { Ok(()) } -#[tokio::test] -async fn test_get_list_id_by_uri_nonexistent() -> eyre::Result<()> { - common::ensure_test_db_ready().await; - let pool = common::test_diesel_pool(); - let mut conn = pool.get().await - .wrap_err("Failed to get connection")?; - - // Query for non-existent list - let result = db::get_list_id_by_uri( - &mut conn, - 999, - "3ktpjpifdsr2d" - ).await; - - // Should fail for non-existent list (NotFound error, not SQL syntax error) - assert!(result.is_err(), "Should fail for non-existent list"); - - Ok(()) -} - #[tokio::test] async fn test_get_user_blocks_empty() -> eyre::Result<()> { common::ensure_test_db_ready().await;