1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use diesel::{
5 backend::Backend,
6 connection::DefaultLoadingMode,
7 deserialize::{self, FromSql, FromSqlRow},
8 expression::AsExpression,
9 prelude::*,
10 serialize::{self, IsNull, Output, ToSql},
11 sql_types::Integer,
12};
13use itertools::Itertools;
14use serde::{Deserialize, Serialize};
15use xmtp_common::fmt;
16use xmtp_proto::types::{Cursor, GroupId};
17
18use super::{
19 ConnectionExt, Sqlite,
20 db_connection::DbConnection,
21 schema::group_intents::{self, dsl},
22};
23use crate::{
24 Delete, NotFound, StorageError, group_message::QueryGroupMessage, impl_fetch, impl_store,
25};
26
27mod error;
28mod prepared;
29mod types;
30pub use error::*;
31pub use prepared::*;
32pub use types::*;
33
34pub type ID = i32;
35
36#[repr(i32)]
37#[derive(
38 Debug,
39 Clone,
40 Copy,
41 PartialEq,
42 Eq,
43 AsExpression,
44 FromSqlRow,
45 Serialize,
46 Deserialize,
47 strum::EnumIter,
48)]
49#[diesel(sql_type = Integer)]
50pub enum IntentKind {
51 SendMessage = 1,
52 KeyUpdate = 2,
53 MetadataUpdate = 3,
54 UpdateGroupMembership = 4,
55 UpdateAdminList = 5,
56 UpdatePermission = 6,
57 ReaddInstallations = 7,
58 ProposeMemberUpdate = 8,
59 ProposeGroupContextExtensions = 9,
60 CommitPendingProposals = 10,
61 #[doc(alias = "AppData migration")]
69 BootstrapMigration = 11,
70 AppDataUpdate = 12,
82}
83
84impl IntentKind {
85 pub fn all() -> impl Iterator<Item = IntentKind> {
101 use strum::IntoEnumIterator;
102 IntentKind::iter()
103 }
104}
105
106impl std::fmt::Display for IntentKind {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 let description = match self {
109 IntentKind::SendMessage => "SendMessage",
110 IntentKind::KeyUpdate => "KeyUpdate",
111 IntentKind::MetadataUpdate => "MetadataUpdate",
112 IntentKind::UpdateGroupMembership => "UpdateGroupMembership",
113 IntentKind::UpdateAdminList => "UpdateAdminList",
114 IntentKind::UpdatePermission => "UpdatePermission",
115 IntentKind::ReaddInstallations => "ReaddInstallations",
116 IntentKind::ProposeMemberUpdate => "ProposeMemberUpdate",
117 IntentKind::ProposeGroupContextExtensions => "ProposeGroupContextExtensions",
118 IntentKind::CommitPendingProposals => "CommitPendingProposals",
119 IntentKind::BootstrapMigration => "BootstrapMigration",
120 IntentKind::AppDataUpdate => "AppDataUpdate",
121 };
122 write!(f, "{}", description)
123 }
124}
125
126#[repr(i32)]
127#[derive(Debug, Clone, Copy, PartialEq, Eq, AsExpression, FromSqlRow)]
128#[diesel(sql_type = Integer)]
129pub enum IntentState {
130 ToPublish = 1,
131 Published = 2,
132 Committed = 3,
133 Error = 4,
134 Processed = 5,
135 Superseded = 6,
141}
142
143#[derive(Queryable, Selectable, Identifiable, PartialEq, Clone)]
144#[diesel(table_name = group_intents)]
145#[diesel(primary_key(id))]
146pub struct StoredGroupIntent {
147 pub id: ID,
148 pub kind: IntentKind,
149 pub group_id: GroupId,
150 pub data: Vec<u8>,
151 pub state: IntentState,
152 pub payload_hash: Option<Vec<u8>>,
153 pub post_commit_data: Option<Vec<u8>>,
154 pub publish_attempts: i32,
155 pub staged_commit: Option<Vec<u8>>,
156 pub published_in_epoch: Option<i64>,
157 pub should_push: bool,
158 pub sequence_id: Option<i64>,
159}
160
161impl std::fmt::Debug for StoredGroupIntent {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 write!(f, "StoredGroupIntent {{ ")?;
164 write!(f, "id: {}, ", self.id)?;
165 write!(f, "kind: {}, ", self.kind)?;
166 write!(
167 f,
168 "group_id: {}, ",
169 fmt::truncate_hex(hex::encode(self.group_id))
170 )?;
171 write!(f, "data: {}, ", fmt::truncate_hex(hex::encode(&self.data)))?;
172 write!(f, "state: {:?}, ", self.state)?;
173 write!(
174 f,
175 "payload_hash: {:?}, ",
176 self.payload_hash
177 .as_ref()
178 .map(|h| fmt::truncate_hex(hex::encode(h)))
179 )?;
180 write!(
181 f,
182 "post_commit_data: {:?}, ",
183 self.post_commit_data
184 .as_ref()
185 .map(|d| fmt::truncate_hex(hex::encode(d)))
186 )?;
187 write!(f, "publish_attempts: {:?}, ", self.publish_attempts)?;
188 write!(
189 f,
190 "staged_commit: {:?}, ",
191 self.staged_commit
192 .as_ref()
193 .map(|c| fmt::truncate_hex(hex::encode(c)))
194 )?;
195 write!(f, "published_in_epoch: {:?} ", self.published_in_epoch)?;
196 write!(f, " }}")?;
197 Ok(())
198 }
199}
200
201impl_fetch!(StoredGroupIntent, group_intents, ID, select);
202
203impl<C: ConnectionExt> Delete<StoredGroupIntent> for DbConnection<C> {
204 type Key = ID;
205 fn delete(&self, key: ID) -> Result<usize, StorageError> {
206 Ok(self
207 .raw_query(|raw_conn| diesel::delete(dsl::group_intents.find(key)).execute(raw_conn))?)
208 }
209}
210
211#[derive(Insertable, Debug, PartialEq, Clone, Builder)]
215#[diesel(table_name = group_intents)]
216#[builder(setter(into), build_fn(error = "StorageError"))]
217pub struct NewGroupIntent {
218 pub kind: IntentKind,
219 pub group_id: GroupId,
220 pub data: Vec<u8>,
221 pub should_push: bool,
222 #[builder(default = "IntentState::ToPublish")]
223 pub state: IntentState,
224}
225
226impl_store!(NewGroupIntent, group_intents);
227
228impl NewGroupIntent {
229 pub fn builder() -> NewGroupIntentBuilder {
230 NewGroupIntentBuilder::default()
231 }
232
233 pub fn new(
234 kind: IntentKind,
235 group_id: impl Into<GroupId>,
236 data: Vec<u8>,
237 should_push: bool,
238 ) -> Self {
239 Self {
240 kind,
241 group_id: group_id.into(),
242 data,
243 state: IntentState::ToPublish,
244 should_push,
245 }
246 }
247}
248
249pub trait QueryGroupIntent {
250 fn insert_group_intent(
251 &self,
252 to_save: NewGroupIntent,
253 ) -> Result<StoredGroupIntent, crate::ConnectionError>;
254
255 fn find_group_intents<Id: AsRef<[u8]>>(
257 &self,
258 group_id: Id,
259 allowed_states: Option<Vec<IntentState>>,
260 allowed_kinds: Option<Vec<IntentKind>>,
261 ) -> Result<Vec<StoredGroupIntent>, crate::ConnectionError>;
262
263 fn set_group_intent_published(
266 &self,
267 intent_id: ID,
268 payload_hash: &[u8],
269 post_commit_data: Option<Vec<u8>>,
270 staged_commit: Option<Vec<u8>>,
271 published_in_epoch: i64,
272 ) -> Result<(), StorageError>;
273
274 fn set_group_intent_committed(&self, intent_id: ID, cursor: Cursor)
276 -> Result<(), StorageError>;
277
278 fn set_group_intent_processed(&self, intent_id: ID) -> Result<(), StorageError>;
280
281 fn set_group_intent_superseded(&self, intent_id: ID) -> Result<(), StorageError>;
284
285 fn supersede_pending_intents_for_inactive_group(
296 &self,
297 group_id: &[u8],
298 ) -> Result<usize, StorageError>;
299
300 fn set_group_intent_to_publish(&self, intent_id: ID) -> Result<(), StorageError>;
303
304 fn set_group_intent_error(&self, intent_id: ID) -> Result<(), StorageError>;
306
307 fn find_group_intent_by_payload_hash(
310 &self,
311 payload_hash: &[u8],
312 ) -> Result<Option<StoredGroupIntent>, StorageError>;
313
314 fn own_intent_kind_is_unreadable(&self, payload_hash: &[u8]) -> Result<bool, StorageError>;
323
324 fn find_dependant_commits<P: AsRef<[u8]>>(
326 &self,
327 payload_hashes: &[P],
328 ) -> Result<HashMap<PayloadHash, IntentDependency>, StorageError>;
329
330 fn increment_intent_publish_attempt_count(&self, intent_id: ID) -> Result<(), StorageError>;
331
332 fn set_group_intent_error_and_fail_msg(
333 &self,
334 intent: &StoredGroupIntent,
335 msg_id: Option<Vec<u8>>,
336 ) -> Result<(), StorageError>;
337}
338
339impl<T> QueryGroupIntent for &T
340where
341 T: QueryGroupIntent,
342{
343 fn insert_group_intent(
344 &self,
345 to_save: NewGroupIntent,
346 ) -> Result<StoredGroupIntent, crate::ConnectionError> {
347 (**self).insert_group_intent(to_save)
348 }
349
350 fn find_group_intents<Id: AsRef<[u8]>>(
351 &self,
352 group_id: Id,
353 allowed_states: Option<Vec<IntentState>>,
354 allowed_kinds: Option<Vec<IntentKind>>,
355 ) -> Result<Vec<StoredGroupIntent>, crate::ConnectionError> {
356 (**self).find_group_intents(group_id, allowed_states, allowed_kinds)
357 }
358
359 fn set_group_intent_published(
360 &self,
361 intent_id: ID,
362 payload_hash: &[u8],
363 post_commit_data: Option<Vec<u8>>,
364 staged_commit: Option<Vec<u8>>,
365 published_in_epoch: i64,
366 ) -> Result<(), StorageError> {
367 (**self).set_group_intent_published(
368 intent_id,
369 payload_hash,
370 post_commit_data,
371 staged_commit,
372 published_in_epoch,
373 )
374 }
375
376 fn set_group_intent_committed(
377 &self,
378 intent_id: ID,
379 cursor: Cursor,
380 ) -> Result<(), StorageError> {
381 (**self).set_group_intent_committed(intent_id, cursor)
382 }
383
384 fn set_group_intent_processed(&self, intent_id: ID) -> Result<(), StorageError> {
385 (**self).set_group_intent_processed(intent_id)
386 }
387
388 fn set_group_intent_superseded(&self, intent_id: ID) -> Result<(), StorageError> {
389 (**self).set_group_intent_superseded(intent_id)
390 }
391
392 fn supersede_pending_intents_for_inactive_group(
393 &self,
394 group_id: &[u8],
395 ) -> Result<usize, StorageError> {
396 (**self).supersede_pending_intents_for_inactive_group(group_id)
397 }
398
399 fn set_group_intent_to_publish(&self, intent_id: ID) -> Result<(), StorageError> {
400 (**self).set_group_intent_to_publish(intent_id)
401 }
402
403 fn set_group_intent_error(&self, intent_id: ID) -> Result<(), StorageError> {
404 (**self).set_group_intent_error(intent_id)
405 }
406
407 fn find_group_intent_by_payload_hash(
408 &self,
409 payload_hash: &[u8],
410 ) -> Result<Option<StoredGroupIntent>, StorageError> {
411 (**self).find_group_intent_by_payload_hash(payload_hash)
412 }
413
414 fn own_intent_kind_is_unreadable(&self, payload_hash: &[u8]) -> Result<bool, StorageError> {
415 (**self).own_intent_kind_is_unreadable(payload_hash)
416 }
417
418 fn find_dependant_commits<P: AsRef<[u8]>>(
419 &self,
420 payload_hashes: &[P],
421 ) -> Result<HashMap<PayloadHash, IntentDependency>, StorageError> {
422 (**self).find_dependant_commits(payload_hashes)
423 }
424
425 fn increment_intent_publish_attempt_count(&self, intent_id: ID) -> Result<(), StorageError> {
426 (**self).increment_intent_publish_attempt_count(intent_id)
427 }
428
429 fn set_group_intent_error_and_fail_msg(
430 &self,
431 intent: &StoredGroupIntent,
432 msg_id: Option<Vec<u8>>,
433 ) -> Result<(), StorageError> {
434 (**self).set_group_intent_error_and_fail_msg(intent, msg_id)
435 }
436}
437
438impl<C: ConnectionExt> QueryGroupIntent for DbConnection<C> {
439 #[xmtp_common::db_span]
440 fn insert_group_intent(
441 &self,
442 to_save: NewGroupIntent,
443 ) -> Result<StoredGroupIntent, crate::ConnectionError> {
444 self.raw_query(|conn| {
445 diesel::insert_into(dsl::group_intents)
446 .values(to_save)
447 .returning(StoredGroupIntent::as_returning())
448 .get_result(conn)
449 })
450 }
451
452 #[xmtp_common::db_span]
454 fn find_group_intents<Id: AsRef<[u8]>>(
455 &self,
456 group_id: Id,
457 allowed_states: Option<Vec<IntentState>>,
458 allowed_kinds: Option<Vec<IntentKind>>,
459 ) -> Result<Vec<StoredGroupIntent>, crate::ConnectionError> {
460 let group_id = group_id.as_ref();
461 let mut query = dsl::group_intents
462 .into_boxed()
463 .filter(dsl::group_id.eq(group_id));
464
465 if let Some(allowed_states) = allowed_states {
466 query = query.filter(dsl::state.eq_any(allowed_states));
467 }
468
469 if let Some(allowed_kinds) = allowed_kinds {
470 query = query.filter(dsl::kind.eq_any(allowed_kinds));
471 }
472
473 query = query.order(dsl::id.asc());
474
475 self.raw_query(|conn| {
476 query
477 .select(StoredGroupIntent::as_select())
478 .load::<StoredGroupIntent>(conn)
479 })
480 }
481
482 #[tracing::instrument(level = "debug", skip(self, payload_hash), fields(intent_id = intent_id, payload_hash = hex::encode(payload_hash)))]
485 fn set_group_intent_published(
486 &self,
487 intent_id: ID,
488 payload_hash: &[u8],
489 post_commit_data: Option<Vec<u8>>,
490 staged_commit: Option<Vec<u8>>,
491 published_in_epoch: i64,
492 ) -> Result<(), StorageError> {
493 let rows_changed = self.raw_query(|conn| {
494 diesel::update(dsl::group_intents)
495 .filter(dsl::id.eq(intent_id))
496 .filter(dsl::state.eq(IntentState::ToPublish))
499 .set((
500 dsl::state.eq(IntentState::Published),
501 dsl::payload_hash.eq(payload_hash),
502 dsl::post_commit_data.eq(post_commit_data),
503 dsl::staged_commit.eq(staged_commit),
504 dsl::published_in_epoch.eq(published_in_epoch),
505 ))
506 .execute(conn)
507 })?;
508
509 if rows_changed == 0 {
510 let already_published = self.raw_query(|conn| {
511 dsl::group_intents
512 .filter(dsl::id.eq(intent_id))
513 .select(StoredGroupIntent::as_select())
514 .first::<StoredGroupIntent>(conn)
515 });
516
517 if already_published.is_ok() {
518 return Ok(());
519 } else {
520 return Err(NotFound::IntentForToPublish(intent_id).into());
521 }
522 }
523 Ok(())
524 }
525
526 #[tracing::instrument(level = "debug", skip(self))]
528 fn set_group_intent_committed(
529 &self,
530 intent_id: ID,
531 cursor: Cursor,
532 ) -> Result<(), StorageError> {
533 let rows_changed: usize = self.raw_query(|conn| {
534 diesel::update(dsl::group_intents)
535 .filter(dsl::id.eq(intent_id))
536 .filter(dsl::state.eq(IntentState::Published))
539 .set((
540 dsl::state.eq(IntentState::Committed),
541 dsl::sequence_id.eq(cursor.0 as i64),
542 ))
543 .execute(conn)
544 })?;
545
546 if rows_changed == 0 {
548 return Err(NotFound::IntentForCommitted(intent_id).into());
549 }
550
551 Ok(())
552 }
553
554 #[tracing::instrument(level = "debug", skip(self))]
558 fn set_group_intent_superseded(&self, intent_id: ID) -> Result<(), StorageError> {
559 let rows_changed = self.raw_query(|conn| {
560 diesel::update(dsl::group_intents)
561 .filter(dsl::id.eq(intent_id))
562 .filter(dsl::state.eq(IntentState::ToPublish))
567 .set(dsl::state.eq(IntentState::Superseded))
568 .execute(conn)
569 })?;
570
571 if rows_changed == 0 {
572 return Err(NotFound::IntentForToPublish(intent_id).into());
573 }
574
575 Ok(())
576 }
577
578 #[tracing::instrument(level = "debug", skip(self))]
589 fn supersede_pending_intents_for_inactive_group(
590 &self,
591 group_id: &[u8],
592 ) -> Result<usize, StorageError> {
593 let rows_changed = self.raw_query(|conn| {
594 diesel::update(dsl::group_intents)
595 .filter(dsl::group_id.eq(group_id))
596 .filter(
597 dsl::state
598 .eq(IntentState::ToPublish)
599 .or(dsl::state.eq(IntentState::Published)),
600 )
601 .set((
602 dsl::state.eq(IntentState::Error),
603 dsl::prepared_envelopes.eq(None::<Vec<u8>>),
604 dsl::staged_commit.eq(None::<Vec<u8>>),
605 dsl::payload_hash.eq(None::<Vec<u8>>),
606 dsl::published_in_epoch.eq(None::<i64>),
607 ))
608 .execute(conn)
609 })?;
610 Ok(rows_changed)
611 }
612
613 #[tracing::instrument(level = "debug", skip(self))]
615 fn set_group_intent_processed(&self, intent_id: ID) -> Result<(), StorageError> {
616 let rows_changed = self.raw_query(|conn| {
617 diesel::update(dsl::group_intents)
618 .filter(dsl::id.eq(intent_id))
619 .set(dsl::state.eq(IntentState::Processed))
620 .execute(conn)
621 })?;
622
623 if rows_changed == 0 {
625 return Err(NotFound::IntentById(intent_id).into());
626 }
627
628 Ok(())
629 }
630
631 #[tracing::instrument(level = "debug", skip(self))]
634 fn set_group_intent_to_publish(&self, intent_id: ID) -> Result<(), StorageError> {
635 let rows_changed = self.raw_query(|conn| {
636 diesel::update(dsl::group_intents)
637 .filter(dsl::id.eq(intent_id))
638 .filter(dsl::state.eq(IntentState::Published))
641 .set((
642 dsl::state.eq(IntentState::ToPublish),
643 dsl::payload_hash.eq(None::<Vec<u8>>),
645 dsl::post_commit_data.eq(None::<Vec<u8>>),
646 dsl::published_in_epoch.eq(None::<i64>),
647 dsl::staged_commit.eq(None::<Vec<u8>>),
648 dsl::prepared_envelopes.eq(None::<Vec<u8>>),
649 ))
650 .execute(conn)
651 })?;
652
653 if rows_changed == 0 {
654 return Err(NotFound::IntentForPublish(intent_id).into());
655 }
656 Ok(())
657 }
658
659 #[tracing::instrument(level = "debug", skip(self))]
661 fn set_group_intent_error(&self, intent_id: ID) -> Result<(), StorageError> {
662 let rows_changed = self.raw_query(|conn| {
663 diesel::update(dsl::group_intents)
664 .filter(dsl::id.eq(intent_id))
665 .set(dsl::state.eq(IntentState::Error))
666 .execute(conn)
667 })?;
668
669 if rows_changed == 0 {
670 return Err(NotFound::IntentById(intent_id).into());
671 }
672
673 Ok(())
674 }
675
676 #[xmtp_common::db_span]
679 fn find_group_intent_by_payload_hash(
680 &self,
681 payload_hash: &[u8],
682 ) -> Result<Option<StoredGroupIntent>, StorageError> {
683 let result = self.raw_query(|conn| {
684 dsl::group_intents
685 .filter(dsl::payload_hash.eq(payload_hash))
686 .select(StoredGroupIntent::as_select())
687 .first::<StoredGroupIntent>(conn)
688 .optional()
689 })?;
690
691 Ok(result)
692 }
693
694 #[xmtp_common::db_span]
695 fn own_intent_kind_is_unreadable(&self, payload_hash: &[u8]) -> Result<bool, StorageError> {
696 let kind = self.raw_query(|conn| {
699 dsl::group_intents
700 .filter(dsl::payload_hash.eq(payload_hash))
701 .select(dsl::kind)
702 .first::<i32>(conn)
703 .optional()
704 })?;
705 Ok(kind.is_some_and(|kind| !IntentKind::all().any(|known| known as i32 == kind)))
708 }
709
710 #[xmtp_common::db_span]
713 fn find_dependant_commits<P: AsRef<[u8]>>(
714 &self,
715 payload_hashes: &[P],
716 ) -> Result<HashMap<PayloadHash, IntentDependency>, StorageError> {
717 use super::schema::refresh_state;
718 use crate::encrypted_store::refresh_state::EntityKind;
719
720 let hashes = payload_hashes
721 .iter()
722 .map(|h| PayloadHashRef::from(h.as_ref()));
723
724 let map: HashMap<PayloadHash, Vec<IntentDependency>> = self.raw_query(|conn| {
726 dsl::group_intents
727 .filter(dsl::payload_hash.eq_any(hashes))
728 .inner_join(
729 refresh_state::table.on(refresh_state::entity_id
730 .eq(dsl::group_id)
731 .and(refresh_state::entity_kind.eq(EntityKind::ApplicationMessage))),
732 )
733 .select((
734 dsl::payload_hash.assume_not_null(),
735 refresh_state::sequence_id,
736 dsl::group_id,
737 ))
738 .load_iter::<(Vec<u8>, i64, GroupId), DefaultLoadingMode>(conn)?
739 .map_ok(|(hash, sequence_id, group_id)| {
740 (
741 PayloadHash::from(hash),
742 IntentDependency {
743 cursor: Cursor(sequence_id as u64),
744 group_id,
745 },
746 )
747 })
748 .process_results(|iter| iter.into_grouping_map().collect())
749 })?;
750
751 let map = map
752 .into_iter()
753 .map(|(hash, mut d)| {
754 if d.len() > 1 {
755 return Err(GroupIntentError::MoreThanOneDependency {
756 payload_hash: hash.clone(),
757 cursors: d.iter().map(|d| d.cursor).collect(),
758 group_id: d[0].group_id,
759 }
760 .into());
761 }
762
763 let dep = d
766 .pop()
767 .ok_or_else(|| GroupIntentError::NoDependencyFound { hash: hash.clone() })
768 .map_err(StorageError::from)?;
769 Ok::<_, StorageError>((hash, dep))
770 })
771 .try_collect()?;
772
773 Ok(map)
774 }
775
776 #[tracing::instrument(level = "debug", skip(self))]
777 fn increment_intent_publish_attempt_count(&self, intent_id: ID) -> Result<(), StorageError> {
778 self.raw_query(|conn| {
779 diesel::update(dsl::group_intents)
780 .filter(dsl::id.eq(intent_id))
781 .set(dsl::publish_attempts.eq(dsl::publish_attempts + 1))
782 .execute(conn)
783 })?;
784
785 Ok(())
786 }
787
788 #[tracing::instrument(level = "debug", skip_all, fields(intent_id = %intent.id, intent_kind = %intent.kind, group_id = %intent.group_id))]
789 fn set_group_intent_error_and_fail_msg(
790 &self,
791 intent: &StoredGroupIntent,
792 msg_id: Option<Vec<u8>>,
793 ) -> Result<(), StorageError> {
794 self.set_group_intent_error(intent.id)?;
795 if let Some(id) = msg_id {
796 self.set_delivery_status_to_failed(&id)?;
797 }
798 Ok(())
799 }
800}
801
802impl ToSql<Integer, Sqlite> for IntentKind
803where
804 i32: ToSql<Integer, Sqlite>,
805{
806 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
807 out.set_value(*self as i32);
808 Ok(IsNull::No)
809 }
810}
811
812impl FromSql<Integer, Sqlite> for IntentKind
813where
814 i32: FromSql<Integer, Sqlite>,
815{
816 fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
817 match i32::from_sql(bytes)? {
818 1 => Ok(IntentKind::SendMessage),
819 2 => Ok(IntentKind::KeyUpdate),
820 3 => Ok(IntentKind::MetadataUpdate),
821 4 => Ok(IntentKind::UpdateGroupMembership),
822 5 => Ok(IntentKind::UpdateAdminList),
823 6 => Ok(IntentKind::UpdatePermission),
824 7 => Ok(IntentKind::ReaddInstallations),
825 8 => Ok(IntentKind::ProposeMemberUpdate),
826 9 => Ok(IntentKind::ProposeGroupContextExtensions),
827 10 => Ok(IntentKind::CommitPendingProposals),
828 11 => Ok(IntentKind::BootstrapMigration),
829 12 => Ok(IntentKind::AppDataUpdate),
830 x => Err(format!("Unrecognized IntentKind variant {}", x).into()),
831 }
832 }
833}
834
835impl ToSql<Integer, Sqlite> for IntentState
836where
837 i32: ToSql<Integer, Sqlite>,
838{
839 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
840 out.set_value(*self as i32);
841 Ok(IsNull::No)
842 }
843}
844
845impl FromSql<Integer, Sqlite> for IntentState
846where
847 i32: FromSql<Integer, Sqlite>,
848{
849 fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
850 match i32::from_sql(bytes)? {
851 1 => Ok(IntentState::ToPublish),
852 2 => Ok(IntentState::Published),
853 3 => Ok(IntentState::Committed),
854 4 => Ok(IntentState::Error),
855 5 => Ok(IntentState::Processed),
856 6 => Ok(IntentState::Superseded),
857 x => Err(format!("Unrecognized variant {}", x).into()),
858 }
859 }
860}
861
862#[cfg(test)]
863pub(crate) mod tests {
864 use super::*;
865 use crate::{
866 Fetch, Store,
867 group::{GroupMembershipState, StoredGroup},
868 test_utils::with_connection,
869 };
870 use xmtp_common::{Generate, rand_vec};
871
872 fn insert_group<C: ConnectionExt>(conn: &DbConnection<C>, group_id: GroupId) {
873 StoredGroup::builder()
874 .id(group_id)
875 .created_at_ns(100)
876 .membership_state(GroupMembershipState::Allowed)
877 .added_by_inbox_id("placeholder_address")
878 .build()
879 .unwrap()
880 .store(conn)
881 .unwrap();
882 }
883
884 impl NewGroupIntent {
885 pub fn new_test(
888 kind: IntentKind,
889 group_id: GroupId,
890 data: Vec<u8>,
891 state: IntentState,
892 ) -> Self {
893 Self {
894 kind,
895 group_id,
896 data,
897 state,
898 should_push: false,
899 }
900 }
901 }
902
903 fn find_first_intent<C: ConnectionExt>(
904 conn: &DbConnection<C>,
905 group_id: GroupId,
906 ) -> StoredGroupIntent {
907 conn.raw_query(|raw_conn| {
908 dsl::group_intents
909 .filter(dsl::group_id.eq(group_id))
910 .select(StoredGroupIntent::as_select())
911 .first(raw_conn)
912 })
913 .unwrap()
914 }
915
916 #[xmtp_common::test]
924 fn intent_kind_discriminants_are_contiguous() {
925 let mut discriminants: Vec<i32> = IntentKind::all().map(|k| k as i32).collect();
926 discriminants.sort_unstable();
927 let count = discriminants.len();
928 assert_eq!(
929 discriminants,
930 (1..=count as i32).collect::<Vec<_>>(),
931 "IntentKind discriminants must be exactly 1..={} with no gaps or duplicates",
932 count
933 );
934 }
935
936 #[xmtp_common::test]
941 fn unknown_kind_row_is_excluded_by_kind_filter() {
942 let group_id = GroupId::generate();
943
944 with_connection(|conn| {
945 insert_group(conn, group_id);
946
947 NewGroupIntent::new_test(
949 IntentKind::SendMessage,
950 group_id,
951 rand_vec::<24>(),
952 IntentState::ToPublish,
953 )
954 .store(conn)
955 .unwrap();
956
957 let future_kind = IntentKind::all().count() as i32 + 1;
961 conn.raw_query(|raw_conn| {
962 diesel::insert_into(dsl::group_intents)
963 .values((
964 dsl::kind.eq(future_kind),
965 dsl::group_id.eq(group_id),
966 dsl::data.eq(rand_vec::<24>()),
967 dsl::state.eq(IntentState::ToPublish),
968 dsl::publish_attempts.eq(0),
969 dsl::should_push.eq(false),
970 ))
971 .execute(raw_conn)
972 })
973 .unwrap();
974
975 let intents = conn
978 .find_group_intents(
979 group_id,
980 Some(vec![IntentState::ToPublish]),
981 Some(IntentKind::all().collect()),
982 )
983 .unwrap();
984 assert_eq!(intents.len(), 1);
985 assert_eq!(intents[0].kind, IntentKind::SendMessage);
986
987 assert!(
990 conn.find_group_intents(group_id, Some(vec![IntentState::ToPublish]), None)
991 .is_err(),
992 "unfiltered query should surface the FromSql error for unknown kinds"
993 );
994 })
995 }
996
997 #[xmtp_common::test]
1001 fn an_unreadable_own_intent_kind_is_reported_not_hidden() {
1002 let group_id = GroupId::generate();
1003
1004 with_connection(|conn| {
1005 insert_group(conn, group_id);
1006 let known_hash = rand_vec::<32>();
1007 let future_hash = rand_vec::<32>();
1008
1009 conn.raw_query(|raw_conn| {
1010 diesel::insert_into(dsl::group_intents)
1011 .values((
1012 dsl::kind.eq(IntentKind::SendMessage),
1013 dsl::group_id.eq(group_id),
1014 dsl::data.eq(rand_vec::<24>()),
1015 dsl::state.eq(IntentState::Published),
1016 dsl::payload_hash.eq(Some(known_hash.clone())),
1017 dsl::publish_attempts.eq(0),
1018 dsl::should_push.eq(false),
1019 ))
1020 .execute(raw_conn)
1021 })
1022 .unwrap();
1023
1024 let future_kind = IntentKind::all().count() as i32 + 1;
1025 conn.raw_query(|raw_conn| {
1026 diesel::insert_into(dsl::group_intents)
1027 .values((
1028 dsl::kind.eq(future_kind),
1029 dsl::group_id.eq(group_id),
1030 dsl::data.eq(rand_vec::<24>()),
1031 dsl::state.eq(IntentState::Published),
1032 dsl::payload_hash.eq(Some(future_hash.clone())),
1033 dsl::publish_attempts.eq(0),
1034 dsl::should_push.eq(false),
1035 ))
1036 .execute(raw_conn)
1037 })
1038 .unwrap();
1039
1040 assert!(!conn.own_intent_kind_is_unreadable(&known_hash).unwrap());
1042 assert!(
1043 !conn
1044 .own_intent_kind_is_unreadable(&rand_vec::<32>())
1045 .unwrap()
1046 );
1047
1048 assert!(conn.own_intent_kind_is_unreadable(&future_hash).unwrap());
1051 assert!(
1052 conn.find_group_intent_by_payload_hash(&future_hash)
1053 .is_err(),
1054 "the typed lookup still cannot decode it; the probe is what callers use"
1055 );
1056 })
1057 }
1058
1059 #[xmtp_common::test]
1060 fn test_store_and_fetch() {
1061 let group_id = GroupId::generate();
1062 let data = rand_vec::<24>();
1063 let kind = IntentKind::UpdateGroupMembership;
1064 let state = IntentState::ToPublish;
1065
1066 let to_insert = NewGroupIntent::new_test(kind, group_id, data.clone(), state);
1067
1068 with_connection(|conn| {
1069 insert_group(conn, group_id);
1071
1072 to_insert.store(conn).unwrap();
1073
1074 let results = conn
1075 .find_group_intents(group_id, Some(vec![IntentState::ToPublish]), None)
1076 .unwrap();
1077
1078 assert_eq!(results.len(), 1);
1079 assert_eq!(results[0].kind, kind);
1080 assert_eq!(results[0].data, data);
1081 assert_eq!(results[0].group_id.as_slice(), group_id.as_slice());
1082
1083 let id = results[0].id;
1084
1085 let fetched: StoredGroupIntent = conn.fetch(&id).unwrap().unwrap();
1086
1087 assert_eq!(fetched.id, id);
1088 })
1089 }
1090
1091 #[xmtp_common::test]
1092 fn test_query() {
1093 let group_id = GroupId::generate();
1094
1095 let test_intents: Vec<NewGroupIntent> = vec![
1096 NewGroupIntent::new_test(
1097 IntentKind::UpdateGroupMembership,
1098 group_id,
1099 rand_vec::<24>(),
1100 IntentState::ToPublish,
1101 ),
1102 NewGroupIntent::new_test(
1103 IntentKind::KeyUpdate,
1104 group_id,
1105 rand_vec::<24>(),
1106 IntentState::Published,
1107 ),
1108 NewGroupIntent::new_test(
1109 IntentKind::KeyUpdate,
1110 group_id,
1111 rand_vec::<24>(),
1112 IntentState::Committed,
1113 ),
1114 ];
1115
1116 with_connection(|conn| {
1117 insert_group(conn, group_id);
1119
1120 for case in test_intents {
1121 case.store(conn).unwrap();
1122 }
1123
1124 let mut results = conn
1126 .find_group_intents(
1127 group_id,
1128 Some(vec![IntentState::ToPublish, IntentState::Published]),
1129 None,
1130 )
1131 .unwrap();
1132
1133 assert_eq!(results.len(), 2);
1134
1135 results = conn
1137 .find_group_intents(group_id, None, Some(vec![IntentKind::KeyUpdate]))
1138 .unwrap();
1139 assert_eq!(results.len(), 2);
1140
1141 results = conn
1143 .find_group_intents(
1144 group_id,
1145 Some(vec![IntentState::Committed]),
1146 Some(vec![IntentKind::KeyUpdate]),
1147 )
1148 .unwrap();
1149
1150 assert_eq!(results.len(), 1);
1151
1152 results = conn
1154 .find_group_intents(
1155 group_id,
1156 Some(vec![IntentState::Committed]),
1157 Some(vec![IntentKind::SendMessage]),
1158 )
1159 .unwrap();
1160
1161 assert_eq!(results.len(), 0);
1162
1163 results = conn.find_group_intents(group_id, None, None).unwrap();
1165 assert_eq!(results.len(), 3);
1166 })
1167 }
1168
1169 #[xmtp_common::test]
1170 fn find_by_payload_hash() {
1171 let group_id = GroupId::generate();
1172
1173 with_connection(|conn| {
1174 insert_group(conn, group_id);
1175
1176 NewGroupIntent::new(
1178 IntentKind::UpdateGroupMembership,
1179 group_id,
1180 rand_vec::<24>(),
1181 false,
1182 )
1183 .store(conn)
1184 .unwrap();
1185
1186 let intent = find_first_intent(conn, group_id);
1188
1189 let payload_hash = rand_vec::<24>();
1191 let post_commit_data = rand_vec::<24>();
1192 conn.set_group_intent_published(
1193 intent.id,
1194 &payload_hash,
1195 Some(post_commit_data.clone()),
1196 None,
1197 1,
1198 )
1199 .unwrap();
1200
1201 let find_result = conn
1202 .find_group_intent_by_payload_hash(&payload_hash)
1203 .unwrap()
1204 .unwrap();
1205
1206 assert_eq!(find_result.id, intent.id);
1207 assert_eq!(find_result.published_in_epoch, Some(1));
1208 })
1209 }
1210
1211 #[xmtp_common::test]
1212 fn test_happy_path_state_transitions() {
1213 let group_id = GroupId::generate();
1214
1215 with_connection(|conn| {
1216 insert_group(conn, group_id);
1217
1218 NewGroupIntent::new(
1220 IntentKind::UpdateGroupMembership,
1221 group_id,
1222 rand_vec::<24>(),
1223 false,
1224 )
1225 .store(conn)
1226 .unwrap();
1227
1228 let mut intent = find_first_intent(conn, group_id);
1229
1230 let payload_hash = rand_vec::<24>();
1232 let post_commit_data = rand_vec::<24>();
1233 conn.set_group_intent_published(
1234 intent.id,
1235 &payload_hash,
1236 Some(post_commit_data.clone()),
1237 None,
1238 1,
1239 )
1240 .unwrap();
1241
1242 intent = conn.fetch(&intent.id).unwrap().unwrap();
1243 assert_eq!(intent.state, IntentState::Published);
1244 assert_eq!(intent.payload_hash, Some(payload_hash.clone()));
1245 assert_eq!(intent.post_commit_data, Some(post_commit_data.clone()));
1246
1247 conn.set_group_intent_committed(intent.id, Cursor::default())
1248 .unwrap();
1249 intent = conn.fetch(&intent.id).unwrap().unwrap();
1251 assert_eq!(intent.state, IntentState::Committed);
1252 assert_eq!(intent.payload_hash, Some(payload_hash.clone()));
1254 })
1255 }
1256
1257 #[xmtp_common::test]
1258 fn test_republish_state_transition() {
1259 let group_id = GroupId::generate();
1260
1261 with_connection(|conn| {
1262 insert_group(conn, group_id);
1263
1264 NewGroupIntent::new(
1266 IntentKind::UpdateGroupMembership,
1267 group_id,
1268 rand_vec::<24>(),
1269 false,
1270 )
1271 .store(conn)
1272 .unwrap();
1273
1274 let mut intent = find_first_intent(conn, group_id);
1275
1276 let payload_hash = rand_vec::<24>();
1278 let post_commit_data = rand_vec::<24>();
1279 conn.set_group_intent_published(
1280 intent.id,
1281 &payload_hash,
1282 Some(post_commit_data.clone()),
1283 None,
1284 1,
1285 )
1286 .unwrap();
1287
1288 intent = conn.fetch(&intent.id).unwrap().unwrap();
1289 assert_eq!(intent.state, IntentState::Published);
1290 assert_eq!(intent.payload_hash, Some(payload_hash.clone()));
1291
1292 conn.set_group_intent_to_publish(intent.id).unwrap();
1294 intent = conn.fetch(&intent.id).unwrap().unwrap();
1295 assert_eq!(intent.state, IntentState::ToPublish);
1296 assert!(intent.payload_hash.is_none());
1297 assert!(intent.post_commit_data.is_none());
1298 })
1299 }
1300
1301 #[xmtp_common::test]
1302 fn test_invalid_state_transition() {
1303 let group_id = GroupId::generate();
1304
1305 with_connection(|conn| {
1306 insert_group(conn, group_id);
1307
1308 NewGroupIntent::new(
1310 IntentKind::UpdateGroupMembership,
1311 group_id,
1312 rand_vec::<24>(),
1313 false,
1314 )
1315 .store(conn)
1316 .unwrap();
1317
1318 let intent = find_first_intent(conn, group_id);
1319
1320 let commit_result = conn.set_group_intent_committed(intent.id, Cursor::default());
1321 assert!(commit_result.is_err());
1322 assert!(matches!(
1323 commit_result.err().unwrap(),
1324 StorageError::NotFound(_)
1325 ));
1326
1327 let to_publish_result = conn.set_group_intent_to_publish(intent.id);
1328 assert!(to_publish_result.is_err());
1329 assert!(matches!(
1330 to_publish_result.err().unwrap(),
1331 StorageError::NotFound(_)
1332 ));
1333 })
1334 }
1335
1336 #[xmtp_common::test]
1337 fn test_increment_publish_attempts() {
1338 let group_id = GroupId::generate();
1339 with_connection(|conn| {
1340 insert_group(conn, group_id);
1341 NewGroupIntent::new(
1342 IntentKind::UpdateGroupMembership,
1343 group_id,
1344 rand_vec::<24>(),
1345 false,
1346 )
1347 .store(conn)
1348 .unwrap();
1349
1350 let mut intent = find_first_intent(conn, group_id);
1351 assert_eq!(intent.publish_attempts, 0);
1352 conn.increment_intent_publish_attempt_count(intent.id)
1353 .unwrap();
1354 intent = find_first_intent(conn, group_id);
1355 assert_eq!(intent.publish_attempts, 1);
1356 conn.increment_intent_publish_attempt_count(intent.id)
1357 .unwrap();
1358 intent = find_first_intent(conn, group_id);
1359 assert_eq!(intent.publish_attempts, 2);
1360 })
1361 }
1362 #[xmtp_common::test]
1363 fn test_find_dependant_commits() {
1364 use crate::encrypted_store::refresh_state::{EntityKind, QueryRefreshState};
1365
1366 let group_id = GroupId::generate();
1367 let payload_hash1 = rand_vec::<24>();
1368 let payload_hash2 = rand_vec::<24>();
1369
1370 with_connection(|conn| {
1371 insert_group(conn, group_id);
1372 NewGroupIntent::new(IntentKind::SendMessage, group_id, rand_vec::<24>(), false)
1373 .store(conn)
1374 .unwrap();
1375
1376 let intent1 = find_first_intent(conn, group_id);
1377 conn.set_group_intent_published(intent1.id, &payload_hash1, None, None, 1)
1378 .unwrap();
1379
1380 NewGroupIntent::new(IntentKind::KeyUpdate, group_id, rand_vec::<24>(), false)
1381 .store(conn)
1382 .unwrap();
1383 let intents = conn.find_group_intents(group_id, None, None).unwrap();
1384 let intent2 = intents.iter().find(|i| i.id != intent1.id).unwrap();
1385 conn.set_group_intent_published(intent2.id, &payload_hash2, None, None, 1)
1386 .unwrap();
1387
1388 conn.update_cursor(group_id, EntityKind::ApplicationMessage, Cursor(100))
1389 .unwrap();
1390
1391 let result = conn
1392 .find_dependant_commits(&[&payload_hash1, &payload_hash2])
1393 .unwrap();
1394
1395 assert_eq!(result.len(), 2);
1396 let dep1 = result
1397 .get(&PayloadHash::from(payload_hash1.clone()))
1398 .unwrap();
1399 assert_eq!(dep1.cursor.0, 100);
1400
1401 assert_eq!(dep1.group_id.as_ref(), &group_id);
1402
1403 let dep2 = result
1404 .get(&PayloadHash::from(payload_hash2.clone()))
1405 .unwrap();
1406 assert_eq!(dep2.cursor.0, 100);
1407
1408 assert_eq!(dep2.group_id.as_ref(), &group_id);
1409 })
1410 }
1411
1412 #[xmtp_common::test]
1413 fn bootstrap_migration_intent_round_trips_through_sql() {
1414 let group_id = GroupId::generate();
1418 let data = rand_vec::<24>();
1419 let kind = IntentKind::BootstrapMigration;
1420 let to_insert =
1421 NewGroupIntent::new_test(kind, group_id, data.clone(), IntentState::ToPublish);
1422
1423 with_connection(|conn| {
1424 insert_group(conn, group_id);
1425 to_insert.store(conn).unwrap();
1426
1427 let results = conn
1428 .find_group_intents(group_id, Some(vec![IntentState::ToPublish]), None)
1429 .unwrap();
1430
1431 assert_eq!(results.len(), 1);
1432 assert_eq!(results[0].kind, IntentKind::BootstrapMigration);
1433 assert_eq!(format!("{}", results[0].kind), "BootstrapMigration");
1434 })
1435 }
1436}