Skip to main content

xmtp_db/encrypted_store/
group.rs

1//! The Group database table. Stored information surrounding group membership and ID's.
2use super::{
3    ConnectionExt, Sqlite,
4    consent_record::ConsentState,
5    db_connection::DbConnection,
6    schema::groups::{self, dsl},
7};
8use crate::NotFound;
9use crate::{DuplicateItem, StorageError, impl_fetch, impl_store, impl_store_or_ignore};
10use derive_builder::Builder;
11use diesel::{
12    backend::Backend,
13    deserialize::{self, FromSql, FromSqlRow},
14    dsl::sql,
15    expression::AsExpression,
16    prelude::*,
17    serialize::{self, IsNull, Output, ToSql},
18    sql_types::Integer,
19};
20use serde::{Deserialize, Serialize};
21mod convert;
22mod dms;
23mod version;
24
25pub use dms::QueryDms;
26pub use version::QueryGroupVersion;
27use xmtp_proto::types::{Cursor, GroupId};
28
29#[derive(
30    Debug,
31    Clone,
32    Serialize,
33    Deserialize,
34    PartialEq,
35    Insertable,
36    Identifiable,
37    Queryable,
38    Builder,
39    Selectable,
40    QueryableByName,
41)]
42#[diesel(table_name = groups)]
43#[diesel(primary_key(id))]
44#[diesel(check_for_backend(Sqlite))]
45#[builder(setter(into), build_fn(error = "StorageError"))]
46#[derive(AsChangeset)]
47/// A Unique group chat
48pub struct StoredGroup {
49    /// Randomly generated ID by group creator
50    pub id: GroupId,
51    /// Based on timestamp of this welcome message
52    pub created_at_ns: i64,
53    /// Enum, [`GroupMembershipState`] representing access to the group
54    pub membership_state: GroupMembershipState,
55    /// Track when the latest, most recent installations were checked
56    #[builder(default = "0")]
57    pub installations_last_checked: i64,
58    /// The inbox_id of who added the user to a group.
59    pub added_by_inbox_id: String,
60    /// The sequence id of the welcome message
61    #[builder(default = None)]
62    pub sequence_id: Option<i64>,
63    /// The last time the leaf node encryption key was rotated
64    #[builder(default = "0")]
65    pub rotated_at_ns: i64,
66    /// Enum, [`ConversationType`] signifies the group conversation type which extends to who can access it.
67    #[builder(default = "self.default_conversation_type()")]
68    pub conversation_type: ConversationType,
69    /// The inbox_id of the DM target
70    #[builder(default = None)]
71    pub dm_id: Option<String>,
72    /// Timestamp of when the last message was sent for this group (updated automatically in a trigger)
73    #[builder(default = None)]
74    pub last_message_ns: Option<i64>,
75    /// The Time in NS when the messages should be deleted
76    #[builder(default = None)]
77    pub message_disappear_from_ns: Option<i64>,
78    /// How long a message in the group can live in NS
79    #[builder(default = None)]
80    pub message_disappear_in_ns: Option<i64>,
81    /// The version of the protocol that the group is paused for, None is not paused
82    #[builder(default = None)]
83    pub paused_for_version: Option<String>,
84    #[builder(default = false)]
85    pub maybe_forked: bool,
86    #[builder(default = "String::new()")]
87    pub fork_details: String,
88    /// Whether the user should publish the commit log for this group
89    #[builder(default = false)]
90    pub should_publish_commit_log: bool,
91    /// The consensus public key of the commit log for this group
92    /// Derived from the first entry of the commit log
93    #[builder(default = None)]
94    pub commit_log_public_key: Option<Vec<u8>>,
95    /// Whether the local commit log has diverged from the remote commit log
96    /// NULL if the remote commit log is not up to date yet
97    #[builder(default = None)]
98    pub is_commit_log_forked: Option<bool>,
99    /// Whether the pending-remove list is empty
100    /// NULL if the pending-remove didn't receive an update yet
101    #[builder(default = None)]
102    pub has_pending_leave_request: Option<bool>,
103    /// Optional notification rule for this conversation.
104    #[builder(default = None)]
105    #[serde(default)]
106    pub push_override: Option<i32>,
107    //todo: store member role?
108}
109
110impl StoredGroup {
111    pub fn cursor(&self) -> Option<Cursor> {
112        self.sequence_id.map(|sequence| Cursor(sequence as u64))
113    }
114}
115
116impl StoredGroupBuilder {
117    pub fn cursor(&mut self, cursor: Cursor) -> &mut Self {
118        self.sequence_id = Some(Some(cursor.0 as i64));
119        self
120    }
121}
122
123/// A subset of the group table for fetching the commit log public key
124#[derive(Queryable)]
125#[diesel(table_name = groups)]
126pub struct StoredGroupCommitLogPublicKey {
127    pub id: GroupId,
128    pub commit_log_public_key: Option<Vec<u8>>,
129}
130
131/// A struct for fetching groups that need readd requests with their latest epoch
132#[derive(Debug, Clone, Queryable, QueryableByName)]
133pub struct StoredGroupForReaddRequest {
134    #[diesel(sql_type = diesel::sql_types::Binary)]
135    pub group_id: GroupId,
136    #[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::BigInt>)]
137    pub latest_commit_sequence_id: Option<i64>,
138}
139
140/// A struct for fetching groups that need to respond to readd requests
141#[derive(Debug, Clone, Queryable, QueryableByName)]
142pub struct StoredGroupForRespondingReadds {
143    #[diesel(sql_type = diesel::sql_types::Binary)]
144    pub group_id: GroupId,
145    #[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
146    pub dm_id: Option<String>,
147    #[diesel(sql_type = diesel::sql_types::Integer)]
148    pub conversation_type: ConversationType,
149    #[diesel(sql_type = diesel::sql_types::BigInt)]
150    pub created_at_ns: i64,
151}
152
153// TODO: Create two more structs that delegate to StoredGroup
154impl_fetch!(StoredGroup, groups, GroupId);
155impl_store!(StoredGroup, groups);
156impl_store_or_ignore!(StoredGroup, groups);
157
158impl StoredGroupBuilder {
159    fn default_conversation_type(&self) -> ConversationType {
160        if self.dm_id.is_some() {
161            ConversationType::Dm
162        } else {
163            ConversationType::Group
164        }
165    }
166}
167
168impl StoredGroup {
169    pub fn builder() -> StoredGroupBuilder {
170        StoredGroupBuilder::default()
171    }
172}
173
174#[derive(Debug, Clone, Default)]
175pub enum GroupQueryOrderBy {
176    #[default]
177    CreatedAt,
178    LastActivity,
179}
180
181#[derive(Debug, Default, Clone)]
182pub struct GroupQueryArgs {
183    pub allowed_states: Option<Vec<GroupMembershipState>>,
184    pub created_after_ns: Option<i64>,
185    pub created_before_ns: Option<i64>,
186    pub last_activity_after_ns: Option<i64>,
187    pub last_activity_before_ns: Option<i64>,
188    pub limit: Option<i64>,
189    pub conversation_type: Option<ConversationType>,
190    pub consent_states: Option<Vec<ConsentState>>,
191    pub include_sync_groups: bool,
192    pub include_duplicate_dms: bool,
193    pub should_publish_commit_log: Option<bool>,
194    pub order_by: Option<GroupQueryOrderBy>,
195}
196
197impl AsRef<GroupQueryArgs> for GroupQueryArgs {
198    fn as_ref(&self) -> &GroupQueryArgs {
199        self
200    }
201}
202
203impl GroupQueryArgs {
204    pub fn validate(&self) -> Result<(), crate::ConnectionError> {
205        if self.last_activity_after_ns.is_some() && self.created_after_ns.is_some() {
206            return Err(crate::ConnectionError::InvalidQuery(
207                "last_activity_after_ns and created_after_ns cannot be used together".to_string(),
208            ));
209        }
210
211        if self.last_activity_before_ns.is_some() && self.created_before_ns.is_some() {
212            return Err(crate::ConnectionError::InvalidQuery(
213                "last_activity_before_ns and created_before_ns cannot be used together".to_string(),
214            ));
215        }
216
217        Ok(())
218    }
219}
220
221pub trait QueryGroup {
222    /// Return regular `Purpose::Conversation` groups with additional optional filters
223    fn find_groups<A: AsRef<GroupQueryArgs>>(
224        &self,
225        args: A,
226    ) -> Result<Vec<StoredGroup>, crate::ConnectionError>;
227
228    fn find_groups_by_id_paged<A: AsRef<GroupQueryArgs>>(
229        &self,
230        args: A,
231        offset: i64,
232    ) -> Result<Vec<StoredGroup>, crate::ConnectionError>;
233
234    /// Updates group membership state
235    fn update_group_membership<Id: AsRef<[u8]>>(
236        &self,
237        group_id: Id,
238        state: GroupMembershipState,
239    ) -> Result<(), crate::ConnectionError>;
240
241    fn all_sync_groups(&self) -> Result<Vec<StoredGroup>, crate::ConnectionError>;
242
243    fn find_sync_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError>;
244
245    fn primary_sync_group(&self) -> Result<Option<StoredGroup>, crate::ConnectionError>;
246
247    /// Return a single group that matches the given ID
248    fn find_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError>;
249
250    /// Return a single group that matches the given welcome ID
251    fn find_group_by_sequence_id(
252        &self,
253        cursor: Cursor,
254    ) -> Result<Option<StoredGroup>, crate::ConnectionError>;
255
256    fn get_rotated_at_ns(&self, group_id: &GroupId) -> Result<i64, StorageError>;
257
258    /// Updates the 'last time checked' we checked for new installations.
259    fn update_rotated_at_ns(&self, group_id: &GroupId) -> Result<(), StorageError>;
260
261    fn get_installations_time_checked(&self, group_id: &GroupId) -> Result<i64, StorageError>;
262
263    /// Updates the 'last time checked' we checked for new installations.
264    fn update_installations_time_checked(&self, group_id: &GroupId) -> Result<(), StorageError>;
265
266    fn update_message_disappearing_from_ns(
267        &self,
268        group_id: &GroupId,
269        from_ns: Option<i64>,
270    ) -> Result<(), StorageError>;
271
272    fn update_message_disappearing_in_ns(
273        &self,
274        group_id: &GroupId,
275        in_ns: Option<i64>,
276    ) -> Result<(), StorageError>;
277
278    fn insert_or_replace_group(&self, group: StoredGroup) -> Result<StoredGroup, StorageError>;
279
280    /// Get all the welcome ids turned into groups
281    fn group_cursors(&self) -> Result<Vec<Cursor>, crate::ConnectionError>;
282
283    fn mark_group_as_maybe_forked(
284        &self,
285        group_id: &GroupId,
286        fork_details: String,
287    ) -> Result<(), StorageError>;
288
289    fn clear_fork_flag_for_group(&self, group_id: &GroupId) -> Result<(), crate::ConnectionError>;
290
291    fn has_duplicate_dm(&self, group_id: &GroupId) -> Result<bool, crate::ConnectionError>;
292
293    /// Get conversations for all conversations that require a remote commit log publish (DMs and groups where user is super admin, excluding sync groups)
294    fn get_conversation_ids_for_remote_log_publish(
295        &self,
296    ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError>;
297
298    /// Get conversations for all conversations that require a remote commit log download (DMs and groups that are not sync groups)
299    fn get_conversation_ids_for_remote_log_download(
300        &self,
301    ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError>;
302
303    /// Get conversation IDs for fork checking (excludes already forked conversations and sync groups)
304    fn get_conversation_ids_for_fork_check(&self) -> Result<Vec<Vec<u8>>, crate::ConnectionError>;
305
306    /// Get conversation IDs for conversations that are forked and need readd requests
307    fn get_conversation_ids_for_requesting_readds(
308        &self,
309    ) -> Result<Vec<StoredGroupForReaddRequest>, crate::ConnectionError>;
310
311    /// Get conversation IDs for conversations that need to respond to readd requests
312    fn get_conversation_ids_for_responding_readds(
313        &self,
314    ) -> Result<Vec<StoredGroupForRespondingReadds>, crate::ConnectionError>;
315
316    fn get_conversation_type(
317        &self,
318        group_id: &GroupId,
319    ) -> Result<ConversationType, crate::ConnectionError>;
320
321    /// Updates the commit log public key for a group
322    fn set_group_commit_log_public_key(
323        &self,
324        group_id: &GroupId,
325        public_key: &[u8],
326    ) -> Result<(), StorageError>;
327
328    /// Updates the is_commit_log_forked status for a group
329    fn set_group_commit_log_forked_status(
330        &self,
331        group_id: &GroupId,
332        is_forked: Option<bool>,
333    ) -> Result<(), StorageError>;
334
335    /// Gets the is_commit_log_forked status for a group
336    fn get_group_commit_log_forked_status(
337        &self,
338        group_id: &GroupId,
339    ) -> Result<Option<bool>, StorageError>;
340
341    /// Updates the has_pending_leave_request status for a group
342    fn set_group_has_pending_leave_request_status(
343        &self,
344        group_id: &GroupId,
345        has_pending_leave_request: Option<bool>,
346    ) -> Result<(), StorageError>;
347
348    fn get_groups_have_pending_leave_request(&self)
349    -> Result<Vec<Vec<u8>>, crate::ConnectionError>;
350}
351
352impl<T> QueryGroup for &T
353where
354    T: QueryGroup,
355{
356    /// Return regular `Purpose::Conversation` groups with additional optional filters
357    fn find_groups<A: AsRef<GroupQueryArgs>>(
358        &self,
359        args: A,
360    ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
361        (**self).find_groups(args)
362    }
363
364    fn find_groups_by_id_paged<A: AsRef<GroupQueryArgs>>(
365        &self,
366        args: A,
367        offset: i64,
368    ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
369        (**self).find_groups_by_id_paged(args, offset)
370    }
371
372    /// Updates group membership state
373    fn update_group_membership<Id: AsRef<[u8]>>(
374        &self,
375        group_id: Id,
376        state: GroupMembershipState,
377    ) -> Result<(), crate::ConnectionError> {
378        (**self).update_group_membership(group_id, state)
379    }
380
381    fn all_sync_groups(&self) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
382        (**self).all_sync_groups()
383    }
384
385    fn find_sync_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
386        (**self).find_sync_group(id)
387    }
388
389    fn primary_sync_group(&self) -> Result<Option<StoredGroup>, crate::ConnectionError> {
390        (**self).primary_sync_group()
391    }
392
393    /// Return a single group that matches the given ID
394    fn find_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
395        (**self).find_group(id)
396    }
397
398    /// Return a single group that matches the given welcome ID
399    fn find_group_by_sequence_id(
400        &self,
401        cursor: Cursor,
402    ) -> Result<Option<StoredGroup>, crate::ConnectionError> {
403        (**self).find_group_by_sequence_id(cursor)
404    }
405
406    fn get_rotated_at_ns(&self, group_id: &GroupId) -> Result<i64, StorageError> {
407        (**self).get_rotated_at_ns(group_id)
408    }
409
410    /// Updates the 'last time checked' we checked for new installations.
411    fn update_rotated_at_ns(&self, group_id: &GroupId) -> Result<(), StorageError> {
412        (**self).update_rotated_at_ns(group_id)
413    }
414
415    fn get_installations_time_checked(&self, group_id: &GroupId) -> Result<i64, StorageError> {
416        (**self).get_installations_time_checked(group_id)
417    }
418
419    /// Updates the 'last time checked' we checked for new installations.
420    fn update_installations_time_checked(&self, group_id: &GroupId) -> Result<(), StorageError> {
421        (**self).update_installations_time_checked(group_id)
422    }
423
424    fn update_message_disappearing_from_ns(
425        &self,
426        group_id: &GroupId,
427        from_ns: Option<i64>,
428    ) -> Result<(), StorageError> {
429        (**self).update_message_disappearing_from_ns(group_id, from_ns)
430    }
431
432    fn update_message_disappearing_in_ns(
433        &self,
434        group_id: &GroupId,
435        in_ns: Option<i64>,
436    ) -> Result<(), StorageError> {
437        (**self).update_message_disappearing_in_ns(group_id, in_ns)
438    }
439
440    fn insert_or_replace_group(&self, group: StoredGroup) -> Result<StoredGroup, StorageError> {
441        (**self).insert_or_replace_group(group)
442    }
443
444    /// Get all the welcome ids turned into groups
445    fn group_cursors(&self) -> Result<Vec<Cursor>, crate::ConnectionError> {
446        (**self).group_cursors()
447    }
448
449    fn mark_group_as_maybe_forked(
450        &self,
451        group_id: &GroupId,
452        fork_details: String,
453    ) -> Result<(), StorageError> {
454        (**self).mark_group_as_maybe_forked(group_id, fork_details)
455    }
456
457    fn clear_fork_flag_for_group(&self, group_id: &GroupId) -> Result<(), crate::ConnectionError> {
458        (**self).clear_fork_flag_for_group(group_id)
459    }
460
461    fn has_duplicate_dm(&self, group_id: &GroupId) -> Result<bool, crate::ConnectionError> {
462        (**self).has_duplicate_dm(group_id)
463    }
464
465    /// Get conversation IDs for all conversations that require a remote commit log publish (DMs and groups where user is super admin, excluding sync groups)
466    fn get_conversation_ids_for_remote_log_publish(
467        &self,
468    ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
469        (**self).get_conversation_ids_for_remote_log_publish()
470    }
471
472    fn get_conversation_ids_for_remote_log_download(
473        &self,
474    ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
475        (**self).get_conversation_ids_for_remote_log_download()
476    }
477
478    fn get_conversation_ids_for_fork_check(&self) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
479        (**self).get_conversation_ids_for_fork_check()
480    }
481
482    fn get_conversation_ids_for_requesting_readds(
483        &self,
484    ) -> Result<Vec<StoredGroupForReaddRequest>, crate::ConnectionError> {
485        (**self).get_conversation_ids_for_requesting_readds()
486    }
487
488    fn get_conversation_ids_for_responding_readds(
489        &self,
490    ) -> Result<Vec<StoredGroupForRespondingReadds>, crate::ConnectionError> {
491        (**self).get_conversation_ids_for_responding_readds()
492    }
493
494    fn get_conversation_type(
495        &self,
496        group_id: &GroupId,
497    ) -> Result<ConversationType, crate::ConnectionError> {
498        (**self).get_conversation_type(group_id)
499    }
500
501    fn set_group_commit_log_public_key(
502        &self,
503        group_id: &GroupId,
504        public_key: &[u8],
505    ) -> Result<(), StorageError> {
506        (**self).set_group_commit_log_public_key(group_id, public_key)
507    }
508
509    fn set_group_commit_log_forked_status(
510        &self,
511        group_id: &GroupId,
512        is_forked: Option<bool>,
513    ) -> Result<(), StorageError> {
514        (**self).set_group_commit_log_forked_status(group_id, is_forked)
515    }
516
517    fn get_group_commit_log_forked_status(
518        &self,
519        group_id: &GroupId,
520    ) -> Result<Option<bool>, StorageError> {
521        (**self).get_group_commit_log_forked_status(group_id)
522    }
523
524    fn set_group_has_pending_leave_request_status(
525        &self,
526        group_id: &GroupId,
527        has_pending_leave_request: Option<bool>,
528    ) -> Result<(), StorageError> {
529        (**self).set_group_has_pending_leave_request_status(group_id, has_pending_leave_request)
530    }
531
532    fn get_groups_have_pending_leave_request(
533        &self,
534    ) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
535        (**self).get_groups_have_pending_leave_request()
536    }
537}
538
539impl<C: ConnectionExt> QueryGroup for DbConnection<C> {
540    /// Return regular `Purpose::Conversation` groups with additional optional filters
541    #[xmtp_common::db_span]
542    fn find_groups<A: AsRef<GroupQueryArgs>>(
543        &self,
544        args: A,
545    ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
546        use crate::schema::consent_records::dsl as consent_dsl;
547
548        args.as_ref().validate()?;
549
550        let GroupQueryArgs {
551            allowed_states,
552            created_after_ns,
553            created_before_ns,
554            limit,
555            conversation_type,
556            consent_states,
557            include_sync_groups,
558            include_duplicate_dms,
559            last_activity_after_ns,
560            last_activity_before_ns,
561            should_publish_commit_log,
562            order_by,
563        } = args.as_ref();
564
565        let order_expression = match order_by.clone().unwrap_or_default() {
566            GroupQueryOrderBy::CreatedAt => {
567                diesel::dsl::sql::<diesel::sql_types::BigInt>("created_at_ns ASC")
568            }
569            GroupQueryOrderBy::LastActivity => diesel::dsl::sql::<diesel::sql_types::BigInt>(
570                "COALESCE(last_message_ns, created_at_ns) DESC",
571            ),
572        };
573
574        let mut query = dsl::groups
575            .filter(dsl::conversation_type.ne_all(ConversationType::virtual_types()))
576            .order(order_expression)
577            .into_boxed();
578
579        if !include_duplicate_dms {
580            // Fast DM deduplication using EXISTS - avoids expensive window functions
581            // Keep only the latest group for each dm_id (or regular group if not a DM)
582            query = query.filter(sql::<diesel::sql_types::Bool>(
583                "NOT EXISTS (
584                    SELECT 1 FROM groups g2
585                    WHERE COALESCE(g2.dm_id, g2.id) = COALESCE(groups.dm_id, groups.id)
586                    AND (COALESCE(g2.last_message_ns, 0), g2.id) > (COALESCE(groups.last_message_ns, 0), groups.id)
587                )",
588            ));
589        }
590
591        if let Some(limit) = limit {
592            query = query.limit(*limit);
593        }
594
595        if let Some(allowed_states) = allowed_states {
596            query = query.filter(dsl::membership_state.eq_any(allowed_states));
597        }
598
599        // last_activity_after_ns takes precedence over created_after_ns
600        if let Some(last_activity_after_ns) = last_activity_after_ns {
601            // "Activity after" means groups that were either created,
602            // or have sent a message after the specified time.
603            query = query.filter(
604                diesel::dsl::sql::<diesel::sql_types::BigInt>(
605                    "COALESCE(last_message_ns, created_at_ns)",
606                )
607                .gt(last_activity_after_ns),
608            );
609        }
610
611        if let Some(created_after_ns) = created_after_ns {
612            query = query.filter(dsl::created_at_ns.gt(created_after_ns));
613        }
614
615        if let Some(last_activity_before_ns) = last_activity_before_ns {
616            query = query.filter(
617                diesel::dsl::sql::<diesel::sql_types::BigInt>(
618                    "COALESCE(last_message_ns, created_at_ns)",
619                )
620                .lt(last_activity_before_ns),
621            );
622        }
623
624        if let Some(created_before_ns) = created_before_ns {
625            query = query.filter(dsl::created_at_ns.lt(created_before_ns));
626        }
627
628        if let Some(conversation_type) = conversation_type {
629            query = query.filter(dsl::conversation_type.eq(conversation_type));
630        }
631
632        let effective_consent_states = match &consent_states {
633            Some(states) if !states.is_empty() => states.clone(),
634            _ => vec![ConsentState::Allowed, ConsentState::Unknown],
635        };
636
637        let includes_unknown = effective_consent_states.contains(&ConsentState::Unknown);
638        let includes_all = effective_consent_states.len() == 3;
639
640        if let Some(should_publish_commit_log) = should_publish_commit_log {
641            query = query.filter(dsl::should_publish_commit_log.eq(should_publish_commit_log));
642        }
643
644        let filtered_states: Vec<_> = effective_consent_states
645            .iter()
646            .filter(|state| **state != ConsentState::Unknown)
647            .cloned()
648            .collect();
649
650        let mut groups = if includes_all {
651            // No filtering at all
652            self.raw_query(|conn| query.load::<StoredGroup>(conn))?
653        } else if includes_unknown {
654            // LEFT JOIN: include Unknown + NULL + filtered states
655            let left_joined_query = query
656                .left_join(consent_dsl::consent_records.on(
657                    sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
658                ))
659                .filter(
660                    consent_dsl::state
661                        .is_null()
662                        .or(consent_dsl::state.eq(ConsentState::Unknown))
663                        .or(consent_dsl::state.eq_any(filtered_states.clone())),
664                )
665                .select(dsl::groups::all_columns());
666
667            self.raw_query(|conn| left_joined_query.load::<StoredGroup>(conn))?
668        } else {
669            // INNER JOIN: strict match only to specific states (no Unknown or NULL)
670            let inner_joined_query = query
671                .inner_join(consent_dsl::consent_records.on(
672                    sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
673                ))
674                .filter(consent_dsl::state.eq_any(filtered_states.clone()))
675                .select(dsl::groups::all_columns());
676
677            self.raw_query(|conn| inner_joined_query.load::<StoredGroup>(conn))?
678        };
679
680        // Were sync groups explicitly asked for? Was the include_sync_groups flag set to true?
681        // Then query for those separately
682        if matches!(conversation_type, Some(ConversationType::Sync)) || *include_sync_groups {
683            let query = dsl::groups.filter(dsl::conversation_type.eq(ConversationType::Sync));
684            let mut sync_groups = self.raw_query(|conn| query.load(conn))?;
685            groups.append(&mut sync_groups);
686        }
687
688        Ok(groups)
689    }
690
691    #[xmtp_common::db_span]
692    fn find_groups_by_id_paged<A: AsRef<GroupQueryArgs>>(
693        &self,
694        args: A,
695        offset: i64,
696    ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
697        let GroupQueryArgs {
698            created_after_ns,
699            created_before_ns,
700            limit,
701            ..
702        } = args.as_ref();
703
704        let mut query = groups::table
705            .filter(groups::conversation_type.ne_all(ConversationType::virtual_types()))
706            .order(groups::id)
707            .into_boxed();
708
709        if let Some(start_ns) = created_after_ns {
710            query = query.filter(groups::created_at_ns.gt(start_ns));
711        }
712        if let Some(end_ns) = created_before_ns {
713            query = query.filter(groups::created_at_ns.le(end_ns));
714        }
715
716        query = query.limit(limit.unwrap_or(100)).offset(offset);
717
718        self.raw_query(|conn| query.load::<StoredGroup>(conn))
719    }
720
721    /// Updates group membership state
722    #[xmtp_common::db_span]
723    fn update_group_membership<Id: AsRef<[u8]>>(
724        &self,
725        group_id: Id,
726        state: GroupMembershipState,
727    ) -> Result<(), crate::ConnectionError> {
728        self.raw_query(|conn| {
729            diesel::update(dsl::groups.find(group_id.as_ref()))
730                .set(dsl::membership_state.eq(state))
731                .execute(conn)
732        })?;
733
734        Ok(())
735    }
736
737    #[xmtp_common::db_span]
738    fn all_sync_groups(&self) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
739        let query = dsl::groups
740            .order(dsl::created_at_ns.desc())
741            .filter(dsl::conversation_type.eq(ConversationType::Sync));
742
743        self.raw_query(|conn| query.load(conn))
744    }
745
746    #[xmtp_common::db_span]
747    fn find_sync_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
748        let query = dsl::groups
749            .filter(dsl::conversation_type.eq(ConversationType::Sync))
750            .filter(dsl::id.eq(id));
751
752        self.raw_query(|conn| query.first(conn).optional())
753    }
754
755    #[xmtp_common::db_span]
756    fn primary_sync_group(&self) -> Result<Option<StoredGroup>, crate::ConnectionError> {
757        let query = dsl::groups
758            .order(dsl::created_at_ns.desc())
759            .filter(dsl::conversation_type.eq(ConversationType::Sync));
760
761        self.raw_query(|conn| query.first(conn).optional())
762    }
763
764    /// Return a single group that matches the given ID
765    #[xmtp_common::db_span]
766    fn find_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
767        let query = dsl::groups
768            .order(dsl::created_at_ns.asc())
769            .limit(1)
770            .filter(dsl::id.eq(id));
771        let groups = self.raw_query(|conn| query.load(conn))?;
772
773        Ok(groups.into_iter().next())
774    }
775
776    /// Return a single group that matches the given welcome ID
777    #[xmtp_common::db_span]
778    fn find_group_by_sequence_id(
779        &self,
780        cursor: Cursor,
781    ) -> Result<Option<StoredGroup>, crate::ConnectionError> {
782        let query = dsl::groups
783            .order(dsl::created_at_ns.asc())
784            .filter(dsl::sequence_id.eq(cursor.0 as i64));
785
786        let groups = self.raw_query(|conn| query.load(conn))?;
787
788        if groups.len() > 1 {
789            tracing::warn!(
790                welcome_id = cursor.0,
791                "More than one group found for welcome_id {}",
792                cursor.0
793            );
794        }
795        Ok(groups.into_iter().next())
796    }
797
798    fn get_rotated_at_ns(&self, group_id: &GroupId) -> Result<i64, StorageError> {
799        let last_ts: Option<i64> = self.raw_query(|conn| {
800            dsl::groups
801                .find(&group_id)
802                .select(dsl::rotated_at_ns)
803                .first(conn)
804                .optional()
805        })?;
806
807        last_ts.ok_or(StorageError::NotFound(NotFound::InstallationTimeForGroup(
808            *group_id,
809        )))
810    }
811
812    /// Updates the 'last time checked' we checked for new installations.
813    fn update_rotated_at_ns(&self, group_id: &GroupId) -> Result<(), StorageError> {
814        self.raw_query(|conn| {
815            let now = xmtp_common::time::now_ns();
816            diesel::update(dsl::groups.find(group_id))
817                .set(dsl::rotated_at_ns.eq(now))
818                .execute(conn)
819        })?;
820
821        Ok(())
822    }
823
824    fn get_installations_time_checked(&self, group_id: &GroupId) -> Result<i64, StorageError> {
825        let last_ts = self.raw_query(|conn| {
826            dsl::groups
827                .find(&group_id)
828                .select(dsl::installations_last_checked)
829                .first(conn)
830                .optional()
831        })?;
832
833        last_ts.ok_or(NotFound::InstallationTimeForGroup(*group_id).into())
834    }
835
836    /// Updates the 'last time checked' we checked for new installations.
837    fn update_installations_time_checked(&self, group_id: &GroupId) -> Result<(), StorageError> {
838        self.raw_query(|conn| {
839            let now = xmtp_common::time::now_ns();
840            diesel::update(dsl::groups.find(group_id))
841                .set(dsl::installations_last_checked.eq(now))
842                .execute(conn)
843        })?;
844
845        Ok(())
846    }
847
848    fn update_message_disappearing_from_ns(
849        &self,
850        group_id: &GroupId,
851        from_ns: Option<i64>,
852    ) -> Result<(), StorageError> {
853        self.raw_query(|conn| {
854            diesel::update(dsl::groups.find(group_id))
855                .set(dsl::message_disappear_from_ns.eq(from_ns))
856                .execute(conn)
857        })?;
858
859        Ok(())
860    }
861
862    fn update_message_disappearing_in_ns(
863        &self,
864        group_id: &GroupId,
865        in_ns: Option<i64>,
866    ) -> Result<(), StorageError> {
867        self.raw_query(|conn| {
868            diesel::update(dsl::groups.find(group_id))
869                .set(dsl::message_disappear_in_ns.eq(in_ns))
870                .execute(conn)
871        })?;
872
873        Ok(())
874    }
875
876    fn insert_or_replace_group(&self, group: StoredGroup) -> Result<StoredGroup, StorageError> {
877        let maybe_inserted_group: Option<StoredGroup> = self.raw_query(|conn| {
878            diesel::insert_into(dsl::groups)
879                .values(&group)
880                .on_conflict_do_nothing()
881                .get_result(conn)
882                .optional()
883        })?;
884
885        if maybe_inserted_group.is_none() {
886            let mut existing_group: StoredGroup =
887                self.raw_query(|conn| dsl::groups.find(&group.id).first(conn))?;
888            // A restored group should be overwritten
889            if matches!(
890                existing_group.membership_state,
891                GroupMembershipState::Restored
892            ) {
893                self.raw_query(|c| {
894                    diesel::update(dsl::groups.find(&group.id))
895                        .set(&group)
896                        .execute(c)
897                })?;
898            }
899
900            if existing_group.sequence_id == group.sequence_id {
901                tracing::info!("Group welcome id already exists");
902                // Error so OpenMLS db transaction are rolled back on duplicate welcomes
903                Err(StorageError::Duplicate(DuplicateItem::WelcomeId(
904                    existing_group.cursor(),
905                )))
906            } else {
907                tracing::info!("Group already exists");
908                // If the welcome id is greater than the existing group welcome, update the welcome id
909                // on the existing group
910                if group.sequence_id.is_some()
911                    && (existing_group.sequence_id.is_none()
912                        || group.sequence_id > existing_group.sequence_id)
913                {
914                    self.raw_query(|c| {
915                        diesel::update(dsl::groups.find(&group.id))
916                            .set((dsl::sequence_id.eq(group.sequence_id),))
917                            .execute(c)
918                    })?;
919                    existing_group.sequence_id = group.sequence_id;
920                }
921                Ok(existing_group)
922            }
923        } else {
924            Ok(self.raw_query(|c| dsl::groups.find(group.id).first(c))?)
925        }
926    }
927
928    /// Get all the welcome ids turned into groups
929    fn group_cursors(&self) -> Result<Vec<Cursor>, crate::ConnectionError> {
930        self.raw_query(|conn| {
931            Ok(dsl::groups
932                .filter(dsl::sequence_id.is_not_null())
933                .select(dsl::sequence_id)
934                .load::<Option<i64>>(conn)?
935                .into_iter()
936                .flatten()
937                .map(|sequence| Cursor(sequence as u64))
938                .collect())
939        })
940    }
941
942    fn mark_group_as_maybe_forked(
943        &self,
944        group_id: &GroupId,
945        fork_details: String,
946    ) -> Result<(), StorageError> {
947        self.raw_query(|conn| {
948            diesel::update(dsl::groups.find(group_id))
949                .set((
950                    dsl::maybe_forked.eq(true),
951                    dsl::fork_details.eq(fork_details),
952                ))
953                .execute(conn)
954        })?;
955
956        Ok(())
957    }
958
959    fn clear_fork_flag_for_group(&self, group_id: &GroupId) -> Result<(), crate::ConnectionError> {
960        self.raw_query(|conn| {
961            diesel::update(dsl::groups.find(group_id))
962                .set((dsl::maybe_forked.eq(false), dsl::fork_details.eq("")))
963                .execute(conn)
964        })?;
965        Ok(())
966    }
967
968    fn has_duplicate_dm(&self, group_id: &GroupId) -> Result<bool, crate::ConnectionError> {
969        self.raw_query(|conn| {
970            let dm_id: Option<String> = dsl::groups
971                .filter(dsl::id.eq(group_id))
972                .select(dsl::dm_id)
973                .first::<Option<String>>(conn)
974                .optional()?
975                .flatten();
976
977            if let Some(dm_id) = dm_id {
978                let count: i64 = dsl::groups
979                    .filter(dsl::conversation_type.eq(ConversationType::Dm))
980                    .filter(dsl::dm_id.eq(dm_id))
981                    .count()
982                    .get_result(conn)?;
983
984                Ok(count > 1)
985            } else {
986                Ok(false)
987            }
988        })
989    }
990
991    /// Get conversation IDs for all conversations that require a remote commit log publish
992    /// (DMs and groups where user is super admin, excluding sync groups and rejected groups)
993    #[xmtp_common::db_span]
994    fn get_conversation_ids_for_remote_log_publish(
995        &self,
996    ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
997        use crate::schema::consent_records::dsl as consent_dsl;
998
999        let query = dsl::groups
1000            .filter(
1001                dsl::conversation_type
1002                    .eq(ConversationType::Dm)
1003                    .or(dsl::conversation_type
1004                        .eq(ConversationType::Group)
1005                        .and(dsl::should_publish_commit_log.eq(true))),
1006            )
1007            .inner_join(consent_dsl::consent_records.on(
1008                sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
1009            ))
1010            .filter(consent_dsl::state.eq(ConsentState::Allowed))
1011            .select((dsl::id, dsl::commit_log_public_key))
1012            .order(dsl::created_at_ns.asc());
1013
1014        self.raw_query(|conn| query.load::<StoredGroupCommitLogPublicKey>(conn))
1015    }
1016
1017    // All dms and groups that are not sync groups and have consent state Allowed
1018    #[xmtp_common::db_span]
1019    fn get_conversation_ids_for_remote_log_download(
1020        &self,
1021    ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
1022        use crate::schema::consent_records::dsl as consent_dsl;
1023
1024        let query = dsl::groups
1025            .filter(dsl::conversation_type.ne_all(ConversationType::virtual_types()))
1026            .inner_join(consent_dsl::consent_records.on(
1027                sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
1028            ))
1029            .filter(consent_dsl::state.eq(ConsentState::Allowed))
1030            .select((dsl::id, dsl::commit_log_public_key));
1031
1032        self.raw_query(|conn| query.load::<StoredGroupCommitLogPublicKey>(conn))
1033    }
1034
1035    // Get conversation IDs for fork checking (excludes already forked conversations and sync groups)
1036    #[xmtp_common::db_span]
1037    fn get_conversation_ids_for_fork_check(&self) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
1038        let query = dsl::groups
1039            .filter(
1040                dsl::conversation_type
1041                    .ne_all(ConversationType::virtual_types())
1042                    .and(
1043                        dsl::is_commit_log_forked
1044                            .is_null()
1045                            .or(dsl::is_commit_log_forked.ne(Some(true))),
1046                    ),
1047            )
1048            .select(dsl::id);
1049
1050        self.raw_query(|conn| query.load::<Vec<u8>>(conn))
1051    }
1052
1053    #[xmtp_common::db_span]
1054    fn get_conversation_ids_for_requesting_readds(
1055        &self,
1056    ) -> Result<Vec<StoredGroupForReaddRequest>, crate::ConnectionError> {
1057        use super::schema::{groups::dsl as groups_dsl, remote_commit_log::dsl as rcl_dsl};
1058        use diesel::dsl::max;
1059
1060        self.raw_query(|conn| {
1061            groups_dsl::groups
1062                .left_join(rcl_dsl::remote_commit_log.on(groups_dsl::id.eq(rcl_dsl::group_id)))
1063                .filter(
1064                    groups_dsl::conversation_type
1065                        .ne_all(ConversationType::virtual_types())
1066                        .and(groups_dsl::is_commit_log_forked.eq(true)),
1067                )
1068                .group_by(groups_dsl::id)
1069                .select((groups_dsl::id, max(rcl_dsl::commit_sequence_id).nullable()))
1070                .load::<StoredGroupForReaddRequest>(conn)
1071        })
1072    }
1073
1074    #[xmtp_common::db_span]
1075    fn get_conversation_ids_for_responding_readds(
1076        &self,
1077    ) -> Result<Vec<StoredGroupForRespondingReadds>, crate::ConnectionError> {
1078        use super::schema::{groups::dsl as groups_dsl, readd_status::dsl as readd_dsl};
1079        use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl};
1080
1081        self.raw_query(|conn| {
1082            readd_dsl::readd_status
1083                .inner_join(groups_dsl::groups.on(readd_dsl::group_id.eq(groups_dsl::id)))
1084                .filter(readd_dsl::requested_at_sequence_id.is_not_null())
1085                .filter(
1086                    readd_dsl::requested_at_sequence_id
1087                        .ge(readd_dsl::responded_at_sequence_id)
1088                        .or(readd_dsl::responded_at_sequence_id.is_null()),
1089                )
1090                .select((
1091                    groups_dsl::id,
1092                    groups_dsl::dm_id,
1093                    groups_dsl::conversation_type,
1094                    groups_dsl::created_at_ns,
1095                ))
1096                .distinct()
1097                .load::<StoredGroupForRespondingReadds>(conn)
1098        })
1099    }
1100
1101    #[xmtp_common::db_span]
1102    fn get_conversation_type(
1103        &self,
1104        group_id: &GroupId,
1105    ) -> Result<ConversationType, crate::ConnectionError> {
1106        let query = dsl::groups
1107            .filter(dsl::id.eq(group_id))
1108            .select(dsl::conversation_type);
1109        let conversation_type = self.raw_query(|conn| query.first(conn))?;
1110        Ok(conversation_type)
1111    }
1112
1113    fn set_group_commit_log_public_key(
1114        &self,
1115        group_id: &GroupId,
1116        public_key: &[u8],
1117    ) -> Result<(), StorageError> {
1118        use crate::schema::groups::dsl;
1119        let num_updated = self.raw_query(|conn| {
1120            diesel::update(dsl::groups)
1121                .filter(
1122                    dsl::id
1123                        .eq(group_id)
1124                        .and(dsl::commit_log_public_key.is_null()),
1125                )
1126                .set(dsl::commit_log_public_key.eq(public_key))
1127                .execute(conn)
1128        })?;
1129        if num_updated == 0 {
1130            return Err(StorageError::Duplicate(DuplicateItem::CommitLogPublicKey(
1131                group_id.as_ref().to_vec(),
1132            )));
1133        }
1134        Ok(())
1135    }
1136
1137    fn set_group_commit_log_forked_status(
1138        &self,
1139        group_id: &GroupId,
1140        is_forked: Option<bool>,
1141    ) -> Result<(), StorageError> {
1142        use crate::schema::groups::dsl;
1143        self.raw_query(|conn| {
1144            diesel::update(dsl::groups.find(group_id))
1145                .set(dsl::is_commit_log_forked.eq(is_forked))
1146                .execute(conn)
1147        })?;
1148        Ok(())
1149    }
1150
1151    fn get_group_commit_log_forked_status(
1152        &self,
1153        group_id: &GroupId,
1154    ) -> Result<Option<bool>, StorageError> {
1155        use crate::schema::groups::dsl;
1156        self.raw_query(|conn| {
1157            dsl::groups
1158                .find(group_id)
1159                .select(dsl::is_commit_log_forked)
1160                .first::<Option<bool>>(conn)
1161        })
1162        .map_err(StorageError::from)
1163    }
1164
1165    fn set_group_has_pending_leave_request_status(
1166        &self,
1167        group_id: &GroupId,
1168        has_pending_leave_request: Option<bool>,
1169    ) -> Result<(), StorageError> {
1170        use crate::schema::groups::dsl;
1171        self.raw_query(|conn| {
1172            diesel::update(dsl::groups.find(group_id))
1173                .set(dsl::has_pending_leave_request.eq(has_pending_leave_request))
1174                .execute(conn)
1175        })?;
1176        Ok(())
1177    }
1178
1179    #[xmtp_common::db_span]
1180    fn get_groups_have_pending_leave_request(
1181        &self,
1182    ) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
1183        let query = dsl::groups
1184            .filter(
1185                dsl::conversation_type
1186                    .ne(ConversationType::Sync)
1187                    .and(dsl::has_pending_leave_request.eq(Some(true))),
1188            )
1189            .select(dsl::id);
1190
1191        self.raw_query(|conn| query.load::<Vec<u8>>(conn))
1192    }
1193}
1194
1195#[repr(i32)]
1196#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, AsExpression, FromSqlRow)]
1197#[diesel(sql_type = Integer)]
1198/// Status of membership in a group, once a user sends a request to join
1199pub enum GroupMembershipState {
1200    /// User is allowed to interact with this Group
1201    Allowed = 1,
1202    /// User has been Rejected from this Group
1203    Rejected = 2,
1204    /// User is Pending acceptance to the Group
1205    Pending = 3,
1206    /// Group has been restored from an archive, but is not active yet.
1207    Restored = 4,
1208    /// User is Pending to get removed of the Group
1209    PendingRemove = 5,
1210}
1211
1212impl ToSql<Integer, Sqlite> for GroupMembershipState
1213where
1214    i32: ToSql<Integer, Sqlite>,
1215{
1216    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
1217        out.set_value(*self as i32);
1218        Ok(IsNull::No)
1219    }
1220}
1221
1222impl FromSql<Integer, Sqlite> for GroupMembershipState
1223where
1224    i32: FromSql<Integer, Sqlite>,
1225{
1226    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
1227        match i32::from_sql(bytes)? {
1228            1 => Ok(GroupMembershipState::Allowed),
1229            2 => Ok(GroupMembershipState::Rejected),
1230            3 => Ok(GroupMembershipState::Pending),
1231            4 => Ok(GroupMembershipState::Restored),
1232            5 => Ok(GroupMembershipState::PendingRemove),
1233            x => Err(format!("Unrecognized variant {}", x).into()),
1234        }
1235    }
1236}
1237
1238pub use xmtp_proto::types::ConversationType;
1239
1240pub trait DmIdExt {
1241    fn other_inbox_id(&self, id: &str) -> String;
1242}
1243
1244impl DmIdExt for String {
1245    fn other_inbox_id(&self, id: &str) -> String {
1246        // drop the "dm:"
1247        let dm_id = &self[3..];
1248
1249        // If my id is the first half, return the second half, otherwise return first half
1250        let target_inbox = if dm_id[..id.len()] == *id {
1251            // + 1 because there is a colon (:)
1252            &dm_id[(id.len() + 1)..]
1253        } else {
1254            &dm_id[..id.len()]
1255        };
1256
1257        target_inbox.to_string()
1258    }
1259}
1260
1261#[cfg(test)]
1262pub(crate) mod tests {
1263    pub use super::dms::tests::*;
1264    use super::*;
1265
1266    use crate::{
1267        Fetch, Store,
1268        consent_record::{ConsentType, StoredConsentRecord},
1269        readd_status::ReaddStatus,
1270        schema::groups::dsl::groups,
1271        test_utils::{with_connection, with_connection_async},
1272    };
1273    use xmtp_common::{Generate, assert_ok, rand_vec, time::now_ns};
1274
1275    /// Generate a test group
1276    pub fn generate_group(state: Option<GroupMembershipState>) -> StoredGroup {
1277        // Default behavior: Use `now_ns()` as the creation time
1278        generate_group_with_created_at(state, now_ns())
1279    }
1280
1281    pub fn generate_group_with_created_at(
1282        state: Option<GroupMembershipState>,
1283        created_at_ns: i64,
1284    ) -> StoredGroup {
1285        let id = GroupId::generate();
1286        let membership_state = state.unwrap_or(GroupMembershipState::Allowed);
1287        StoredGroup::builder()
1288            .id(id)
1289            .created_at_ns(created_at_ns)
1290            .membership_state(membership_state)
1291            .added_by_inbox_id("placeholder_address")
1292            .build()
1293            .unwrap()
1294    }
1295
1296    /// Generate a test group with welcome
1297    pub fn generate_group_with_welcome(
1298        state: Option<GroupMembershipState>,
1299        welcome_id: Option<i64>,
1300    ) -> StoredGroup {
1301        let id = GroupId::generate();
1302        let created_at_ns = now_ns();
1303        let membership_state = state.unwrap_or(GroupMembershipState::Allowed);
1304        StoredGroup::builder()
1305            .id(id)
1306            .created_at_ns(created_at_ns)
1307            .membership_state(membership_state)
1308            .added_by_inbox_id("placeholder_address")
1309            .sequence_id(welcome_id.unwrap_or(xmtp_common::rand_i64()))
1310            .conversation_type(ConversationType::Group)
1311            .build()
1312            .unwrap()
1313    }
1314
1315    /// Generate a test consent
1316    pub fn generate_consent_record(
1317        entity_type: ConsentType,
1318        state: ConsentState,
1319        entity: String,
1320    ) -> StoredConsentRecord {
1321        StoredConsentRecord {
1322            entity_type,
1323            state,
1324            entity,
1325            consented_at_ns: now_ns(),
1326        }
1327    }
1328
1329    #[xmtp_common::test]
1330    fn test_it_stores_group() {
1331        with_connection(|conn| {
1332            let test_group = generate_group(None);
1333
1334            test_group.store(conn).unwrap();
1335            assert_eq!(
1336                conn.raw_query(|raw_conn| groups.first::<StoredGroup>(raw_conn))
1337                    .unwrap(),
1338                test_group
1339            );
1340        })
1341    }
1342
1343    #[xmtp_common::test]
1344    fn test_it_fetches_group() {
1345        with_connection(|conn| {
1346            let test_group = generate_group(None);
1347
1348            conn.raw_query(|raw_conn| {
1349                diesel::insert_into(groups)
1350                    .values(test_group.clone())
1351                    .execute(raw_conn)
1352            })
1353            .unwrap();
1354
1355            let fetched_group: Option<StoredGroup> = conn.fetch(&test_group.id).unwrap();
1356            assert_eq!(fetched_group, Some(test_group));
1357        })
1358    }
1359
1360    #[xmtp_common::test]
1361    fn test_it_updates_group_membership_state() {
1362        with_connection(|conn| {
1363            let test_group = generate_group(Some(GroupMembershipState::Pending));
1364
1365            test_group.store(conn).unwrap();
1366            conn.update_group_membership(test_group.id, GroupMembershipState::Rejected)
1367                .unwrap();
1368
1369            let updated_group: StoredGroup = conn.fetch(&test_group.id).ok().flatten().unwrap();
1370            assert_eq!(
1371                updated_group,
1372                StoredGroup {
1373                    membership_state: GroupMembershipState::Rejected,
1374                    ..test_group
1375                }
1376            );
1377        })
1378    }
1379
1380    #[xmtp_common::test]
1381    async fn test_find_groups() {
1382        let wait_in_wasm = async || {
1383            // web has current time resolution only to millisecond,
1384            // which is too slow for this test to pass and the timestamps to be different
1385            // force generated groups to be created at different times
1386
1387            if cfg!(target_arch = "wasm32") {
1388                xmtp_common::time::sleep(std::time::Duration::from_millis(1)).await;
1389            }
1390        };
1391        with_connection_async(|conn| async move {
1392            let test_group_1 = generate_group(Some(GroupMembershipState::Pending));
1393            test_group_1.store(&conn).unwrap();
1394            wait_in_wasm().await;
1395            let test_group_2 = generate_group(Some(GroupMembershipState::Allowed));
1396            test_group_2.store(&conn).unwrap();
1397            wait_in_wasm().await;
1398            let test_group_3 = generate_dm(Some(GroupMembershipState::Allowed));
1399            test_group_3.store(&conn).unwrap();
1400
1401            let other_inbox_id = test_group_3
1402                .dm_id
1403                .unwrap()
1404                .other_inbox_id("placeholder_inbox_id_1");
1405
1406            let all_results = conn
1407                .find_groups(GroupQueryArgs {
1408                    conversation_type: Some(ConversationType::Group),
1409                    ..Default::default()
1410                })
1411                .unwrap();
1412            assert_eq!(all_results.len(), 2);
1413
1414            let pending_results = conn
1415                .find_groups(GroupQueryArgs {
1416                    allowed_states: Some(vec![GroupMembershipState::Pending]),
1417                    conversation_type: Some(ConversationType::Group),
1418                    ..Default::default()
1419                })
1420                .unwrap();
1421            assert_eq!(pending_results[0].id, test_group_1.id);
1422            assert_eq!(pending_results.len(), 1);
1423
1424            // Offset and limit
1425            let results_with_limit = conn
1426                .find_groups(GroupQueryArgs {
1427                    conversation_type: Some(ConversationType::Group),
1428                    limit: Some(1),
1429                    ..Default::default()
1430                })
1431                .unwrap();
1432            assert_eq!(results_with_limit.len(), 1);
1433            assert_eq!(results_with_limit[0].id, test_group_1.id);
1434
1435            let results_with_created_at_ns_after = conn
1436                .find_groups(GroupQueryArgs {
1437                    conversation_type: Some(ConversationType::Group),
1438                    limit: Some(1),
1439                    created_after_ns: Some(test_group_1.created_at_ns),
1440                    ..Default::default()
1441                })
1442                .unwrap();
1443            assert_eq!(results_with_created_at_ns_after.len(), 1);
1444            assert_eq!(results_with_created_at_ns_after[0].id, test_group_2.id);
1445
1446            // Sync groups SHOULD NOT be returned
1447            let synced_groups = conn.primary_sync_group().unwrap();
1448            assert!(synced_groups.is_none());
1449
1450            // test that dm groups are included
1451            let dm_results = conn.find_groups(GroupQueryArgs::default()).unwrap();
1452            assert_eq!(dm_results.len(), 3);
1453            assert_eq!(dm_results[2].id, test_group_3.id);
1454
1455            // test find_dm_group
1456            let dm_result = conn
1457                .find_active_dm_group(format!("dm:placeholder_inbox_id_1:{}", other_inbox_id))
1458                .unwrap();
1459            assert!(dm_result.is_some());
1460
1461            // test only dms are returned
1462            let dm_results = conn
1463                .find_groups(GroupQueryArgs {
1464                    conversation_type: Some(ConversationType::Dm),
1465                    ..Default::default()
1466                })
1467                .unwrap();
1468            assert_eq!(dm_results.len(), 1);
1469            assert_eq!(dm_results[0].id, test_group_3.id);
1470        })
1471        .await
1472    }
1473
1474    #[xmtp_common::test]
1475    async fn test_installations_last_checked_is_updated() {
1476        with_connection_async(|conn| async move {
1477            let test_group = generate_group(None);
1478            test_group.store(&conn).unwrap();
1479
1480            // Check that the installations update has not been performed, yet
1481            assert_eq!(test_group.installations_last_checked, 0);
1482
1483            if cfg!(target_arch = "wasm32") {
1484                // web has current time resolution only to millisecond,
1485                // which is too slow for this test to pass and the timestamps to be different
1486                xmtp_common::time::sleep(std::time::Duration::from_millis(1)).await;
1487            }
1488            // Check that some event occurred which triggers an installation list update.
1489            // Here we invoke that event directly
1490            let result = conn.update_installations_time_checked(&test_group.id);
1491            assert_ok!(result);
1492
1493            // Check that the latest installation list timestamp has been updated
1494            let fetched_group: StoredGroup = conn.fetch(&test_group.id).ok().flatten().unwrap();
1495            assert_ne!(fetched_group.installations_last_checked, 0);
1496            assert!(fetched_group.created_at_ns < fetched_group.installations_last_checked);
1497        })
1498        .await
1499    }
1500
1501    #[xmtp_common::test]
1502    fn test_new_group_has_correct_purpose() {
1503        with_connection(|conn| {
1504            let test_group = generate_group(None);
1505
1506            conn.raw_query(|raw_conn| {
1507                diesel::insert_into(groups)
1508                    .values(test_group.clone())
1509                    .execute(raw_conn)
1510            })
1511            .unwrap();
1512
1513            let fetched_group: Option<StoredGroup> = conn.fetch(&test_group.id).unwrap();
1514            assert_eq!(fetched_group, Some(test_group));
1515            let conversation_type = fetched_group.unwrap().conversation_type;
1516            assert_eq!(conversation_type, ConversationType::Group);
1517        })
1518    }
1519
1520    #[xmtp_common::test]
1521    fn test_find_groups_by_consent_state() {
1522        with_connection(|conn| {
1523            let test_group_1 = generate_group(Some(GroupMembershipState::Allowed));
1524            test_group_1.store(conn).unwrap();
1525            let test_group_2 = generate_group(Some(GroupMembershipState::Allowed));
1526            test_group_2.store(conn).unwrap();
1527            let test_group_3 = generate_dm(Some(GroupMembershipState::Allowed));
1528            test_group_3.store(conn).unwrap();
1529            let test_group_4 = generate_dm(Some(GroupMembershipState::Allowed));
1530            test_group_4.store(conn).unwrap();
1531
1532            let test_group_1_consent = generate_consent_record(
1533                ConsentType::ConversationId,
1534                ConsentState::Allowed,
1535                hex::encode(test_group_1.id),
1536            );
1537            test_group_1_consent.store(conn).unwrap();
1538            let test_group_2_consent = generate_consent_record(
1539                ConsentType::ConversationId,
1540                ConsentState::Denied,
1541                hex::encode(test_group_2.id),
1542            );
1543            test_group_2_consent.store(conn).unwrap();
1544            let test_group_3_consent = generate_consent_record(
1545                ConsentType::ConversationId,
1546                ConsentState::Allowed,
1547                hex::encode(test_group_3.id),
1548            );
1549            test_group_3_consent.store(conn).unwrap();
1550
1551            let all_results = conn
1552                .find_groups(GroupQueryArgs {
1553                    consent_states: Some(vec![
1554                        ConsentState::Allowed,
1555                        ConsentState::Unknown,
1556                        ConsentState::Denied,
1557                    ]),
1558                    ..Default::default()
1559                })
1560                .unwrap();
1561            assert_eq!(all_results.len(), 4);
1562
1563            let default_results = conn.find_groups(GroupQueryArgs::default()).unwrap();
1564            assert_eq!(default_results.len(), 3);
1565
1566            let allowed_results = conn
1567                .find_groups(GroupQueryArgs {
1568                    consent_states: Some(vec![ConsentState::Allowed]),
1569                    ..Default::default()
1570                })
1571                .unwrap();
1572            assert_eq!(allowed_results.len(), 2);
1573
1574            let allowed_unknown_results = conn
1575                .find_groups(GroupQueryArgs {
1576                    consent_states: Some(vec![ConsentState::Allowed, ConsentState::Unknown]),
1577                    ..Default::default()
1578                })
1579                .unwrap();
1580            assert_eq!(allowed_unknown_results.len(), 3);
1581
1582            let denied_results = conn
1583                .find_groups(GroupQueryArgs {
1584                    consent_states: Some(vec![ConsentState::Denied]),
1585                    ..Default::default()
1586                })
1587                .unwrap();
1588            assert_eq!(denied_results.len(), 1);
1589            assert_eq!(denied_results[0].id, test_group_2.id);
1590
1591            let unknown_results = conn
1592                .find_groups(GroupQueryArgs {
1593                    consent_states: Some(vec![ConsentState::Unknown]),
1594                    ..Default::default()
1595                })
1596                .unwrap();
1597            assert_eq!(unknown_results.len(), 1);
1598            assert_eq!(unknown_results[0].id, test_group_4.id);
1599
1600            let empty_array_results = conn
1601                .find_groups(GroupQueryArgs {
1602                    consent_states: Some(vec![]),
1603                    ..Default::default()
1604                })
1605                .unwrap();
1606            assert_eq!(empty_array_results.len(), 3);
1607        })
1608    }
1609
1610    #[xmtp_common::test]
1611    fn test_get_sequence_ids() {
1612        with_connection(|conn| {
1613            let mls_groups = [
1614                generate_group_with_welcome(None, Some(30)),
1615                generate_group(None),
1616                generate_group(None),
1617                generate_group_with_welcome(None, Some(10)),
1618            ];
1619            for g in mls_groups.iter() {
1620                g.store(conn).unwrap();
1621            }
1622            assert_eq!(
1623                vec![30, 10],
1624                conn.group_cursors()
1625                    .unwrap()
1626                    .into_iter()
1627                    .map(|c| c.0)
1628                    .collect::<Vec<u64>>()
1629            );
1630        })
1631    }
1632
1633    #[xmtp_common::test(unwrap_try = true)]
1634    fn test_insert_or_replace_group_update_preserves_cursor() {
1635        with_connection(|conn| {
1636            let group = generate_group(None);
1637            assert!(group.sequence_id.is_none());
1638            group.store(conn).unwrap();
1639            conn.insert_or_replace_group(StoredGroup {
1640                sequence_id: Some(5),
1641                ..group.clone()
1642            })
1643            .unwrap();
1644            let stored: StoredGroup = conn.fetch(&group.id).unwrap().unwrap();
1645            assert_eq!(stored.sequence_id, Some(5));
1646            assert_eq!(conn.group_cursors().unwrap(), vec![Cursor(5)]);
1647        });
1648    }
1649
1650    #[xmtp_common::test]
1651    fn test_find_group_default_excludes_denied() {
1652        with_connection(|conn| {
1653            // Create three groups: one allowed, one denied, one unknown (no consent)
1654            let allowed_group = generate_group(Some(GroupMembershipState::Allowed));
1655            allowed_group.store(conn).unwrap();
1656
1657            let denied_group = generate_group(Some(GroupMembershipState::Allowed));
1658            denied_group.store(conn).unwrap();
1659
1660            let unknown_group = generate_group(Some(GroupMembershipState::Allowed));
1661            unknown_group.store(conn).unwrap();
1662
1663            // Create consent records for allowed and denied; leave unknown_group without one
1664            let allowed_consent = generate_consent_record(
1665                ConsentType::ConversationId,
1666                ConsentState::Allowed,
1667                hex::encode(allowed_group.id),
1668            );
1669            allowed_consent.store(conn).unwrap();
1670
1671            let denied_consent = generate_consent_record(
1672                ConsentType::ConversationId,
1673                ConsentState::Denied,
1674                hex::encode(denied_group.id),
1675            );
1676            denied_consent.store(conn).unwrap();
1677
1678            // Query using default args (no consent_states specified)
1679            let default_results = conn.find_groups(GroupQueryArgs::default()).unwrap();
1680
1681            // Expect to include only: allowed_group and unknown_group (2 total)
1682            assert_eq!(default_results.len(), 2);
1683            let returned_ids: Vec<_> = default_results.iter().map(|g| &g.id).collect();
1684            assert!(returned_ids.contains(&&allowed_group.id));
1685            assert!(returned_ids.contains(&&unknown_group.id));
1686            assert!(!returned_ids.contains(&&denied_group.id));
1687        })
1688    }
1689
1690    #[xmtp_common::test(unwrap_try = true)]
1691    fn test_get_conversation_ids_for_remote_log_publish() {
1692        with_connection(|conn| {
1693            let mut group1 = generate_group(None);
1694            let mut group2 = generate_group(None);
1695            let mut group3 = generate_group(None);
1696            let mut group4 = generate_group(None);
1697            group1.should_publish_commit_log = true;
1698            group1.commit_log_public_key = None;
1699            generate_consent_record(
1700                ConsentType::ConversationId,
1701                ConsentState::Allowed,
1702                hex::encode(group1.id),
1703            )
1704            .store(conn)?;
1705            group2.should_publish_commit_log = true;
1706            group2.commit_log_public_key = Some(rand_vec::<32>());
1707
1708            group3.should_publish_commit_log = true;
1709            group3.commit_log_public_key = Some(rand_vec::<32>());
1710            generate_consent_record(
1711                ConsentType::ConversationId,
1712                ConsentState::Allowed,
1713                hex::encode(group3.id),
1714            )
1715            .store(conn)?;
1716            group4.should_publish_commit_log = false;
1717            group1.store(conn)?;
1718            group2.store(conn)?;
1719            group3.store(conn)?;
1720            group4.store(conn)?;
1721
1722            let commit_log_keys = conn.get_conversation_ids_for_remote_log_publish().unwrap();
1723            assert_eq!(commit_log_keys.len(), 2);
1724            assert_eq!(commit_log_keys[0].id, group1.id);
1725            assert_eq!(commit_log_keys[1].id, group3.id);
1726            assert_eq!(commit_log_keys[0].commit_log_public_key, None);
1727            assert_eq!(
1728                commit_log_keys[1].commit_log_public_key,
1729                group3.commit_log_public_key
1730            );
1731        })
1732    }
1733
1734    #[xmtp_common::test]
1735    fn test_get_conversation_ids_for_remote_log_publish_with_consent() {
1736        with_connection(|conn| {
1737            // Create groups: one with Allowed consent, one with Denied consent, one with no consent
1738            let mut allowed_group = generate_group(None);
1739            allowed_group.should_publish_commit_log = true;
1740            allowed_group.store(conn).unwrap();
1741
1742            let mut denied_group = generate_group(None);
1743            denied_group.should_publish_commit_log = true;
1744            denied_group.store(conn).unwrap();
1745
1746            let mut no_consent_group = generate_group(None);
1747            no_consent_group.should_publish_commit_log = true;
1748            no_consent_group.store(conn).unwrap();
1749
1750            // Create consent records
1751            let allowed_consent = generate_consent_record(
1752                ConsentType::ConversationId,
1753                ConsentState::Allowed,
1754                hex::encode(allowed_group.id),
1755            );
1756            allowed_consent.store(conn).unwrap();
1757
1758            let denied_consent = generate_consent_record(
1759                ConsentType::ConversationId,
1760                ConsentState::Denied,
1761                hex::encode(denied_group.id),
1762            );
1763            denied_consent.store(conn).unwrap();
1764
1765            // Function should only return groups with Allowed consent state
1766            let commit_log_keys = conn.get_conversation_ids_for_remote_log_publish().unwrap();
1767            assert_eq!(commit_log_keys.len(), 1);
1768            assert_eq!(commit_log_keys[0].id, allowed_group.id);
1769        })
1770    }
1771
1772    #[xmtp_common::test]
1773    fn test_get_conversation_ids_for_remote_log_download_with_consent() {
1774        with_connection(|conn| {
1775            // Create groups: one with Allowed consent, one with Denied consent, one with no consent
1776            let allowed_group = generate_group(None);
1777            allowed_group.store(conn).unwrap();
1778
1779            let denied_group = generate_group(None);
1780            denied_group.store(conn).unwrap();
1781
1782            let no_consent_group = generate_group(None);
1783            no_consent_group.store(conn).unwrap();
1784
1785            // Create a sync group (should be excluded regardless of consent)
1786            let mut sync_group = generate_group(None);
1787            sync_group.conversation_type = ConversationType::Sync;
1788            sync_group.store(conn).unwrap();
1789            let sync_consent = generate_consent_record(
1790                ConsentType::ConversationId,
1791                ConsentState::Allowed,
1792                hex::encode(sync_group.id),
1793            );
1794            sync_consent.store(conn).unwrap();
1795
1796            // Create consent records
1797            let allowed_consent = generate_consent_record(
1798                ConsentType::ConversationId,
1799                ConsentState::Allowed,
1800                hex::encode(allowed_group.id),
1801            );
1802            allowed_consent.store(conn).unwrap();
1803
1804            let denied_consent = generate_consent_record(
1805                ConsentType::ConversationId,
1806                ConsentState::Denied,
1807                hex::encode(denied_group.id),
1808            );
1809            denied_consent.store(conn).unwrap();
1810
1811            // Function should only return groups with Allowed consent state, excluding sync groups
1812            let conversation_ids = conn.get_conversation_ids_for_remote_log_download().unwrap();
1813            assert_eq!(conversation_ids.len(), 1);
1814            assert_eq!(conversation_ids[0].id, allowed_group.id);
1815        })
1816    }
1817
1818    #[xmtp_common::test]
1819    fn test_get_conversation_ids_for_responding_readds() {
1820        with_connection(|conn| {
1821            // Create test groups
1822            let group_id_1 = GroupId::ONE;
1823            let group_id_2 = GroupId::TWO;
1824            let group_id_3 = GroupId::THREE;
1825
1826            let group1 = StoredGroup::builder()
1827                .id(group_id_1)
1828                .created_at_ns(1000)
1829                .membership_state(GroupMembershipState::Allowed)
1830                .added_by_inbox_id("placeholder_address")
1831                .build()
1832                .unwrap();
1833            group1.store(conn).unwrap();
1834
1835            let group2 = StoredGroup::builder()
1836                .id(group_id_2)
1837                .created_at_ns(2000)
1838                .membership_state(GroupMembershipState::Allowed)
1839                .added_by_inbox_id("placeholder_address")
1840                .build()
1841                .unwrap();
1842            group2.store(conn).unwrap();
1843
1844            let group3 = StoredGroup::builder()
1845                .id(group_id_3)
1846                .created_at_ns(3000)
1847                .membership_state(GroupMembershipState::Allowed)
1848                .added_by_inbox_id("placeholder_address")
1849                .build()
1850                .unwrap();
1851            group3.store(conn).unwrap();
1852
1853            // Create readd status entries with various test cases
1854            let test_cases = vec![
1855                // Case 1: Pending readd (requested_at > responded_at)
1856                ReaddStatus {
1857                    group_id: group_id_1,
1858                    installation_id: vec![1],
1859                    requested_at_sequence_id: Some(10),
1860                    responded_at_sequence_id: Some(5),
1861                },
1862                // Case 2: Pending readd (responded_at is None)
1863                ReaddStatus {
1864                    group_id: group_id_1,
1865                    installation_id: vec![2],
1866                    requested_at_sequence_id: Some(8),
1867                    responded_at_sequence_id: None,
1868                },
1869                // Case 4: Not pending (requested_at < responded_at)
1870                ReaddStatus {
1871                    group_id: group_id_2,
1872                    installation_id: vec![4],
1873                    requested_at_sequence_id: Some(12),
1874                    responded_at_sequence_id: Some(15),
1875                },
1876                // Case 5: Not pending (requested_at is None)
1877                ReaddStatus {
1878                    group_id: group_id_2,
1879                    installation_id: vec![5],
1880                    requested_at_sequence_id: None,
1881                    responded_at_sequence_id: Some(20),
1882                },
1883                // Case 6: Pending readd (requested_at == responded_at, should be pending)
1884                ReaddStatus {
1885                    group_id: group_id_3,
1886                    installation_id: vec![6],
1887                    requested_at_sequence_id: Some(25),
1888                    responded_at_sequence_id: Some(25),
1889                },
1890            ];
1891
1892            // Store all test cases
1893            for status in test_cases {
1894                status.store(conn).unwrap();
1895            }
1896
1897            // Call the method under test
1898            let result = conn.get_conversation_ids_for_responding_readds().unwrap();
1899
1900            // Should return groups 1 and 3 (both have pending readd requests)
1901            // Group 2 has no pending readds
1902            assert_eq!(result.len(), 2);
1903
1904            // Results should be sorted by group_id (since we used distinct())
1905            let mut result_group_ids: Vec<GroupId> = result.iter().map(|r| r.group_id).collect();
1906            result_group_ids.sort();
1907
1908            assert_eq!(result_group_ids[0], group_id_1);
1909            assert_eq!(result_group_ids[1], group_id_3);
1910
1911            // Check that the correct metadata is returned
1912            let group1_result = result.iter().find(|r| r.group_id == group_id_1).unwrap();
1913            assert_eq!(group1_result.dm_id, None);
1914            assert_eq!(group1_result.conversation_type, ConversationType::Group);
1915            assert_eq!(group1_result.created_at_ns, 1000);
1916
1917            let group3_result = result.iter().find(|r| r.group_id == group_id_3).unwrap();
1918            assert_eq!(group3_result.dm_id, None);
1919            assert_eq!(group3_result.conversation_type, ConversationType::Group);
1920            assert_eq!(group3_result.created_at_ns, 3000);
1921        })
1922    }
1923
1924    /// Regression guard for the `find_group` query-span instrumentation.
1925    ///
1926    /// `find_group` is annotated with `#[xmtp_common::db_span]`, which expands to
1927    /// `#[tracing::instrument(err, skip_all, fields(operation = "db.find_group"))]`.
1928    /// Two contracts must hold for the DB telemetry to be useful and safe:
1929    ///   1. the span carries `operation = "db.find_group"` (the metric dimension
1930    ///      consumed downstream), and
1931    ///   2. `skip_all` keeps the raw `group_id` argument OUT of the span — a leak
1932    ///      of the per-group id as a span field would explode metric cardinality.
1933    ///
1934    /// Capture mechanism: a tiny in-crate `tracing::Subscriber` that records the
1935    /// fields of every span created while it is the default dispatcher. We do NOT
1936    /// use `xmtp_common::traced_test!` / a `tracing_subscriber` fmt subscriber
1937    /// because `xmtp_db` does not depend on the `tracing-subscriber` crate (only
1938    /// `tracing` is a direct dependency), so naming it in source would fail to
1939    /// compile. A custom `Subscriber` also lets us read span *attributes* directly
1940    /// at `new_span` time, which is exactly where the `instrument` macro records
1941    /// the static `operation` field — no event needs to fire inside the span and
1942    /// no span-event/`FmtSpan` configuration is required, so the assertion is
1943    /// deterministic and not flaky. Scoping via `with_default` around only the
1944    /// `find_group` call keeps unrelated framework spans out of the buffer.
1945    #[test]
1946    fn test_find_group_span_emits_operation_and_skips_group_id() {
1947        use std::sync::{
1948            Arc,
1949            atomic::{AtomicU64, Ordering},
1950        };
1951        use tracing::field::{Field, Visit};
1952
1953        /// Records `field=Debug(value)` pairs for every span created while
1954        /// installed, appending them to a shared, thread-safe buffer.
1955        #[derive(Clone, Default)]
1956        struct CaptureSubscriber {
1957            buf: Arc<parking_lot::Mutex<String>>,
1958            next_id: Arc<AtomicU64>,
1959        }
1960
1961        struct FieldVisitor<'a>(&'a mut String);
1962        impl Visit for FieldVisitor<'_> {
1963            fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1964                self.0.push_str(field.name());
1965                self.0.push('=');
1966                self.0.push_str(&format!("{value:?}"));
1967                self.0.push(' ');
1968            }
1969        }
1970
1971        impl tracing::Subscriber for CaptureSubscriber {
1972            fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
1973                true
1974            }
1975
1976            fn new_span(&self, attrs: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1977                let mut line = String::new();
1978                line.push_str("SPAN ");
1979                line.push_str(attrs.metadata().name());
1980                line.push_str(" {");
1981                let mut visitor = FieldVisitor(&mut line);
1982                attrs.record(&mut visitor);
1983                line.push_str("}\n");
1984                self.buf.lock().push_str(&line);
1985
1986                // Hand out a non-zero, monotonically increasing id per span.
1987                let id = self.next_id.fetch_add(1, Ordering::Relaxed) + 1;
1988                tracing::span::Id::from_u64(id)
1989            }
1990
1991            fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
1992            fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
1993            }
1994            fn event(&self, _event: &tracing::Event<'_>) {}
1995            fn enter(&self, _span: &tracing::span::Id) {}
1996            fn exit(&self, _span: &tracing::span::Id) {}
1997        }
1998
1999        with_connection(|conn| {
2000            // Insert a group so `find_group` exercises a real (Ok) query path.
2001            let test_group = generate_group(None);
2002            conn.raw_query(|raw_conn| {
2003                diesel::insert_into(groups)
2004                    .values(test_group.clone())
2005                    .execute(raw_conn)
2006            })
2007            .unwrap();
2008
2009            let capture = CaptureSubscriber::default();
2010
2011            // Scope the subscriber tightly around the single instrumented call so
2012            // only `find_group`'s span lands in the buffer.
2013            tracing::subscriber::with_default(capture.clone(), || {
2014                // `find_group` is synchronous; no runtime needed for the call.
2015                let _ = conn.find_group(&test_group.id);
2016            });
2017
2018            let logged = capture.buf.lock().clone();
2019
2020            // Contract 1: the operation metric dimension is present.
2021            assert!(
2022                logged.contains("operation=\"db.find_group\""),
2023                "expected find_group span to carry operation=\"db.find_group\", got:\n{logged}"
2024            );
2025
2026            // Contract 2: skip_all must keep the raw arg out of the span. The arg
2027            // is `id: &GroupId`, so a leak shows up as an `id=` field (GroupId's
2028            // Debug renders `GroupId(<hex>)`). Asserting on the parameter name `id=`
2029            // (not the substring "group_id", which `GroupId(..)` would not contain)
2030            // makes this a real regression guard: dropping skip_all would fail it.
2031            assert!(
2032                !logged.contains("id="),
2033                "skip_all contract violated: the `id` arg leaked into the find_group \
2034                 span as a field (cardinality risk), got:\n{logged}"
2035            );
2036            // Stronger: only `operation` plus the static `sentry.*` and `otel.*`
2037            // vendor hints (emitted by the span macros for Sentry op/name mapping
2038            // and for the exported OpenTelemetry span name) may appear — every one
2039            // is a compile-time literal, so the cardinality contract holds.
2040            let fields = logged
2041                .split_once('{')
2042                .and_then(|(_, rest)| rest.split_once('}'))
2043                .map(|(inner, _)| inner.trim())
2044                .unwrap_or("");
2045            assert_eq!(
2046                fields,
2047                "operation=\"db.find_group\" sentry.op=\"db\" sentry.name=\"db.find_group\" \
2048                 otel.name=\"db.find_group\"",
2049                "find_group span must carry only operation + static sentry.*/otel.* fields, \
2050                 got: {fields:?}"
2051            );
2052        })
2053    }
2054}