1mod log;
70mod types;
71
72use std::collections::HashSet;
73use std::path::{Path, PathBuf};
74use std::sync::atomic::{AtomicU64, Ordering};
75use std::time::{SystemTime, UNIX_EPOCH};
76
77use doublets::data::LinkReference;
78
79use crate::error::LinkError;
80use crate::link::GenericLink;
81use crate::named_types::NamedTypesDecorator;
82use crate::storage::{LinksStorage, LinksStorageRef};
83
84pub use log::{FileTransitionLog, TransitionLogStore};
85pub use types::{
86 CommitMode, DoubletLink, GenericDoubletLink, GenericTransition, LogRetentionPolicy, Transition,
87 TransitionKind,
88};
89use types::{
90 APPLIED_MARKER_PREFIX, COMMIT_MARKER_PREFIX, ROLLBACK_MARKER_PREFIX, TRANSITION_NAME_PREFIX,
91};
92
93struct PendingTransaction<T> {
96 id: u128,
97 transitions: Vec<GenericTransition<T>>,
98 auto_commit: bool,
99 started_ms: i64,
100}
101
102#[derive(Debug, Clone)]
104pub struct TransactionHandle {
105 pub id: u128,
106 pub started_ms: i64,
107}
108
109pub struct GenericTransactionsDecorator<T, S, L>
112where
113 T: LinkReference,
114 S: LinksStorage<T>,
115 L: TransitionLogStore,
116{
117 inner: S,
118 log_store: L,
119 log: Vec<GenericTransition<T>>,
120 committed: HashSet<u128>,
121 rolled_back: HashSet<u128>,
122 applied: HashSet<i64>,
123 current: Option<PendingTransaction<T>>,
124 sequence_counter: i64,
125 applied_sequence: i64,
126 retention_policy: LogRetentionPolicy,
127 commit_mode: CommitMode,
128 replaying: bool,
129 trace: bool,
130}
131
132pub type TransactionsDecorator =
134 GenericTransactionsDecorator<u32, NamedTypesDecorator, NamedTypesDecorator>;
135
136impl<T, S, L> GenericTransactionsDecorator<T, S, L>
137where
138 T: LinkReference,
139 S: LinksStorage<T>,
140 L: TransitionLogStore,
141{
142 pub fn new(
146 inner: S,
147 log_store: L,
148 retention_policy: LogRetentionPolicy,
149 commit_mode: CommitMode,
150 trace: bool,
151 ) -> Result<Self, LinkError> {
152 let mut decorator = Self {
153 inner,
154 log_store,
155 log: Vec::new(),
156 committed: HashSet::new(),
157 rolled_back: HashSet::new(),
158 applied: HashSet::new(),
159 current: None,
160 sequence_counter: 0,
161 applied_sequence: 0,
162 retention_policy,
163 commit_mode,
164 replaying: false,
165 trace,
166 };
167 decorator.recover()?;
168 Ok(decorator)
169 }
170
171 pub fn make_transitions_database_filename<P: AsRef<Path>>(database_filename: P) -> PathBuf {
173 let path = database_filename.as_ref();
174 let stem = path
175 .file_stem()
176 .and_then(|s| s.to_str())
177 .unwrap_or_default();
178 let name = format!("{stem}.transitions.links");
179 match path.parent() {
180 Some(parent) if !parent.as_os_str().is_empty() => parent.join(name),
181 _ => PathBuf::from(name),
182 }
183 }
184
185 pub fn retention_policy(&self) -> &LogRetentionPolicy {
186 &self.retention_policy
187 }
188
189 pub fn set_retention_policy(&mut self, policy: LogRetentionPolicy) {
190 self.retention_policy = policy;
191 }
192
193 pub fn commit_mode(&self) -> CommitMode {
194 self.commit_mode
195 }
196
197 pub fn set_commit_mode(&mut self, mode: CommitMode) {
198 self.commit_mode = mode;
199 }
200
201 pub fn applied_sequence(&self) -> i64 {
202 self.applied_sequence
203 }
204
205 pub fn last_logged_sequence(&self) -> i64 {
206 self.sequence_counter
207 }
208
209 pub fn log(&self) -> Vec<GenericTransition<T>> {
211 self.log.clone()
212 }
213
214 pub fn inner(&self) -> &S {
215 &self.inner
216 }
217
218 pub fn inner_mut(&mut self) -> &mut S {
219 &mut self.inner
220 }
221
222 pub fn log_store(&self) -> &L {
223 &self.log_store
224 }
225
226 pub fn log_store_mut(&mut self) -> &mut L {
227 &mut self.log_store
228 }
229
230 pub fn into_inner(self) -> (S, L) {
231 (self.inner, self.log_store)
232 }
233
234 pub fn flush(&mut self) -> Result<(), LinkError> {
236 self.inner.flush()?;
237 self.log_store.flush_log()?;
238 Ok(())
239 }
240
241 pub fn save(&mut self) -> Result<(), LinkError> {
244 self.flush()
245 }
246
247 pub fn has_external_changes(&self) -> Result<bool, LinkError> {
250 self.inner.has_external_changes()
251 }
252
253 pub fn reload(&mut self) -> Result<(), LinkError> {
256 if self.current.is_some() {
257 return Err(LinkError::Transaction(
258 "Cannot reload while a transaction is open.".to_string(),
259 ));
260 }
261 self.inner.reload()?;
262 self.recover()
263 }
264
265 pub fn create(&mut self, source: T, target: T) -> Result<T, LinkError> {
268 if self.replaying {
269 return self.inner.create_link(source, target);
270 }
271 let owns = self.ensure_open_transaction();
272 let id = self.inner.create_link(source, target)?;
273 let after = self
274 .inner
275 .get_link(id)
276 .map(|link| GenericDoubletLink::from_link(&link))
277 .unwrap_or_else(|| GenericDoubletLink::new(id, source, target));
278 self.record_transition(TransitionKind::Create, GenericDoubletLink::empty(), after)?;
279 if owns {
280 self.commit_current()?;
281 }
282 Ok(id)
283 }
284
285 pub fn update(&mut self, id: T, source: T, target: T) -> Result<GenericLink<T>, LinkError> {
286 if self.replaying {
287 return self.inner.update_link(id, source, target);
288 }
289 let before = self.snapshot(id);
290 let owns = self.ensure_open_transaction();
291 let prev = match self.inner.update_link(id, source, target) {
292 Ok(prev) => prev,
293 Err(err) => {
294 if owns {
295 self.current = None;
296 }
297 return Err(err);
298 }
299 };
300 let after = self
301 .inner
302 .get_link(id)
303 .map(|link| GenericDoubletLink::from_link(&link))
304 .unwrap_or_else(|| GenericDoubletLink::new(id, source, target));
305 self.record_transition(TransitionKind::Update, before, after)?;
306 if owns {
307 self.commit_current()?;
308 }
309 Ok(prev)
310 }
311
312 pub fn delete(&mut self, id: T) -> Result<GenericLink<T>, LinkError> {
313 if self.replaying {
314 return self.inner.delete_link(id);
315 }
316 let before = self.snapshot(id);
317 let owns = self.ensure_open_transaction();
318 let deleted = match self.inner.delete_link(id) {
319 Ok(d) => d,
320 Err(err) => {
321 if owns {
322 self.current = None;
323 }
324 return Err(err);
325 }
326 };
327 self.record_transition(TransitionKind::Delete, before, GenericDoubletLink::empty())?;
328 if owns {
329 self.commit_current()?;
330 }
331 Ok(deleted)
332 }
333
334 pub fn create_and_update(&mut self, source: T, target: T) -> Result<T, LinkError> {
339 let owns = self.ensure_open_transaction();
340 let zero = T::from_byte(0);
341 let id = self.create(zero, zero)?;
342 self.update(id, source, target)?;
343 if owns {
344 self.commit_current()?;
345 }
346 Ok(id)
347 }
348
349 pub fn exists(&self, id: T) -> bool {
350 self.inner.link_exists(id)
351 }
352
353 pub fn search(&self, source: T, target: T) -> Option<T> {
354 self.inner.search_link(source, target)
355 }
356
357 pub fn get_or_create(&mut self, source: T, target: T) -> Result<T, LinkError> {
358 if let Some(existing) = self.inner.search_link(source, target) {
359 return Ok(existing);
360 }
361 self.create(source, target)
362 }
363
364 pub fn ensure_created(&mut self, id: T) -> Result<T, LinkError> {
365 self.inner.ensure_link_created(id)
368 }
369
370 fn snapshot(&self, id: T) -> GenericDoubletLink<T> {
372 let zero = T::from_byte(0);
373 self.inner
374 .get_link(id)
375 .map(|link| GenericDoubletLink::from_link(&link))
376 .unwrap_or_else(|| GenericDoubletLink::new(id, zero, zero))
377 }
378
379 fn ensure_open_transaction(&mut self) -> bool {
380 if self.current.is_none() {
381 self.current = Some(PendingTransaction {
382 id: new_transaction_id(),
383 transitions: Vec::new(),
384 auto_commit: true,
385 started_ms: now_unix_ms(),
386 });
387 true
388 } else {
389 false
390 }
391 }
392
393 fn record_transition(
394 &mut self,
395 kind: TransitionKind,
396 before: GenericDoubletLink<T>,
397 after: GenericDoubletLink<T>,
398 ) -> Result<(), LinkError> {
399 self.sequence_counter += 1;
400 let sequence = self.sequence_counter;
401 let timestamp_ms = now_unix_ms();
402 let transaction_id = self.current.as_ref().map(|tx| tx.id).ok_or_else(|| {
403 LinkError::Transaction(
404 "internal: missing open transaction while recording transition".to_string(),
405 )
406 })?;
407 let transition = GenericTransition {
408 transaction_id,
409 sequence,
410 timestamp_ms,
411 kind,
412 before,
413 after,
414 };
415 if let Some(current) = self.current.as_mut() {
416 current.transitions.push(transition);
417 }
418 self.log.push(transition);
419 self.write_transition_to_log(&transition)?;
420 if self.trace {
421 eprintln!(
422 "[Transactions] Recorded {:?} seq={} tx={:032x}: ({},{},{}) -> ({},{},{}).",
423 kind,
424 sequence,
425 transaction_id,
426 before.index,
427 before.source,
428 before.target,
429 after.index,
430 after.source,
431 after.target,
432 );
433 }
434 Ok(())
435 }
436
437 fn write_transition_to_log(
438 &mut self,
439 transition: &GenericTransition<T>,
440 ) -> Result<(), LinkError> {
441 self.log_store.append_log_entry(&format!(
442 "{TRANSITION_NAME_PREFIX}{}",
443 transition.serialize()
444 ))
445 }
446
447 fn write_marker(&mut self, name: &str) -> Result<(), LinkError> {
448 self.log_store.append_log_entry(name)
449 }
450
451 pub fn begin_transaction(&mut self) -> Result<TransactionHandle, LinkError> {
454 if self.current.is_some() {
455 return Err(LinkError::Transaction(
456 "Nested transactions are not supported.".to_string(),
457 ));
458 }
459 let id = new_transaction_id();
460 let started_ms = now_unix_ms();
461 self.current = Some(PendingTransaction {
462 id,
463 transitions: Vec::new(),
464 auto_commit: false,
465 started_ms,
466 });
467 Ok(TransactionHandle { id, started_ms })
468 }
469
470 pub fn commit(&mut self) -> Result<(), LinkError> {
471 if self.current.is_none() {
472 return Ok(());
473 }
474 self.commit_current()
475 }
476
477 fn commit_current(&mut self) -> Result<(), LinkError> {
478 let pending = match self.current.take() {
479 Some(p) => p,
480 None => return Ok(()),
481 };
482 self.committed.insert(pending.id);
483 self.write_marker(&format!("{COMMIT_MARKER_PREFIX}{:032x}", pending.id))?;
484 if self.trace {
485 eprintln!(
486 "[Transactions] Committed tx {:032x} (mode={:?}, transitions={}).",
487 pending.id,
488 self.commit_mode,
489 pending.transitions.len()
490 );
491 }
492 for transition in &pending.transitions {
493 self.mark_applied(transition)?;
494 }
495 let _ = pending.auto_commit;
496 let _ = pending.started_ms;
497 self.enforce_retention()?;
498 Ok(())
499 }
500
501 pub fn rollback(&mut self) -> Result<(), LinkError> {
502 let pending = match self.current.take() {
503 Some(p) => p,
504 None => return Ok(()),
505 };
506 self.rolled_back.insert(pending.id);
507 self.replaying = true;
508 for transition in pending.transitions.iter().rev() {
509 self.try_revert_transition(transition);
510 }
511 self.replaying = false;
512 self.write_marker(&format!("{ROLLBACK_MARKER_PREFIX}{:032x}", pending.id))?;
513 if self.trace {
514 eprintln!(
515 "[Transactions] Rolled back tx {:032x} ({} transitions).",
516 pending.id,
517 pending.transitions.len(),
518 );
519 }
520 self.enforce_retention()?;
521 Ok(())
522 }
523
524 pub fn apply_transition(&mut self, transition: &GenericTransition<T>) {
527 self.replaying = true;
528 self.try_apply_transition(transition, false);
529 self.replaying = false;
530 }
531
532 pub fn revert_transition(&mut self, transition: &GenericTransition<T>) {
535 self.replaying = true;
536 self.try_revert_transition(transition);
537 self.replaying = false;
538 }
539
540 fn try_apply_transition(&mut self, transition: &GenericTransition<T>, record_applied: bool) {
541 let zero = T::from_byte(0);
542 let result: Result<(), LinkError> = match transition.kind {
543 TransitionKind::Create => {
544 if transition.after.index != zero && !self.inner.link_exists(transition.after.index)
545 {
546 self.inner
547 .ensure_link_created(transition.after.index)
548 .and_then(|_| {
549 self.inner
550 .update_link(
551 transition.after.index,
552 transition.after.source,
553 transition.after.target,
554 )
555 .map(|_| ())
556 })
557 } else {
558 Ok(())
559 }
560 }
561 TransitionKind::Update => {
562 if transition.after.index != zero && self.inner.link_exists(transition.after.index)
563 {
564 self.inner
565 .update_link(
566 transition.after.index,
567 transition.after.source,
568 transition.after.target,
569 )
570 .map(|_| ())
571 } else {
572 Ok(())
573 }
574 }
575 TransitionKind::Delete => {
576 if transition.before.index != zero
577 && self.inner.link_exists(transition.before.index)
578 {
579 self.inner.delete_link(transition.before.index).map(|_| ())
580 } else {
581 Ok(())
582 }
583 }
584 };
585 if let Err(e) = result {
586 if self.trace {
587 eprintln!(
588 "[Transactions] Failed to apply transition seq={}: {e}",
589 transition.sequence
590 );
591 }
592 }
593 if record_applied {
594 let _ = self.mark_applied(transition);
595 }
596 }
597
598 fn try_revert_transition(&mut self, transition: &GenericTransition<T>) {
599 let zero = T::from_byte(0);
600 let result: Result<(), LinkError> = match transition.kind {
601 TransitionKind::Create => {
602 if transition.after.index != zero && self.inner.link_exists(transition.after.index)
603 {
604 self.inner.delete_link(transition.after.index).map(|_| ())
605 } else {
606 Ok(())
607 }
608 }
609 TransitionKind::Update => {
610 if transition.before.index != zero
611 && self.inner.link_exists(transition.before.index)
612 {
613 self.inner
614 .update_link(
615 transition.before.index,
616 transition.before.source,
617 transition.before.target,
618 )
619 .map(|_| ())
620 } else {
621 Ok(())
622 }
623 }
624 TransitionKind::Delete => {
625 if transition.before.index != zero
626 && !self.inner.link_exists(transition.before.index)
627 {
628 self.inner
629 .ensure_link_created(transition.before.index)
630 .and_then(|_| {
631 self.inner
632 .update_link(
633 transition.before.index,
634 transition.before.source,
635 transition.before.target,
636 )
637 .map(|_| ())
638 })
639 } else {
640 Ok(())
641 }
642 }
643 };
644 if let Err(e) = result {
645 if self.trace {
646 eprintln!(
647 "[Transactions] Failed to revert transition seq={}: {e}",
648 transition.sequence
649 );
650 }
651 }
652 }
653
654 fn mark_applied(&mut self, transition: &GenericTransition<T>) -> Result<(), LinkError> {
655 if self.applied.insert(transition.sequence) {
656 self.write_marker(&format!("{APPLIED_MARKER_PREFIX}{}", transition.sequence))?;
657 if transition.sequence > self.applied_sequence {
658 self.applied_sequence = transition.sequence;
659 }
660 }
661 Ok(())
662 }
663
664 pub fn recover(&mut self) -> Result<(), LinkError> {
676 self.log.clear();
677 self.committed.clear();
678 self.rolled_back.clear();
679 self.applied.clear();
680 self.sequence_counter = 0;
681 self.applied_sequence = 0;
682
683 for entry in self.log_store.read_log_entries()? {
684 if let Some(payload) = entry.strip_prefix(TRANSITION_NAME_PREFIX) {
685 match GenericTransition::<T>::parse(payload) {
686 Ok(transition) => {
687 insert_ordered(&mut self.log, transition);
688 if transition.sequence > self.sequence_counter {
689 self.sequence_counter = transition.sequence;
690 }
691 }
692 Err(LinkError::AddressOutOfRange(value)) => {
693 return Err(LinkError::AddressOutOfRange(value))
694 }
695 Err(error) => {
696 if self.trace {
697 eprintln!("[Transactions] Skipping unreadable log entry: {error}");
698 }
699 }
700 }
701 } else if let Some(rest) = entry.strip_prefix(COMMIT_MARKER_PREFIX) {
702 if let Ok(tx_id) = u128::from_str_radix(rest, 16) {
703 self.committed.insert(tx_id);
704 }
705 } else if let Some(rest) = entry.strip_prefix(ROLLBACK_MARKER_PREFIX) {
706 if let Ok(tx_id) = u128::from_str_radix(rest, 16) {
707 self.rolled_back.insert(tx_id);
708 }
709 } else if let Some(rest) = entry.strip_prefix(APPLIED_MARKER_PREFIX) {
710 if let Ok(seq) = rest.parse::<i64>() {
711 self.applied.insert(seq);
712 if seq > self.applied_sequence {
713 self.applied_sequence = seq;
714 }
715 }
716 }
717 }
718
719 let log_snapshot: Vec<GenericTransition<T>> = self.log.clone();
721 self.replaying = true;
722 for transition in &log_snapshot {
723 if !self.committed.contains(&transition.transaction_id) {
724 continue;
725 }
726 if self.applied.contains(&transition.sequence) {
727 continue;
728 }
729 self.try_apply_transition(transition, true);
730 }
731 let mut pending_tx_ids: Vec<u128> = Vec::new();
733 for transition in log_snapshot.iter().rev() {
734 if self.committed.contains(&transition.transaction_id) {
735 continue;
736 }
737 if self.rolled_back.contains(&transition.transaction_id) {
738 continue;
739 }
740 self.try_revert_transition(transition);
741 if !pending_tx_ids.contains(&transition.transaction_id) {
742 pending_tx_ids.push(transition.transaction_id);
743 }
744 }
745 self.replaying = false;
746 for tx_id in pending_tx_ids {
747 self.rolled_back.insert(tx_id);
748 self.write_marker(&format!("{ROLLBACK_MARKER_PREFIX}{tx_id:032x}"))?;
749 }
750 Ok(())
751 }
752
753 fn enforce_retention(&mut self) -> Result<(), LinkError> {
754 match self.retention_policy.clone() {
755 LogRetentionPolicy::Infinite => Ok(()),
756 LogRetentionPolicy::Sized { max_transitions } => self.enforce_sized(max_transitions),
757 LogRetentionPolicy::Chunked {
758 chunk_size,
759 archive_directory,
760 } => self.enforce_chunked(chunk_size, &archive_directory),
761 }
762 }
763
764 fn enforce_sized(&mut self, max_transitions: u64) -> Result<(), LinkError> {
765 if max_transitions == 0 {
766 return Ok(());
767 }
768 while self.log.len() as u64 > max_transitions {
769 let head = self.log[0];
770 if !self.applied.contains(&head.sequence) {
771 self.replaying = true;
772 self.try_apply_transition(&head, true);
773 self.replaying = false;
774 if !self.applied.contains(&head.sequence) {
775 break; }
777 }
778 self.log.remove(0);
779 if self.trace {
780 eprintln!(
781 "[Transactions] Dropped applied transition seq={} per sized retention.",
782 head.sequence
783 );
784 }
785 }
786 Ok(())
787 }
788
789 fn enforce_chunked(
790 &mut self,
791 chunk_size: u64,
792 archive_directory: &Path,
793 ) -> Result<(), LinkError> {
794 if chunk_size == 0 {
795 return Ok(());
796 }
797 if (self.log.len() as u64) < chunk_size {
798 return Ok(());
799 }
800 let chunk: Vec<GenericTransition<T>> =
801 self.log.iter().take(chunk_size as usize).copied().collect();
802 for transition in &chunk {
803 if !self.applied.contains(&transition.sequence) {
804 self.replaying = true;
805 self.try_apply_transition(transition, true);
806 self.replaying = false;
807 if !self.applied.contains(&transition.sequence) {
808 return Ok(()); }
810 }
811 }
812 std::fs::create_dir_all(archive_directory).map_err(|error| {
813 LinkError::StorageError(format!(
814 "failed to create archive dir {}: {error}",
815 archive_directory.display()
816 ))
817 })?;
818 let timestamp = now_unix_ms();
819 let file_name = format!(
820 "transitions-chunk-{timestamp}-{:032x}.log",
821 new_transaction_id()
822 );
823 let path = archive_directory.join(file_name);
824 use std::io::Write;
825 let mut file = std::fs::File::create(&path).map_err(|error| {
826 LinkError::StorageError(format!(
827 "failed to create archive file {}: {error}",
828 path.display()
829 ))
830 })?;
831 for transition in &chunk {
832 writeln!(file, "{}", transition.serialize())?;
833 }
834 file.flush()?;
835 if self.trace {
836 eprintln!(
837 "[Transactions] Archived {} transitions to {}.",
838 chunk.len(),
839 path.display()
840 );
841 }
842 self.log.drain(0..chunk.len());
843 Ok(())
844 }
845}
846
847impl<T, S, L> GenericTransactionsDecorator<T, S, L>
850where
851 T: LinkReference,
852 S: LinksStorageRef<T>,
853 L: TransitionLogStore,
854{
855 pub fn get(&self, id: T) -> Option<&GenericLink<T>> {
856 self.inner.get_link_ref(id)
857 }
858
859 pub fn all(&self) -> Vec<&GenericLink<T>> {
860 self.inner.all_link_refs()
861 }
862
863 pub fn query(
864 &self,
865 index: Option<T>,
866 source: Option<T>,
867 target: Option<T>,
868 ) -> Vec<&GenericLink<T>> {
869 self.inner.query_link_refs(index, source, target)
870 }
871}
872
873fn insert_ordered<T: LinkReference>(
876 list: &mut Vec<GenericTransition<T>>,
877 transition: GenericTransition<T>,
878) {
879 let mut lo = 0usize;
880 let mut hi = list.len();
881 while lo < hi {
882 let mid = (lo + hi) / 2;
883 if list[mid].sequence < transition.sequence {
884 lo = mid + 1;
885 } else {
886 hi = mid;
887 }
888 }
889 list.insert(lo, transition);
890}
891
892static TX_COUNTER: AtomicU64 = AtomicU64::new(0);
893
894fn new_transaction_id() -> u128 {
895 let count = TX_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
898 let now = now_unix_ms() as u128;
899 (now << 64) | count
900}
901
902fn now_unix_ms() -> i64 {
903 SystemTime::now()
904 .duration_since(UNIX_EPOCH)
905 .map(|d| d.as_millis() as i64)
906 .unwrap_or(0)
907}
908
909#[cfg(test)]
910mod tests {
911 use super::*;
912
913 #[test]
914 fn retention_policy_parses_specs() {
915 assert!(matches!(
916 LogRetentionPolicy::parse("infinite").unwrap(),
917 LogRetentionPolicy::Infinite
918 ));
919 assert!(matches!(
920 LogRetentionPolicy::parse("sized:1000").unwrap(),
921 LogRetentionPolicy::Sized {
922 max_transitions: 1000
923 }
924 ));
925 match LogRetentionPolicy::parse("chunked:500:/tmp/x").unwrap() {
926 LogRetentionPolicy::Chunked {
927 chunk_size,
928 archive_directory,
929 } => {
930 assert_eq!(chunk_size, 500);
931 assert_eq!(archive_directory, PathBuf::from("/tmp/x"));
932 }
933 _ => panic!("expected Chunked"),
934 }
935 assert!(LogRetentionPolicy::parse("garbage").is_err());
936 }
937
938 #[test]
939 fn transition_round_trips_through_serialize() {
940 let t = Transition {
941 transaction_id: 0xabcdef1234567890u128,
942 sequence: 42,
943 timestamp_ms: 1234567890,
944 kind: TransitionKind::Update,
945 before: DoubletLink::new(1, 2, 3),
946 after: DoubletLink::new(1, 4, 5),
947 };
948 let parsed = Transition::try_parse(&t.serialize()).unwrap();
949 assert_eq!(t, parsed);
950 }
951
952 #[test]
953 fn wide_transition_is_rejected_by_a_narrow_address_type() {
954 let wide = GenericTransition::<u64> {
955 transaction_id: 7,
956 sequence: 1,
957 timestamp_ms: 0,
958 kind: TransitionKind::Create,
959 before: GenericDoubletLink::empty(),
960 after: GenericDoubletLink::new(u32::MAX as u64 + 1, 0, 0),
961 };
962 assert!(matches!(
963 GenericTransition::<u32>::parse(&wide.serialize()),
964 Err(LinkError::AddressOutOfRange(_))
965 ));
966 assert!(GenericTransition::<u64>::parse(&wide.serialize()).is_ok());
967 }
968}