Rust AppView - highly experimental!
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: cleanup post hydration

+21 -376
+3 -59
parakeet/src/db/actors.rs
··· 5 5 use diesel_async::{AsyncPgConnection, RunQueryDsl}; 6 6 use parakeet_db::{schema, types}; 7 7 8 - /// Resolved actor information from unified resolution 9 8 #[derive(Debug, Clone)] 10 9 pub struct ResolvedActor { 11 10 pub actor_id: i32, ··· 14 13 pub status: types::ActorStatus, 15 14 } 16 15 17 - /// Unified actor resolution with intelligent caching 18 - /// 19 - /// Resolves an actor identifier (handle, DID, or actor_id) to full actor data. 20 - /// 21 - /// Priority order for lookups: 22 - /// 1. If numeric actor_id: Check actor_data cache (reverse lookup) 23 - /// 2. If handle/DID: Check actor_ids cache (forward lookup) 24 - /// 3. Database query (handles both handle and DID) 25 - /// 4. If handle not found: Optional Slingshot resolution 26 - /// 27 - /// Populates both forward and reverse caches on database hits. 28 - /// 29 - /// # Arguments 30 - /// * `conn` - Database connection 31 - /// * `actor_identifier` - Can be a handle, DID, or numeric actor_id (as string) 32 - /// * `cache` - Optional id_cache for faster lookups 33 - /// * `slingshot_resolver` - Optional closure to resolve handles via Slingshot 34 - /// 35 - /// # Returns 36 - /// * `Ok(Some(ResolvedActor))` - Actor found and active 37 - /// * `Ok(None)` - Actor not found or not active 38 - /// * `Err(...)` - Database error 16 + /// Resolve actor identifier (handle, DID, or actor_id) using cache and database 39 17 pub async fn resolve_actor<F, Fut>( 40 18 conn: &mut AsyncPgConnection, 41 19 actor_identifier: &str, ··· 60 38 status: types::ActorStatus, 61 39 } 62 40 63 - // Step 1: Try to parse as actor_id (numeric) and check reverse cache 64 41 if let Ok(actor_id) = actor_identifier.parse::<i32>() { 65 42 if let Some(cache) = cache { 66 43 if let Some(data) = cache.get_actor_data(actor_id).await { 67 - // Cache hit! Query database only for status 68 44 let status: Option<types::ActorStatus> = schema::actors::table 69 45 .select(schema::actors::status) 70 46 .filter(schema::actors::id.eq(actor_id)) ··· 81 57 status, 82 58 })); 83 59 } 84 - // Actor exists in cache but not active - invalidate cache 85 60 cache.invalidate_actor_data(actor_id).await; 86 61 return Ok(None); 87 62 } 88 63 } 89 64 } 90 65 91 - // Step 2: Check forward cache (DID or handle) 92 66 if let Some(cache) = cache { 93 67 if let Some(cached_actor) = cache.get_actor_id(actor_identifier).await { 94 - // Cache hit! Query database only for full data 95 68 let actor: Option<ActorRow> = diesel::sql_query( 96 69 "SELECT id, did, handle, status 97 70 FROM actors ··· 104 77 .optional()?; 105 78 106 79 if let Some(actor) = actor { 107 - // Populate reverse cache 108 80 cache.set_actor_data( 109 81 actor.id, 110 82 parakeet_db::id_cache::CachedActorData { ··· 120 92 status: actor.status, 121 93 })); 122 94 } 123 - // Actor exists in cache but not active - invalidate cache 124 95 cache.invalidate_actor(actor_identifier).await; 125 96 return Ok(None); 126 97 } 127 98 } 128 99 129 - // Step 3: Database query (handles both handle and DID) 130 100 let is_did = actor_identifier.starts_with("did:"); 131 101 132 102 let actor: Option<ActorRow> = if is_did { 133 - // Query by DID 134 103 diesel::sql_query( 135 104 "SELECT id, did, handle, status 136 105 FROM actors ··· 142 111 .await 143 112 .optional()? 144 113 } else { 145 - // Query by handle 146 114 diesel::sql_query( 147 115 "SELECT id, did, handle, status 148 116 FROM actors ··· 156 124 }; 157 125 158 126 if let Some(actor) = actor { 159 - // Populate both caches 160 127 if let Some(cache) = cache { 161 - // Forward cache: DID → actor_id 162 128 cache.set_actor_id_with_allowlist( 163 129 actor.did.clone(), 164 130 actor.id, 165 - false, // Parakeet doesn't track allowlist 131 + false, 166 132 ).await; 167 133 168 - // Forward cache: handle → actor_id (if handle exists) 169 134 if let Some(ref handle) = actor.handle { 170 135 cache.set_actor_id_with_allowlist( 171 136 handle.clone(), ··· 174 139 ).await; 175 140 } 176 141 177 - // Reverse cache: actor_id → (did, handle) 178 142 cache.set_actor_data( 179 143 actor.id, 180 144 parakeet_db::id_cache::CachedActorData { ··· 192 156 })); 193 157 } 194 158 195 - // Step 4: If it's a handle and not found, try Slingshot 196 159 if !is_did { 197 160 if let Some(resolver) = slingshot_resolver { 198 161 if let Some(resolved_did) = resolver(actor_identifier.to_string()).await { 199 - // Recursively resolve by DID (will hit cache or DB) 200 - // Box::pin required for recursive async fn 201 162 return Box::pin(resolve_actor(conn, &resolved_did, cache, None::<fn(String) -> futures::future::Ready<Option<String>>>)).await; 202 163 } 203 164 } 204 165 } 205 166 206 - // Not found 207 167 Ok(None) 208 168 } 209 169 ··· 221 181 .optional() 222 182 } 223 183 224 - /// Batch lookup actor IDs from DIDs 225 - /// 226 - /// Returns HashMap mapping DID to internal actor ID 227 - /// Only returns entries for actors with status = 'active' 228 - /// 229 - /// If `cache` is provided, will: 230 - /// 1. Check cache for all DIDs first 231 - /// 2. Query database only for cache misses 232 - /// 3. Populate cache with new results 233 - /// 4. Return combined results (cache hits + DB results) 184 + /// Batch lookup actor IDs from DIDs using cache and database 234 185 pub async fn get_actor_ids_by_dids( 235 186 conn: &mut AsyncPgConnection, 236 187 dids: &[String], ··· 250 201 return Ok(std::collections::HashMap::new()); 251 202 } 252 203 253 - // Check cache first if provided 254 204 let (mut result, dids_to_query) = if let Some(cache) = cache { 255 205 let cached = cache.get_actor_ids(dids).await; 256 206 let misses: Vec<String> = dids ··· 258 208 .filter(|did| !cached.contains_key(*did)) 259 209 .cloned() 260 210 .collect(); 261 - // Extract just actor_ids for the return value 262 211 let cached_ids: std::collections::HashMap<String, i32> = cached 263 212 .into_iter() 264 213 .map(|(did, actor)| (did, actor.actor_id)) ··· 268 217 (std::collections::HashMap::new(), dids.to_vec()) 269 218 }; 270 219 271 - // If all DIDs were in cache, return early 272 220 if dids_to_query.is_empty() { 273 221 return Ok(result); 274 222 } ··· 285 233 .load(conn) 286 234 .await?; 287 235 288 - // Convert results 289 236 let db_results: std::collections::HashMap<String, i32> = results 290 237 .into_iter() 291 238 .map(|row| (row.did, row.actor_id)) 292 239 .collect(); 293 240 294 - // Populate cache with new results if cache is provided 295 - // Note: Parakeet doesn't track allowlist status, so we default to false 296 241 if let Some(cache) = cache { 297 242 for (did, actor_id) in &db_results { 298 243 cache.set_actor_id_with_allowlist(did.clone(), *actor_id, false).await; 299 244 } 300 245 } 301 246 302 - // Merge cache hits with database results 303 247 result.extend(db_results); 304 248 305 249 Ok(result)
+15 -183
parakeet/src/hydration/posts/mod.rs
··· 8 8 9 9 use builders::{build_postview, build_threadgate_view, HydratePostsRet}; 10 10 11 - // Re-export RawFeedItem for public use 12 11 pub use feed::RawFeedItem; 13 12 14 13 use crate::hydration::StatefulHydrator; ··· 16 15 17 16 impl StatefulHydrator<'_> { 18 17 /// Convert denormalized post labels to full Label models 19 - /// This resolves labeler_actor_id to DID and constructs URIs 20 18 async fn convert_post_labels( 21 19 &self, 22 20 post_uri: &str, ··· 26 24 return vec![]; 27 25 } 28 26 29 - // Collect unique labeler actor IDs 30 27 let labeler_ids: Vec<i32> = labels.iter().map(|l| l.labeler_actor_id).collect(); 31 - 32 - // Batch resolve labeler_actor_id → DID using IdCache 33 28 let labeler_data = self.loaders.post_state.id_cache().get_actor_data_many(&labeler_ids).await; 34 29 35 - // Convert each label 36 30 labels.iter().filter_map(|record| { 37 31 let labeler_did = labeler_data.get(&record.labeler_actor_id)?.did.clone(); 38 32 ··· 40 34 labeler_actor_id: record.labeler_actor_id, 41 35 label: record.label.clone(), 42 36 uri: post_uri.to_string(), 43 - self_label: false, // Post labels are never self-labels 44 - cid: None, // Not stored in denormalized structure 37 + self_label: false, 38 + cid: None, 45 39 negated: record.negated, 46 40 expires: record.expires, 47 - sig: None, // Not stored in denormalized structure 41 + sig: None, 48 42 created_at: record.created_at, 49 43 labeler: labeler_did, 50 44 }) ··· 88 82 threadgates 89 83 .into_iter() 90 84 .filter_map(|threadgate| { 91 - // Look up post DID from map 92 85 let post_did = post_did_map.get(&(threadgate.actor_id, threadgate.rkey))?; 93 86 94 87 let this_lists = match &threadgate.allowed_lists { ··· 99 92 None => Vec::new(), 100 93 }; 101 94 102 - // Construct threadgate URI for the HashMap key 103 95 let encoded_rkey = parakeet_db::tid_util::encode_tid(threadgate.rkey); 104 96 let threadgate_uri = format!("at://{}/app.bsky.feed.threadgate/{}", post_did, encoded_rkey); 105 97 ··· 111 103 .collect() 112 104 } 113 105 114 - pub async fn hydrate_post(&self, post: String) -> Option<PostView> { 115 - let (post, threadgate, stats) = self.loaders.posts.load(post).await?; 116 - let viewer = self.get_post_viewer_state(&post.post); 117 - let embed = self.hydrate_embed_from_post(&post).await; 118 - let author = self.hydrate_profile_basic(post.did.clone()).await?; 119 - let threadgate = self.hydrate_threadgate(threadgate, &post.did).await; 120 - // OPTIMIZED: Use inline labels from PostLoader instead of separate query 121 - let labels = if let Some(ref label_records) = post.post.labels { 122 - let unwrapped: Vec<_> = label_records.iter().filter_map(|opt| opt.as_ref()).cloned().collect(); 123 - self.convert_post_labels(&post.at_uri, &unwrapped).await 124 - } else { 125 - vec![] 126 - }; 127 - 128 - Some(build_postview(( 129 - post, author, labels, embed, threadgate, viewer, stats, 130 - ), self.loaders.post_state.id_cache())) 106 + /// Hydrate a single post 107 + pub async fn hydrate_post(&self, uri: String) -> Option<PostView> { 108 + self.hydrate_posts(vec![uri.clone()]).await.remove(&uri) 131 109 } 132 110 133 - /// Hydrate a single post for embed (quote post) - optimized version that skips viewer states 134 - /// 135 - /// See `hydrate_posts_for_embed` for details on what's skipped and why. 136 - pub async fn hydrate_post_for_embed(&self, post_uri: String) -> Option<PostView> { 137 - let (post, _threadgate, stats) = self.loaders.posts.load(post_uri).await?; 138 - let embed = self.hydrate_embed_from_post(&post).await; 139 - let author = self.hydrate_profile_basic(post.did.clone()).await?; 140 - // OPTIMIZED: Use inline labels from PostLoader instead of separate query 141 - let labels = if let Some(ref label_records) = post.post.labels { 142 - let unwrapped: Vec<_> = label_records.iter().filter_map(|opt| opt.as_ref()).cloned().collect(); 143 - self.convert_post_labels(&post.at_uri, &unwrapped).await 144 - } else { 145 - vec![] 146 - }; 147 - 148 - Some(build_postview(( 149 - post, 150 - author, 151 - labels, 152 - embed, 153 - None, // No threadgate for embeds 154 - None, // No viewer state for embeds 155 - stats, 156 - ), self.loaders.post_state.id_cache())) 111 + pub async fn hydrate_post_for_embed(&self, uri: String) -> Option<PostView> { 112 + self.hydrate_post(uri).await 157 113 } 158 114 159 115 pub(super) async fn hydrate_posts_inner( 160 116 &self, 161 117 posts: Vec<String>, 162 118 ) -> HashMap<String, HydratePostsRet> { 163 - // Load posts (now includes stats via gRPC - no separate PostStatsLoader needed!) 164 119 let load_start = std::time::Instant::now(); 165 120 let posts_with_stats = self.loaders.posts.load_many(posts).await; 166 121 let load_time = load_start.elapsed().as_secs_f64() * 1000.0; ··· 169 124 tracing::warn!(" → Slow load stats and posts: {:.1} ms", load_time); 170 125 } 171 126 172 - // Extract stats from posts into separate map 173 127 let stats: HashMap<String, parakeet_db::models::PostStats> = posts_with_stats 174 128 .iter() 175 129 .filter_map(|(uri, (_, _, stats_opt))| { ··· 177 131 }) 178 132 .collect(); 179 133 180 - // Extract data for hydration: author IDs and create actor_id→DID mapping 181 134 let mut actor_id_to_did: HashMap<i32, String> = HashMap::new(); 182 135 let mut post_key_to_did: HashMap<(i32, i64), String> = HashMap::new(); 183 136 let (author_ids, post_uris_with_keys): (Vec<_>, Vec<(String, i32, i64, String)>) = posts_with_stats 184 137 .values() 185 138 .map(|(post, _, _)| { 186 - let author_id = post.post.actor_id; // Use actor_id for loading 187 - actor_id_to_did.insert(author_id, post.did.clone()); // Map actor_id → DID 188 - post_key_to_did.insert((post.post.actor_id, post.post.rkey), post.did.clone()); // Map (actor_id, rkey) → DID for threadgates 189 - // (uri, actor_id, rkey, post_author_did) 139 + let author_id = post.post.actor_id; 140 + actor_id_to_did.insert(author_id, post.did.clone()); 141 + post_key_to_did.insert((post.post.actor_id, post.post.rkey), post.did.clone()); 190 142 let uri_with_keys = (post.at_uri.clone(), post.post.actor_id, post.post.rkey, post.did.clone()); 191 143 (author_id, uri_with_keys) 192 144 }) ··· 197 149 .filter_map(|(_, threadgate, _)| threadgate.clone()) 198 150 .collect::<Vec<_>>(); 199 151 200 - // Parallelize: hydrate all independent data concurrently (except embeds which needs post data) 201 152 let hydrate_start = std::time::Instant::now(); 202 153 203 - // Capture counts before moving 204 154 let author_count = author_ids.len(); 205 155 let post_uri_count = post_uris_with_keys.len(); 206 156 let threadgate_count = threadgates_to_hydrate.len(); 207 157 208 - // Extract just URIs for labels (doesn't need natural keys or author DIDs) 209 158 let post_uris: Vec<String> = post_uris_with_keys.iter().map(|(uri, _, _, _)| uri.clone()).collect(); 210 159 211 - // Time each operation individually to identify bottlenecks 212 160 let profiles_future = async move { 213 161 let start = std::time::Instant::now(); 214 - let profiles_by_id = self.hydrate_profiles_by_id(author_ids).await; // Use by_id variant 162 + let profiles_by_id = self.hydrate_profiles_by_id(author_ids).await; 215 163 let elapsed = start.elapsed().as_secs_f64() * 1000.0; 216 164 tracing::info!(" → Profiles: {:.1} ms ({} actors)", elapsed, author_count); 217 165 if elapsed > 20.0 { 218 166 tracing::warn!(" → Slow profiles: {:.1} ms", elapsed); 219 167 } 220 168 221 - // Convert HashMap<i32, ProfileViewBasic> to HashMap<String, ProfileViewBasic> 222 169 profiles_by_id 223 170 .into_iter() 224 171 .filter_map(|(actor_id, profile)| { ··· 264 211 threadgates_future 265 212 ); 266 213 267 - // Build embeds from post data (no DB query needed since PostLoader already fetched embed fields) 268 214 let embeds_start = std::time::Instant::now(); 269 215 let mut embeds = self.hydrate_embeds_from_posts(&posts_with_stats).await; 270 216 let embeds_time = embeds_start.elapsed().as_secs_f64() * 1000.0; ··· 291 237 let author = authors.get(&post.did)?.clone(); 292 238 let embed = embeds.remove(&uri); 293 239 let threadgate = threadgate.and_then(|tg| { 294 - // Construct threadgate URI from natural keys for lookup 295 240 let encoded_rkey = parakeet_db::tid_util::encode_tid(tg.rkey); 296 241 let threadgate_uri = format!("at://{}/app.bsky.feed.threadgate/{}", post.did, encoded_rkey); 297 242 threadgates.get(&threadgate_uri).cloned() ··· 316 261 .collect() 317 262 } 318 263 319 - /// Hydrate posts for embeds (quote posts) - optimized version that skips expensive viewer-specific data 320 - /// 321 - /// This is a faster version of `hydrate_posts` specifically for quoted posts in embeds. 322 - /// It skips: 323 - /// - Viewer states (likes, reposts, bookmarks) - not shown in embeds 324 - /// - Threadgates - not relevant for embedded posts 325 - /// 326 - /// It still loads: 327 - /// - Post data and stats (like/repost counts) - required by RecordView schema 328 - /// - Author profiles - required by RecordView schema 329 - /// - Labels - needed for moderation 330 - /// - Nested embeds - needed if the quoted post has images/videos 331 - /// 332 - /// Expected performance: Saves 40-80ms per quote post by skipping viewer states query 333 264 pub async fn hydrate_posts_for_embed(&self, posts: Vec<String>) -> HashMap<String, PostView> { 334 - if posts.is_empty() { 335 - return HashMap::new(); 336 - } 337 - 338 - // Load posts (now includes stats via gRPC - no separate PostStatsLoader needed!) 339 - let load_start = std::time::Instant::now(); 340 - let posts_with_stats = self.loaders.posts.load_many(posts).await; 341 - let load_time = load_start.elapsed().as_secs_f64() * 1000.0; 342 - tracing::debug!(" → [Embed] Load posts + stats: {:.1} ms ({} posts)", load_time, posts_with_stats.len()); 343 - 344 - // Extract stats from posts into separate map 345 - let stats: HashMap<String, parakeet_db::models::PostStats> = posts_with_stats 346 - .iter() 347 - .filter_map(|(uri, (_, _, stats_opt))| { 348 - stats_opt.map(|stats| (uri.clone(), stats)) 349 - }) 350 - .collect(); 351 - 352 - // Extract author IDs and create mapping to DIDs 353 - let mut actor_id_to_did: HashMap<i32, String> = HashMap::new(); 354 - let (author_ids, post_uris) = posts_with_stats 355 - .values() 356 - .map(|(post, _, _)| { 357 - actor_id_to_did.insert(post.post.actor_id, post.did.clone()); 358 - (post.post.actor_id, post.at_uri.clone()) 359 - }) 360 - .unzip::<_, _, Vec<_>, Vec<_>>(); 361 - 362 - let author_count = author_ids.len(); 363 - let post_uri_count = post_uris.len(); 364 - 365 - // Parallelize: hydrate authors and labels (skip viewer states and threadgates!) 366 - let hydrate_start = std::time::Instant::now(); 367 - 368 - let profiles_future = async move { 369 - let start = std::time::Instant::now(); 370 - let profiles_by_id = self.hydrate_profiles_by_id(author_ids).await; // Use by_id variant 371 - let elapsed = start.elapsed().as_secs_f64() * 1000.0; 372 - tracing::debug!(" → [Embed] Profiles: {:.1} ms ({} actors)", elapsed, author_count); 373 - 374 - // Convert HashMap<i32, ProfileViewBasic> to HashMap<String, ProfileViewBasic> 375 - profiles_by_id 376 - .into_iter() 377 - .filter_map(|(actor_id, profile)| { 378 - actor_id_to_did.get(&actor_id).map(|did| (did.clone(), profile)) 379 - }) 380 - .collect::<HashMap<String, ProfileViewBasic>>() 381 - }; 382 - 383 - let labels_future = async { 384 - let start = std::time::Instant::now(); 385 - let result = self.get_label_many_by_actor_ids(&post_uris).await; 386 - let elapsed = start.elapsed().as_secs_f64() * 1000.0; 387 - tracing::debug!(" → [Embed] Labels: {:.1} ms ({} posts)", elapsed, post_uri_count); 388 - result 389 - }; 390 - 391 - let (authors, mut post_labels) = tokio::join!( 392 - profiles_future, 393 - labels_future 394 - ); 395 - 396 - let hydrate_time = hydrate_start.elapsed().as_secs_f64() * 1000.0; 397 - tracing::debug!(" → [Embed] Hydration (authors/labels): {:.1} ms", hydrate_time); 398 - 399 - // Build embeds from post data 400 - let embeds_start = std::time::Instant::now(); 401 - let mut embeds = self.hydrate_embeds_from_posts(&posts_with_stats).await; 402 - let embeds_time = embeds_start.elapsed().as_secs_f64() * 1000.0; 403 - tracing::debug!(" → [Embed] Embeds: {:.1} ms", embeds_time); 404 - 405 - // Build PostViews without viewer states or threadgates 406 - posts_with_stats 407 - .into_iter() 408 - .filter_map(|(uri, (post, _threadgate, _stats))| { 409 - let author = authors.get(&post.did)?.clone(); 410 - let embed = embeds.remove(&uri); 411 - let labels = post_labels.remove(&uri).unwrap_or_default(); 412 - let post_stats = stats.get(&uri).copied(); 413 - 414 - Some(( 415 - uri, 416 - build_postview(( 417 - post, 418 - author, 419 - labels, 420 - embed, 421 - None, // No threadgate for embeds 422 - None, // No viewer state for embeds 423 - post_stats, 424 - ), self.loaders.post_state.id_cache()), 425 - )) 426 - }) 427 - .collect() 265 + self.hydrate_posts(posts).await 428 266 } 429 267 430 268 fn get_post_viewer_state(&self, post: &parakeet_db::models::Post) -> Option<PostViewerState> { 431 269 let viewer_cache = self.viewer_cache.as_ref()?; 432 270 let viewer_did = self.current_actor.as_ref()?; 433 271 434 - // Check if viewer liked this post (array lookup in Rust) 435 272 let like_rkey = post.like_actor_ids 436 273 .as_ref() 437 274 .and_then(|actor_ids| { ··· 441 278 post.like_rkeys.as_ref()?.get(pos)?.as_ref().copied() 442 279 }); 443 280 444 - // Check if viewer reposted this post (array lookup in Rust) 445 281 let repost_rkey = post.repost_actor_ids 446 282 .as_ref() 447 283 .and_then(|actor_ids| { ··· 451 287 post.repost_rkeys.as_ref()?.get(pos)?.as_ref().copied() 452 288 }); 453 289 454 - // Check if viewer bookmarked this post (cached array lookup) 455 290 let bookmarked = viewer_cache.bookmarks 456 291 .iter() 457 292 .any(|b| b.post_actor_id == post.actor_id && b.post_rkey == post.rkey); 458 293 459 - // Check if embed is disabled (postgate rule check in Rust) 460 294 let embed_disabled = post.postgate_rules 461 295 .as_ref() 462 296 .map(|rules| { ··· 466 300 }) 467 301 .unwrap_or(false); 468 302 469 - // Check if pinned (only applies to viewer's own posts) 470 303 let pinned = post.actor_id == viewer_cache.actor_id 471 304 && viewer_cache.pinned_post_rkey == Some(post.rkey); 472 305 473 306 let is_me = viewer_cache.actor_id == post.actor_id; 474 307 475 - // Build viewer state (same as build_viewer, but inlined to avoid data structure conversion) 476 308 let repost = repost_rkey.map(|rkey| { 477 309 let encoded_rkey = parakeet_db::tid_util::encode_tid(rkey); 478 310 format!("at://{viewer_did}/app.bsky.feed.repost/{encoded_rkey}") ··· 486 318 repost, 487 319 like, 488 320 bookmarked, 489 - thread_muted: false, // todo when we have thread mutes 321 + thread_muted: false, 490 322 reply_disabled: false, 491 - embedding_disabled: embed_disabled && !is_me, // poster can always bypass embed disabled 323 + embedding_disabled: embed_disabled && !is_me, 492 324 pinned, 493 325 }) 494 326 }
+1
parakeet/tests/db_feedgens_test.rs
··· 68 68 let result = parakeet::db::get_feedgen_service_did( 69 69 &mut conn, 70 70 "at://did:plc:test/app.bsky.feed.generator/abc123", 71 + None, 71 72 ) 72 73 .await; 73 74
-22
parakeet/tests/db_graph_test.rs
··· 250 250 Ok(()) 251 251 } 252 252 253 - // ============================================================================ 254 - // LIST QUERY TESTS 255 - // ============================================================================ 256 - 257 - #[tokio::test] 258 - async fn test_get_list_id_by_uri() -> eyre::Result<()> { 259 - common::ensure_test_db_ready().await; 260 - let pool = common::test_diesel_pool(); 261 - let mut conn = pool.get().await.wrap_err("Failed to get connection")?; 262 - 263 - let result = parakeet::db::get_list_id_by_uri(&mut conn, 1, "abc123").await; 264 - 265 - // Query should execute without SQL syntax errors (NotFound is ok) 266 - assert!( 267 - result.is_ok() || result.is_err(), 268 - "Query should execute: {:?}", 269 - result 270 - ); 271 - 272 - Ok(()) 273 - } 274 - 275 253 #[tokio::test] 276 254 async fn test_get_actor_lists() -> eyre::Result<()> { 277 255 common::ensure_test_db_ready().await;
-42
parakeet/tests/db_states_test.rs
··· 29 29 } 30 30 31 31 // ============================================================================ 32 - // POST STATE TESTS 33 - // ============================================================================ 34 - 35 - #[tokio::test] 36 - async fn test_get_post_state() -> eyre::Result<()> { 37 - common::ensure_test_db_ready().await; 38 - let pool = common::test_diesel_pool(); 39 - let mut conn = pool.get().await.wrap_err("Failed to get connection")?; 40 - 41 - // Use dummy IDs for SQL syntax validation (doesn't need to exist) 42 - let _result = parakeet::db::get_post_state( 43 - &mut conn, 44 - 999, // viewer_id 45 - 888, // subject_actor_id 46 - 1234567890, // subject_rkey 47 - ) 48 - .await 49 - .wrap_err("SQL syntax error in get_post_state")?; 50 - 51 - Ok(()) 52 - } 53 - 54 - #[tokio::test] 55 - async fn test_get_post_states() -> eyre::Result<()> { 56 - common::ensure_test_db_ready().await; 57 - let pool = common::test_diesel_pool(); 58 - let mut conn = pool.get().await.wrap_err("Failed to get connection")?; 59 - 60 - // get_post_states expects (uri, actor_id, rkey) tuples 61 - let subjects = vec![ 62 - ("at://did:plc:test/app.bsky.feed.post/3ktpjpifdsr2d".to_string(), 888, 1234567890), 63 - ]; 64 - 65 - // Use dummy viewer_id for SQL syntax validation (doesn't need to exist) 66 - let _result = parakeet::db::get_post_states(&mut conn, 999, &subjects) 67 - .await 68 - .wrap_err("SQL syntax error in get_post_states")?; 69 - 70 - Ok(()) 71 - } 72 - 73 - // ============================================================================ 74 32 // LIST STATE TESTS 75 33 // ============================================================================ 76 34
+2 -70
parakeet/tests/sql/db_module_test.rs
··· 82 82 } 83 83 84 84 // ============================================================================ 85 - // POST STATE TESTS (External SQL: post_state.sql) 86 - // ============================================================================ 87 - 88 - #[tokio::test] 89 - async fn test_get_post_state_empty() -> eyre::Result<()> { 90 - common::ensure_test_db_ready().await; 91 - let pool = common::test_diesel_pool(); 92 - let mut conn = pool.get().await 93 - .wrap_err("Failed to get connection")?; 94 - 95 - // Use dummy IDs for SQL syntax validation (doesn't need to exist) 96 - let result = db::get_post_state( 97 - &mut conn, 98 - 999, // viewer_id 99 - 888, // subject_actor_id 100 - 1234567890 // subject_rkey 101 - ).await; 102 - 103 - let result = result.wrap_err("SQL query failed")?; 104 - 105 - Ok(()) 106 - } 107 - 108 - #[tokio::test] 109 - async fn test_get_post_states_empty() -> eyre::Result<()> { 110 - common::ensure_test_db_ready().await; 111 - let pool = common::test_diesel_pool(); 112 - let mut conn = pool.get().await 113 - .wrap_err("Failed to get connection")?; 114 - 115 - // get_post_states expects (uri, actor_id, rkey) tuples 116 - let subjects = vec![ 117 - ("at://did:plc:author/app.bsky.feed.post/3ktpjpifdsr2d".to_string(), 888, 1234567890), 118 - ("at://did:plc:author/app.bsky.feed.post/3kvqomklhtx2d".to_string(), 888, 1234567891), 119 - ]; 120 - 121 - // Use dummy viewer_id for SQL syntax validation (doesn't need to exist) 122 - let result = db::get_post_states( 123 - &mut conn, 124 - 999, 125 - &subjects 126 - ).await; 127 - 128 - let result = result.wrap_err("SQL query failed")?; 129 - 130 - Ok(()) 131 - } 132 - 133 - // ============================================================================ 134 85 // LIST STATE TESTS (External SQL: list_states.sql) 135 86 // ============================================================================ 136 87 ··· 805 756 // Query for a non-existent feedgen (should still validate SQL) 806 757 let result = db::get_feedgen_service_did( 807 758 &mut conn, 808 - "at://did:plc:nonexistent/app.bsky.feed.generator/testfeed" 759 + "at://did:plc:nonexistent/app.bsky.feed.generator/testfeed", 760 + None, 809 761 ).await; 810 762 811 763 // Should fail with NotFound (no rows), but SQL should be valid ··· 1254 1206 ).await; 1255 1207 1256 1208 let result = result.wrap_err("SQL query failed")?; 1257 - 1258 - Ok(()) 1259 - } 1260 - 1261 - #[tokio::test] 1262 - async fn test_get_list_id_by_uri_nonexistent() -> eyre::Result<()> { 1263 - common::ensure_test_db_ready().await; 1264 - let pool = common::test_diesel_pool(); 1265 - let mut conn = pool.get().await 1266 - .wrap_err("Failed to get connection")?; 1267 - 1268 - // Query for non-existent list 1269 - let result = db::get_list_id_by_uri( 1270 - &mut conn, 1271 - 999, 1272 - "3ktpjpifdsr2d" 1273 - ).await; 1274 - 1275 - // Should fail for non-existent list (NotFound error, not SQL syntax error) 1276 - assert!(result.is_err(), "Should fail for non-existent list"); 1277 1209 1278 1210 Ok(()) 1279 1211 }