2828/// before the query is issued so the SQL `LIMIT` clause always reflects this cap.
2929pub const FEED_MAX_LIMIT : i64 = 100 ;
3030
31+ use buzz_datastore_tracing:: datastore_span;
3132use chrono:: { DateTime , Utc } ;
3233use sqlx:: postgres:: PgRow ;
3334use sqlx:: { PgPool , QueryBuilder } ;
@@ -41,8 +42,8 @@ use buzz_core::kind::{
4142} ;
4243use buzz_core:: { CommunityId , StoredEvent } ;
4344
44- use crate :: error:: Result ;
4545use crate :: event:: row_to_stored_event;
46+ use crate :: { error:: Result , Db , RouteDecision , RoutePredicate } ;
4647
4748/// Column list shared by every feed subquery that aliases the `events` table as `e`.
4849const EVENT_COLS : & str =
@@ -303,6 +304,235 @@ pub(crate) async fn query_activity_on(
303304 collect_stored_events ( rows)
304305}
305306
307+ // -- Db API -------------------------------------------------------------------
308+
309+ impl Db {
310+ /// Find events that @mention the given pubkey.
311+ #[ datastore_span( name = "query_feed_mentions" , system = "postgresql" ) ]
312+ pub async fn query_feed_mentions (
313+ & self ,
314+ community : CommunityId ,
315+ pubkey_bytes : & [ u8 ] ,
316+ accessible_channel_ids : & [ Uuid ] ,
317+ since : Option < DateTime < Utc > > ,
318+ limit : i64 ,
319+ ) -> Result < Vec < StoredEvent > > {
320+ crate :: feed:: query_mentions (
321+ & self . pool ,
322+ community,
323+ pubkey_bytes,
324+ accessible_channel_ids,
325+ since,
326+ limit,
327+ )
328+ . await
329+ }
330+
331+ /// [`Db::query_feed_mentions`] with replica routing — same contract and
332+ /// classification-table requirement as [`Db::query_events_routed`].
333+ ///
334+ /// Feed queries route on the BOUNDED arm only: the `accessible_channel_ids`
335+ /// parameter admits community-global rows alongside channel rows, so no
336+ /// single channel's fence floor can prove completeness — the covered arm
337+ /// is structurally unavailable, not merely unchosen.
338+ #[ datastore_span( name = "query_feed_mentions_routed" , system = "postgresql" ) ]
339+ pub async fn query_feed_mentions_routed (
340+ & self ,
341+ path : & ' static str ,
342+ community : CommunityId ,
343+ pubkey_bytes : & [ u8 ] ,
344+ accessible_channel_ids : & [ Uuid ] ,
345+ since : Option < DateTime < Utc > > ,
346+ limit : i64 ,
347+ ) -> Result < Vec < StoredEvent > > {
348+ match self . route_read ( path, RoutePredicate :: Bounded ) . await {
349+ RouteDecision :: Replica ( mut tx, _entry, reason) => match crate :: feed:: query_mentions_on (
350+ & mut tx,
351+ community,
352+ pubkey_bytes,
353+ accessible_channel_ids,
354+ since,
355+ limit,
356+ )
357+ . await
358+ {
359+ Ok ( events) => {
360+ Self :: record_route ( path, "replica" , reason) ;
361+ Ok ( events)
362+ }
363+ Err ( e) => {
364+ tracing:: warn!( path, "replica read failed; re-running on writer: {e}" ) ;
365+ Self :: record_route ( path, "writer" , "replica_error" ) ;
366+ crate :: feed:: query_mentions (
367+ & self . pool ,
368+ community,
369+ pubkey_bytes,
370+ accessible_channel_ids,
371+ since,
372+ limit,
373+ )
374+ . await
375+ }
376+ } ,
377+ RouteDecision :: Writer => {
378+ crate :: feed:: query_mentions (
379+ & self . pool ,
380+ community,
381+ pubkey_bytes,
382+ accessible_channel_ids,
383+ since,
384+ limit,
385+ )
386+ . await
387+ }
388+ }
389+ }
390+
391+ /// Find events that require action from the given pubkey.
392+ #[ datastore_span( name = "query_feed_needs_action" , system = "postgresql" ) ]
393+ pub async fn query_feed_needs_action (
394+ & self ,
395+ community : CommunityId ,
396+ pubkey_bytes : & [ u8 ] ,
397+ accessible_channel_ids : & [ Uuid ] ,
398+ since : Option < DateTime < Utc > > ,
399+ limit : i64 ,
400+ ) -> Result < Vec < StoredEvent > > {
401+ crate :: feed:: query_needs_action (
402+ & self . pool ,
403+ community,
404+ pubkey_bytes,
405+ accessible_channel_ids,
406+ since,
407+ limit,
408+ )
409+ . await
410+ }
411+
412+ /// [`Db::query_feed_needs_action`] with replica routing — BOUNDED arm
413+ /// only; see [`Db::query_feed_mentions_routed`] for why the covered arm
414+ /// is structurally unavailable to feed queries.
415+ #[ datastore_span( name = "query_feed_needs_action_routed" , system = "postgresql" ) ]
416+ pub async fn query_feed_needs_action_routed (
417+ & self ,
418+ path : & ' static str ,
419+ community : CommunityId ,
420+ pubkey_bytes : & [ u8 ] ,
421+ accessible_channel_ids : & [ Uuid ] ,
422+ since : Option < DateTime < Utc > > ,
423+ limit : i64 ,
424+ ) -> Result < Vec < StoredEvent > > {
425+ match self . route_read ( path, RoutePredicate :: Bounded ) . await {
426+ RouteDecision :: Replica ( mut tx, _entry, reason) => {
427+ match crate :: feed:: query_needs_action_on (
428+ & mut tx,
429+ community,
430+ pubkey_bytes,
431+ accessible_channel_ids,
432+ since,
433+ limit,
434+ )
435+ . await
436+ {
437+ Ok ( events) => {
438+ Self :: record_route ( path, "replica" , reason) ;
439+ Ok ( events)
440+ }
441+ Err ( e) => {
442+ tracing:: warn!( path, "replica read failed; re-running on writer: {e}" ) ;
443+ Self :: record_route ( path, "writer" , "replica_error" ) ;
444+ crate :: feed:: query_needs_action (
445+ & self . pool ,
446+ community,
447+ pubkey_bytes,
448+ accessible_channel_ids,
449+ since,
450+ limit,
451+ )
452+ . await
453+ }
454+ }
455+ }
456+ RouteDecision :: Writer => {
457+ crate :: feed:: query_needs_action (
458+ & self . pool ,
459+ community,
460+ pubkey_bytes,
461+ accessible_channel_ids,
462+ since,
463+ limit,
464+ )
465+ . await
466+ }
467+ }
468+ }
469+
470+ /// Find recent activity across accessible channels.
471+ #[ datastore_span( name = "query_feed_activity" , system = "postgresql" ) ]
472+ pub async fn query_feed_activity (
473+ & self ,
474+ community : CommunityId ,
475+ accessible_channel_ids : & [ Uuid ] ,
476+ since : Option < DateTime < Utc > > ,
477+ limit : i64 ,
478+ ) -> Result < Vec < StoredEvent > > {
479+ crate :: feed:: query_activity ( & self . pool , community, accessible_channel_ids, since, limit)
480+ . await
481+ }
482+
483+ /// [`Db::query_feed_activity`] with replica routing — BOUNDED arm only;
484+ /// see [`Db::query_feed_mentions_routed`] for why the covered arm is
485+ /// structurally unavailable to feed queries.
486+ #[ datastore_span( name = "query_feed_activity_routed" , system = "postgresql" ) ]
487+ pub async fn query_feed_activity_routed (
488+ & self ,
489+ path : & ' static str ,
490+ community : CommunityId ,
491+ accessible_channel_ids : & [ Uuid ] ,
492+ since : Option < DateTime < Utc > > ,
493+ limit : i64 ,
494+ ) -> Result < Vec < StoredEvent > > {
495+ match self . route_read ( path, RoutePredicate :: Bounded ) . await {
496+ RouteDecision :: Replica ( mut tx, _entry, reason) => match crate :: feed:: query_activity_on (
497+ & mut tx,
498+ community,
499+ accessible_channel_ids,
500+ since,
501+ limit,
502+ )
503+ . await
504+ {
505+ Ok ( events) => {
506+ Self :: record_route ( path, "replica" , reason) ;
507+ Ok ( events)
508+ }
509+ Err ( e) => {
510+ tracing:: warn!( path, "replica read failed; re-running on writer: {e}" ) ;
511+ Self :: record_route ( path, "writer" , "replica_error" ) ;
512+ crate :: feed:: query_activity (
513+ & self . pool ,
514+ community,
515+ accessible_channel_ids,
516+ since,
517+ limit,
518+ )
519+ . await
520+ }
521+ } ,
522+ RouteDecision :: Writer => {
523+ crate :: feed:: query_activity (
524+ & self . pool ,
525+ community,
526+ accessible_channel_ids,
527+ since,
528+ limit,
529+ )
530+ . await
531+ }
532+ }
533+ }
534+ }
535+
306536// -- Tests --------------------------------------------------------------------
307537
308538#[ cfg( test) ]
0 commit comments