1use std::{
2 cmp, collections::HashMap, fmt, io, num::NonZeroU32, num::ParseIntError, ops::Deref,
3 time::Duration,
4};
5
6use antithesis_sdk::assert_sometimes;
7use bytes::{Bytes, BytesMut};
8use foca::{Identity, Member, Notification, Runtime, Timer};
9use indexmap::{IndexMap, map::Entry};
10use metrics::counter;
11use rusqlite::{
12 ToSql,
13 types::{FromSql, FromSqlError},
14};
15use serde::{Deserialize, Serialize};
16use speedy::{Context, Readable, Reader, Writable, Writer};
17use time::OffsetDateTime;
18use tokio::{
19 sync::{mpsc, oneshot},
20 task::block_in_place,
21};
22use tracing::{debug, error, trace};
23use uhlc::NTP64;
24
25use crate::{
26 actor::{Actor, ActorId, ClusterId},
27 agent::Agent,
28 api::{ColumnName, SqliteValue, TableName},
29 base::{CrsqlDbVersion, CrsqlDbVersionRange, CrsqlSeq, CrsqlSeqRange},
30 change::{Change, ChunkedChanges, MAX_CHANGES_BYTE_SIZE, row_to_change},
31 channel::CorroSender,
32 pubsub::MatchableChange,
33 sqlite::SqlitePoolError,
34 sync::SyncTraceContextV1,
35 updates::match_changes,
36};
37
38#[derive(Debug, Clone, Readable, Writable)]
39pub enum UniPayload {
40 V1 {
41 data: UniPayloadV1,
42 #[speedy(default_on_eof)]
43 cluster_id: ClusterId,
44 },
45}
46
47#[derive(Debug, Clone, Readable, Writable)]
48pub enum UniPayloadV1 {
49 Broadcast(BroadcastV1),
50}
51
52#[derive(Debug, Clone, Readable, Writable)]
53pub enum BiPayload {
54 V1 {
55 data: BiPayloadV1,
56 #[speedy(default_on_eof)]
57 cluster_id: ClusterId,
58 },
59}
60
61#[derive(Debug, Clone, Readable, Writable)]
62pub enum BiPayloadV1 {
63 SyncStart {
64 actor_id: ActorId,
65 #[speedy(default_on_eof)]
66 trace_ctx: SyncTraceContextV1,
67 },
68}
69
70#[derive(Debug)]
71pub enum FocaInput {
72 Announce(Actor),
73 Data(Bytes),
74 ClusterSize(NonZeroU32),
75 ApplyMany(Vec<Member<Actor>>),
76 Cmd(FocaCmd),
77}
78
79#[derive(Debug)]
80pub enum FocaCmd {
81 Rejoin(oneshot::Sender<Result<(), foca::Error>>),
82 MembershipStates(mpsc::Sender<foca::Member<Actor>>),
83 ChangeIdentity(Actor, oneshot::Sender<Result<(), foca::Error>>),
84}
85
86#[derive(Debug, Clone, Readable, Writable)]
87pub enum AuthzV1 {
88 Token(String),
89}
90
91#[derive(Clone, Debug, Readable, Writable)]
92pub enum BroadcastV1 {
93 Change(ChangeV1),
94}
95
96#[derive(Debug, Clone, PartialEq, Readable, Writable)]
97pub struct ColumnChange {
98 pub cid: ColumnName,
99 pub val: SqliteValue,
100 pub col_version: i64,
101 pub seq: CrsqlSeq,
102 pub cl: i64,
103}
104
105#[derive(Debug, Clone, Copy, strum::IntoStaticStr)]
106#[strum(serialize_all = "snake_case")]
107pub enum ChangeSource {
108 Broadcast,
109 Sync,
110}
111
112#[derive(Debug, Clone, PartialEq, Readable, Writable)]
113pub struct ChangeV1 {
114 pub actor_id: ActorId,
115 pub changeset: Changeset,
116}
117
118impl Deref for ChangeV1 {
119 type Target = Changeset;
120
121 fn deref(&self) -> &Self::Target {
122 &self.changeset
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Readable, Writable)]
127pub enum Changeset {
128 Empty {
129 versions: CrsqlDbVersionRange,
130 ts: Option<Timestamp>,
131 },
132 Full {
133 version: CrsqlDbVersion,
134 changes: Vec<Change>,
135 seqs: CrsqlSeqRange,
137 last_seq: CrsqlSeq,
139 ts: Timestamp,
140 },
141 EmptySet {
142 versions: Vec<CrsqlDbVersionRange>,
143 ts: Timestamp,
144 },
145 FullV2 {
146 actor_id: ActorId,
148 version: CrsqlDbVersion,
149 changes: ChangesetPerTable,
150 last_seq: CrsqlSeq,
151 seqs: CrsqlSeqRange,
152 ts: Timestamp,
153 },
154}
155
156impl From<ChangesetParts> for Changeset {
157 fn from(value: ChangesetParts) -> Self {
158 Changeset::Full {
159 version: value.version,
160 changes: value.changes,
161 seqs: value.seqs,
162 last_seq: value.last_seq,
163 ts: value.ts,
164 }
165 }
166}
167
168#[derive(Debug, Default, Clone, PartialEq, Readable, Writable)]
169pub struct ChangesetPerTable(IndexMap<TableName, ChangesetPerTablePk>);
170
171impl ChangesetPerTable {
172 pub fn new(map: IndexMap<TableName, ChangesetPerTablePk>) -> Self {
173 Self(map)
174 }
175
176 pub fn count(&self) -> HashMap<String, usize> {
177 self.iter()
178 .map(|(table, rows)| {
179 (
180 table.to_string(),
181 rows.0.iter().map(|(_, cols)| cols.len()).sum(),
182 )
183 })
184 .collect()
185 }
186
187 pub fn matchable_changes(&self) -> Box<dyn Iterator<Item = MatchableChange<'_>> + '_> {
188 Box::new(self.iter().flat_map(|(table, row)| {
189 row.0.iter().flat_map(|(pk, cols)| {
190 cols.iter().map(|col| MatchableChange {
191 table,
192 pk,
193 column: &col.cid,
194 cl: col.cl,
195 })
196 })
197 }))
198 }
199
200 pub fn insert(&mut self, change: Change) -> usize {
201 let mut cost = change.estimated_column_byte_size();
202 let table_len = change.table.len();
203 let pk_len = change.pk.len();
204
205 let per_table = match self.0.entry(change.table) {
206 Entry::Occupied(e) => e.into_mut(),
207 Entry::Vacant(v) => {
208 cost += table_len;
209 v.insert(ChangesetPerTablePk::default())
210 }
211 };
212
213 let col_changes = match per_table.0.entry(change.pk) {
214 Entry::Occupied(e) => e.into_mut(),
215 Entry::Vacant(v) => {
216 cost += pk_len;
217 v.insert(Default::default())
218 }
219 };
220
221 col_changes.push(ColumnChange {
222 cid: change.cid,
223 val: change.val,
224 col_version: change.col_version,
225 seq: change.seq,
226 cl: change.cl,
227 });
228
229 cost
230 }
231
232 pub fn drain(&mut self) -> Self {
233 Self(self.0.drain(..).collect())
234 }
235}
236
237impl Deref for ChangesetPerTable {
238 type Target = IndexMap<TableName, ChangesetPerTablePk>;
239
240 fn deref(&self) -> &Self::Target {
241 &self.0
242 }
243}
244
245#[derive(Debug, Default, Clone, PartialEq, Readable, Writable)]
246pub struct ChangesetPerTablePk(IndexMap<Vec<u8>, Vec<ColumnChange>>);
247
248pub struct ChangesetParts {
249 pub version: CrsqlDbVersion,
250 pub changes: Vec<Change>,
251 pub seqs: CrsqlSeqRange,
252 pub last_seq: CrsqlSeq,
253 pub ts: Timestamp,
254}
255
256impl Changeset {
257 #[inline]
258 pub fn versions(&self) -> CrsqlDbVersionRange {
259 match self {
260 Changeset::Empty { versions, .. } => *versions,
261 Changeset::EmptySet { .. } => CrsqlDbVersionRange::empty(),
264 Changeset::Full { version, .. } => CrsqlDbVersionRange::single(*version),
265 Changeset::FullV2 { version, .. } => CrsqlDbVersionRange::single(*version),
266 }
267 }
268
269 pub fn processing_cost(&self) -> usize {
271 match self {
272 Changeset::Empty { versions, .. } => cmp::min(versions.len(), 20),
273 Changeset::EmptySet { versions, .. } => versions
274 .iter()
275 .map(|versions| cmp::min(versions.len(), 20))
276 .sum::<usize>(),
277 Changeset::Full { changes, .. } => changes.len(),
278 Changeset::FullV2 { .. } => self.len(),
279 }
280 }
281
282 pub fn max_db_version(&self) -> Option<CrsqlDbVersion> {
283 match self {
284 Changeset::Empty { .. } => None,
285 Changeset::EmptySet { .. } => None,
286 Changeset::Full { version, .. } => Some(*version),
287 Changeset::FullV2 { version, .. } => Some(*version),
288 }
289 }
290
291 #[inline]
292 pub fn seqs(&self) -> Option<CrsqlSeqRange> {
293 match self {
294 Changeset::Empty { .. } => None,
295 Changeset::EmptySet { .. } => None,
296 Changeset::Full { seqs, .. } => Some(*seqs),
297 Changeset::FullV2 { seqs, .. } => Some(*seqs),
298 }
299 }
300
301 pub fn last_seq(&self) -> Option<CrsqlSeq> {
302 match self {
303 Changeset::Empty { .. } => None,
304 Changeset::EmptySet { .. } => None,
305 Changeset::Full { last_seq, .. } => Some(*last_seq),
306 Changeset::FullV2 { last_seq, .. } => Some(*last_seq),
307 }
308 }
309
310 pub fn is_complete(&self) -> bool {
311 match self {
312 Changeset::Empty { .. } => true,
313 Changeset::EmptySet { .. } => true,
314 Changeset::Full { seqs, last_seq, .. } => {
315 seqs.start_int() == 0 && seqs.end_int() == last_seq.0
316 }
317 Changeset::FullV2 { seqs, last_seq, .. } => {
318 seqs.start_int() == 0 && seqs.end_int() == last_seq.0
319 }
320 }
321 }
322
323 pub fn len(&self) -> usize {
324 match self {
325 Changeset::Empty { .. } => 0,
326 Changeset::EmptySet { versions, .. } => versions.len(),
327 Changeset::Full { changes, .. } => changes.len(),
328 Changeset::FullV2 { changes, .. } => changes
329 .0
330 .iter()
331 .map(|(_, pk_changes)| {
332 pk_changes
333 .0
334 .iter()
335 .map(|(_, changes)| changes.len())
336 .sum::<usize>()
337 })
338 .sum::<usize>(),
339 }
340 }
341
342 pub fn is_empty(&self) -> bool {
343 match self {
344 Changeset::Empty { .. } => true,
345 Changeset::EmptySet { .. } => true,
346 Changeset::Full { changes, .. } => changes.is_empty(),
347 Changeset::FullV2 { changes, .. } => changes.0.is_empty(),
348 }
349 }
350
351 pub fn is_empty_set(&self) -> bool {
352 match self {
353 Changeset::Empty { .. } => false,
354 Changeset::EmptySet { .. } => true,
355 Changeset::Full { .. } => false,
356 Changeset::FullV2 { .. } => false,
357 }
358 }
359
360 pub fn ts(&self) -> Option<Timestamp> {
361 match self {
362 Changeset::Empty { ts, .. } => *ts,
363 Changeset::EmptySet { ts, .. } => Some(*ts),
364 Changeset::Full { ts, .. } => Some(*ts),
365 Changeset::FullV2 { ts, .. } => Some(*ts),
366 }
367 }
368
369 pub fn into_parts(self) -> Option<ChangesetParts> {
370 match self {
371 Changeset::Empty { .. } => None,
372 Changeset::EmptySet { .. } => None,
373 Changeset::Full {
374 version,
375 changes,
376 seqs,
377 last_seq,
378 ts,
379 } => Some(ChangesetParts {
380 version,
381 changes,
382 seqs,
383 last_seq,
384 ts,
385 }),
386 Changeset::FullV2 {
387 actor_id,
388 version,
389 changes,
390 seqs,
391 last_seq,
392 ts,
393 } => {
394 let changes = changes
395 .0
396 .into_iter()
397 .flat_map(|(table, pk_changes)| {
398 pk_changes.0.into_iter().flat_map(move |(pk, row)| {
399 let table = table.clone();
400 row.into_iter().map(move |col_change| Change {
401 table: table.clone(),
402 pk: pk.clone(),
403 cid: col_change.cid,
404 val: col_change.val,
405 col_version: col_change.col_version,
406 db_version: version,
407 seq: col_change.seq,
408 site_id: actor_id.to_bytes(),
409 cl: col_change.cl,
410 })
411 })
412 })
413 .collect::<Vec<_>>();
414
415 Some(ChangesetParts {
416 version,
417 changes,
418 seqs,
419 last_seq,
420 ts,
421 })
422 }
423 }
424 }
425
426 pub fn matchable_changes(&self) -> Box<dyn Iterator<Item = MatchableChange<'_>> + '_> {
427 match self {
428 Changeset::Full { changes, .. } => Box::new(changes.iter().map(MatchableChange::from)),
429 Changeset::FullV2 { changes, .. } => Box::new(changes.matchable_changes()),
430 Changeset::Empty { .. } | Changeset::EmptySet { .. } => Box::new(std::iter::empty()),
431 }
432 }
433}
434
435#[derive(Debug, thiserror::Error)]
436pub enum TimestampParseError {
437 #[error("could not parse timestamp: {0:?}")]
438 Parse(ParseIntError),
439}
440
441#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialOrd, Ord)]
442#[serde(transparent)]
443pub struct Timestamp(pub NTP64);
444
445impl Timestamp {
446 pub fn to_time(&self) -> OffsetDateTime {
447 OffsetDateTime::from_unix_timestamp(self.0.as_secs() as i64).unwrap()
448 + time::Duration::nanoseconds(self.0.subsec_nanos() as i64)
449 }
450
451 pub fn to_ntp64(&self) -> NTP64 {
452 self.0
453 }
454
455 pub fn zero() -> Self {
456 Timestamp::from(0u64)
457 }
458
459 pub fn is_zero(&self) -> bool {
460 self.0.0 == 0
461 }
462}
463
464impl PartialEq for Timestamp {
466 fn eq(&self, other: &Self) -> bool {
467 self.0.as_secs() == other.0.as_secs() && self.0.subsec_nanos() == other.0.subsec_nanos()
468 }
469}
470
471impl std::hash::Hash for Timestamp {
472 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
473 self.0.as_secs().hash(state);
474 self.0.subsec_nanos().hash(state);
475 }
476}
477
478impl Deref for Timestamp {
479 type Target = NTP64;
480
481 fn deref(&self) -> &Self::Target {
482 &self.0
483 }
484}
485
486impl fmt::Display for Timestamp {
487 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488 self.0.fmt(f)
489 }
490}
491
492impl From<uhlc::Timestamp> for Timestamp {
493 fn from(ts: uhlc::Timestamp) -> Self {
494 Self(*ts.get_time())
495 }
496}
497
498impl From<NTP64> for Timestamp {
499 fn from(ntp64: NTP64) -> Self {
500 Self(ntp64)
501 }
502}
503
504impl From<u64> for Timestamp {
505 fn from(value: u64) -> Self {
506 Self(NTP64(value))
507 }
508}
509
510impl FromSql for Timestamp {
511 fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
512 match value {
513 rusqlite::types::ValueRef::Text(b) => match std::str::from_utf8(b) {
514 Ok(s) => match s.parse::<u64>() {
515 Ok(ntp) => Ok(Timestamp(NTP64(ntp))),
516 Err(e) => Err(FromSqlError::Other(Box::new(TimestampParseError::Parse(e)))),
517 },
518 Err(e) => Err(FromSqlError::Other(Box::new(e))),
519 },
520 _ => Err(FromSqlError::InvalidType),
521 }
522 }
523}
524
525impl ToSql for Timestamp {
526 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
527 Ok(rusqlite::types::ToSqlOutput::Owned(
528 rusqlite::types::Value::Text(self.0.as_u64().to_string()),
529 ))
530 }
531}
532
533impl<'a, C> Readable<'a, C> for Timestamp
534where
535 C: Context,
536{
537 #[inline]
538 fn read_from<R: Reader<'a, C>>(reader: &mut R) -> Result<Self, C::Error> {
539 Ok(Timestamp(NTP64(u64::read_from(reader)?)))
540 }
541
542 #[inline]
543 fn minimum_bytes_needed() -> usize {
544 std::mem::size_of::<u64>()
545 }
546}
547
548impl<C> Writable<C> for Timestamp
549where
550 C: Context,
551{
552 #[inline]
553 fn write_to<T: ?Sized + Writer<C>>(&self, writer: &mut T) -> Result<(), C::Error> {
554 self.0.0.write_to(writer)
555 }
556
557 #[inline]
558 fn bytes_needed(&self) -> Result<usize, C::Error> {
559 <u64 as speedy::Writable<C>>::bytes_needed(&self.0.0)
560 }
561}
562
563#[derive(Debug, thiserror::Error)]
564pub enum BroadcastEncodeError {
565 #[error(transparent)]
566 Encode(#[from] speedy::Error),
567 #[error(transparent)]
568 Io(#[from] io::Error),
569}
570
571#[derive(Debug, thiserror::Error)]
572pub enum BroadcastDecodeError {
573 #[error(transparent)]
574 Decode(#[from] speedy::Error),
575 #[error("corrupted message, crc mismatch (got: {0}, expected {1})")]
576 Corrupted(u32, u32),
577 #[error(transparent)]
578 Io(#[from] io::Error),
579 #[error("insufficient length received to decode message: {0}")]
580 InsufficientLength(usize),
581}
582
583#[derive(Debug)]
584pub enum BroadcastInput {
585 Rebroadcast(BroadcastV1),
586 AddBroadcast(BroadcastV1),
587}
588
589pub struct DispatchRuntime<T> {
590 pub to_send: CorroSender<(T, Bytes)>,
591 pub to_schedule: CorroSender<(Duration, Timer<T>)>,
592 pub notifications: CorroSender<foca::OwnedNotification<T>>,
593 pub active: bool,
594 pub buf: BytesMut,
595}
596
597impl<T: Identity> Runtime<T> for DispatchRuntime<T> {
598 fn notify(&mut self, notification: Notification<'_, T>) {
599 match ¬ification {
600 Notification::Active => {
601 self.active = true;
602 }
603 Notification::Idle | Notification::Defunct => {
604 self.active = false;
605 }
606 _ => {}
607 };
608
609 if let Err(e) = self.notifications.try_send(notification.to_owned()) {
610 counter!("corro.channel.error", "type" => "full", "name" => "dispatch.notifications")
611 .increment(1);
612 error!("error dispatching notification: {e}");
613 }
614 }
615
616 fn send_to(&mut self, to: T, data: &[u8]) {
617 trace!("cluster send_to {to:?}");
618 self.buf.extend_from_slice(data);
619
620 if let Err(e) = self.to_send.try_send((to, self.buf.split().freeze())) {
621 counter!("corro.channel.error", "type" => "full", "name" => "dispatch.to_send")
622 .increment(1);
623 error!("error dispatching broadcast packet: {e}");
624 }
625 }
626
627 fn submit_after(&mut self, event: Timer<T>, after: Duration) {
628 if let Err(e) = self.to_schedule.try_send((after, event)) {
629 counter!("corro.channel.error", "type" => "full", "name" => "dispatch.to_schedule")
630 .increment(1);
631 error!("error dispatching scheduled event: {e}");
632 }
633 }
634}
635
636impl<T> DispatchRuntime<T> {
637 pub fn new(
638 to_send: CorroSender<(T, Bytes)>,
639 to_schedule: CorroSender<(Duration, Timer<T>)>,
640 notifications: CorroSender<foca::OwnedNotification<T>>,
641 ) -> Self {
642 Self {
643 to_send,
644 to_schedule,
645 notifications,
646 active: false,
647 buf: BytesMut::new(),
648 }
649 }
650}
651
652#[derive(Debug, thiserror::Error)]
653pub enum BroadcastError {
654 #[error(transparent)]
655 Pool(#[from] SqlitePoolError),
656 #[error(transparent)]
657 Sqlite(#[from] rusqlite::Error),
658}
659
660pub async fn broadcast_changes(
661 agent: Agent,
662 db_version: CrsqlDbVersion,
663 last_seq: CrsqlSeq,
664 ts: Timestamp,
665) -> Result<(), BroadcastError> {
666 let actor_id = agent.actor_id();
667 let conn = agent.pool().read().await?;
668 trace!("got conn for broadcast");
669
670 block_in_place(|| {
671 let mut prepped = conn.prepare_cached(
673 r#"
674 SELECT "table", pk, cid, val, col_version, db_version, seq, site_id, cl
675 FROM crsql_changes
676 WHERE db_version = ?
677 AND site_id = crsql_site_id()
678 ORDER BY seq ASC
679 "#,
680 )?;
681 let rows = prepped.query_map([db_version], row_to_change)?;
682 let chunked = ChunkedChanges::new(rows, CrsqlSeq(0), last_seq, MAX_CHANGES_BYTE_SIZE);
683 for changes_seqs in chunked {
684 match changes_seqs {
685 Ok((changes, seqs)) => {
686 for (table_name, count) in changes.count() {
687 counter!("corro.changes.committed", "table" => table_name, "source" => "local").increment(count as u64);
688 }
689
690 trace!("broadcasting changes: {changes:?} for seq: {seqs:?}");
691
692 debug!("match_changes db_version: {db_version}");
693 let changeset = Changeset::FullV2 {
694 actor_id,
695 version: db_version,
696 changes,
697 seqs,
698 last_seq,
699 ts,
700 };
701 match_changes(agent.subs_manager(), &changeset, db_version);
702 match_changes(agent.updates_manager(), &changeset, db_version);
703
704 let tx_bcast = agent.tx_bcast().clone();
705 assert_sometimes!(true, "Corrosion broadcasts changes");
706 tokio::spawn(async move {
707 if let Err(e) = tx_bcast
708 .send(BroadcastInput::AddBroadcast(BroadcastV1::Change(
709 ChangeV1 {
710 actor_id,
711 changeset,
712 },
713 )))
714 .await
715 {
716 error!("could not send change message for broadcast: {e}");
717 }
718 });
719 }
720 Err(e) => {
721 error!(
722 "could not process crsql change (db_version: {db_version}) for broadcast: {e}"
723 );
724 break;
725 }
726 }
727 }
728 Ok::<_, rusqlite::Error>(())
729 })?;
730
731 Ok(())
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use speedy::{Readable, Writable};
738
739 #[test]
740 fn test_changeset_empty_serialization() {
741 let changeset = Changeset::Empty {
742 versions: (CrsqlDbVersion(10)..=CrsqlDbVersion(20)).into(),
743 ts: Some(Timestamp::from(12345u64)),
744 };
745
746 let bytes = changeset.write_to_vec().unwrap();
748
749 let deserialized = Changeset::read_from_buffer(&bytes).unwrap();
751
752 assert_eq!(changeset, deserialized);
753 }
754
755 #[test]
756 fn test_changeset_empty_with_none_ts() {
757 let changeset = Changeset::Empty {
758 versions: (CrsqlDbVersion(5)..=CrsqlDbVersion(5)).into(),
759 ts: None,
760 };
761
762 let bytes = changeset.write_to_vec().unwrap();
763 let deserialized = Changeset::read_from_buffer(&bytes).unwrap();
764
765 assert_eq!(changeset, deserialized);
766 }
767
768 #[test]
769 fn test_changeset_full_serialization() {
770 let changeset = Changeset::Full {
771 version: CrsqlDbVersion(42),
772 changes: vec![], seqs: (CrsqlSeq(100)..=CrsqlSeq(150)).into(),
774 last_seq: CrsqlSeq(200),
775 ts: Timestamp::from(67890u64),
776 };
777
778 let bytes = changeset.write_to_vec().unwrap();
779 let deserialized = Changeset::read_from_buffer(&bytes).unwrap();
780
781 assert_eq!(changeset, deserialized);
782 }
783
784 #[test]
785 fn test_changeset_empty_set_serialization() {
786 let changeset = Changeset::EmptySet {
787 versions: vec![
788 (CrsqlDbVersion(1)..=CrsqlDbVersion(5)).into(),
789 (CrsqlDbVersion(10)..=CrsqlDbVersion(15)).into(),
790 (CrsqlDbVersion(20)..=CrsqlDbVersion(20)).into(),
791 ],
792 ts: Timestamp::from(11111u64),
793 };
794
795 let bytes = changeset.write_to_vec().unwrap();
796 let deserialized = Changeset::read_from_buffer(&bytes).unwrap();
797
798 assert_eq!(changeset, deserialized);
799 }
800
801 #[test]
802 fn test_changeset_empty_set_with_empty_vec() {
803 let changeset = Changeset::EmptySet {
804 versions: vec![],
805 ts: Timestamp::from(22222u64),
806 };
807
808 let bytes = changeset.write_to_vec().unwrap();
809 let deserialized = Changeset::read_from_buffer(&bytes).unwrap();
810
811 assert_eq!(changeset, deserialized);
812 }
813
814 #[test]
815 fn test_changeset_roundtrip_all_variants() {
816 let test_cases = vec![
817 Changeset::Empty {
818 versions: (CrsqlDbVersion(1)..=CrsqlDbVersion(1)).into(),
819 ts: None,
820 },
821 Changeset::Full {
822 version: CrsqlDbVersion(1),
823 changes: vec![],
824 seqs: (CrsqlSeq(0)..=CrsqlSeq(0)).into(),
825 last_seq: CrsqlSeq(0),
826 ts: Timestamp::zero(),
827 },
828 Changeset::EmptySet {
829 versions: vec![(CrsqlDbVersion(1)..=CrsqlDbVersion(3)).into()],
830 ts: Timestamp::from(99999u64),
831 },
832 ];
833
834 for (i, original) in test_cases.into_iter().enumerate() {
835 let bytes = original.write_to_vec().unwrap();
836 let deserialized = Changeset::read_from_buffer(&bytes)
837 .unwrap_or_else(|e| panic!("Failed to deserialize test case {}: {}", i, e));
838
839 assert_eq!(original, deserialized, "Test case {} failed", i);
840 }
841 }
842}