1use itertools::Itertools as _;
4use serde::{Deserialize, Serialize};
5use std::{
6 borrow::Cow,
7 collections::{BTreeSet, HashMap},
8};
9use tracing::trace;
10
11use crate::{
12 IcechunkFormatError, IcechunkFormatErrorKind, IcechunkResult, SnapshotId,
13 flatbuffers::generated,
14 format_constants::SpecVersionBin,
15 lookup_index_by_key,
16 snapshot::{SnapshotInfo, SnapshotProperties},
17};
18
19use chrono::{DateTime, Utc};
20use flatbuffers::{UnionWIPOffset, VerifierOptions, WIPOffset};
21use icechunk_types::ICResultExt as _;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum RepoAvailability {
25 Online,
26 ReadOnly,
27 }
30
31impl From<generated::RepoAvailability> for RepoAvailability {
32 fn from(value: generated::RepoAvailability) -> Self {
33 match value {
34 generated::RepoAvailability::Online => RepoAvailability::Online,
35 generated::RepoAvailability::ReadOnly => RepoAvailability::ReadOnly,
36 _ => RepoAvailability::Online,
37 }
38 }
39}
40
41impl From<RepoAvailability> for generated::RepoAvailability {
42 fn from(value: RepoAvailability) -> Self {
43 match value {
44 RepoAvailability::Online => generated::RepoAvailability::Online,
45 RepoAvailability::ReadOnly => generated::RepoAvailability::ReadOnly,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct RepoStatus {
52 pub availability: RepoAvailability,
53 pub set_at: DateTime<Utc>,
54 pub limited_availability_reason: Option<String>,
55}
56
57impl TryFrom<generated::RepoStatus<'_>> for RepoStatus {
58 type Error = IcechunkFormatError;
59
60 fn try_from(fb_status: generated::RepoStatus<'_>) -> Result<Self, Self::Error> {
61 let ts: i64 = fb_status
62 .set_at()
63 .try_into()
64 .map_err(|_| IcechunkFormatErrorKind::InvalidTimestamp)
65 .capture()?;
66 let set_at = DateTime::from_timestamp_micros(ts)
67 .ok_or(IcechunkFormatErrorKind::InvalidTimestamp)
68 .capture()?;
69 Ok(RepoStatus {
70 availability: fb_status.availability().into(),
71 set_at,
72 limited_availability_reason: fb_status
73 .limited_availability_reason()
74 .map(|s| s.to_string()),
75 })
76 }
77}
78
79impl RepoStatus {
80 pub fn error_msg(&self) -> String {
81 format!(
82 "Repo status is {0:?}, set at {1}, reason: {2:?}",
83 self.availability, self.set_at, self.limited_availability_reason
84 )
85 }
86}
87
88#[derive(PartialEq, Serialize, Deserialize)]
90pub struct RepoInfo {
91 buffer: Vec<u8>,
92}
93
94impl std::fmt::Debug for RepoInfo {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 let tags = self.tags().map(Vec::from_iter).unwrap_or_default();
97 let tags =
98 tags.into_iter().map(|(name, snap)| format!("{name} -> {snap}")).join(", ");
99 let branches = self.branches().map(Vec::from_iter).unwrap_or_default();
100 let branches = branches
101 .into_iter()
102 .map(|(name, snap)| format!("{name} -> {snap}"))
103 .join(", ");
104 let snaps = self.all_snapshots().map(Vec::from_iter).unwrap_or_default();
105 let snaps = snaps
106 .into_iter()
107 .map(|ms| match ms {
108 Ok(snap) => format!(
109 "{} -> {}",
110 snap.id,
111 snap.parent_id.map(|s| s.to_string()).unwrap_or_default()
112 ),
113 Err(_) => "#err".to_string(),
114 })
115 .join(", ");
116 f.debug_struct("RepoInfo")
118 .field("tags", &tags)
119 .field("branches", &branches)
120 .field("snapshots", &snaps)
121 .finish_non_exhaustive()
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum UpdateType {
127 RepoInitializedUpdate,
128 RepoMigratedUpdate {
129 from_version: SpecVersionBin,
130 to_version: SpecVersionBin,
131 },
132 RepoStatusChangedUpdate {
133 status: RepoStatus,
134 },
135 ConfigChangedUpdate,
136 MetadataChangedUpdate,
137 TagCreatedUpdate {
138 name: String,
139 },
140 TagDeletedUpdate {
141 name: String,
142 previous_snap_id: SnapshotId,
143 },
144 BranchCreatedUpdate {
145 name: String,
146 },
147 BranchDeletedUpdate {
148 name: String,
149 previous_snap_id: SnapshotId,
150 },
151 BranchResetUpdate {
152 name: String,
153 previous_snap_id: SnapshotId,
154 },
155 NewCommitUpdate {
156 branch: String,
157 new_snap_id: SnapshotId,
158 },
159 CommitAmendedUpdate {
160 branch: String,
161 previous_snap_id: SnapshotId,
162 new_snap_id: SnapshotId,
163 },
164 NewDetachedSnapshotUpdate {
165 new_snap_id: SnapshotId,
166 },
167 GCRanUpdate,
168 ExpirationRanUpdate,
169 FeatureFlagChanged {
170 id: u16,
171 new_value: Option<bool>,
172 },
173}
174
175static ROOT_OPTIONS: VerifierOptions = VerifierOptions {
176 max_depth: 10,
177 max_tables: 5_000_000,
178 max_apparent_size: 1 << 31, ignore_missing_null_terminator: true,
180};
181
182#[derive(Debug, Clone)]
183pub struct UpdateInfo<I> {
184 pub update_type: UpdateType,
185 pub update_time: DateTime<Utc>,
186 pub previous_updates: I,
187}
188
189type UpdateTuple<'a> = IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)>;
190
191impl RepoInfo {
192 #[expect(clippy::too_many_arguments)]
193 pub fn new<
194 'a,
195 I: IntoIterator<Item = IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)>>,
196 EFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
197 DFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
198 >(
199 spec_version: SpecVersionBin,
200 tags: impl IntoIterator<Item = (&'a str, SnapshotId)>,
201 branches: impl IntoIterator<Item = (&'a str, SnapshotId)>,
202 deleted_tags: impl IntoIterator<Item = &'a str>,
203 snapshots: impl IntoIterator<Item = SnapshotInfo>,
204 metadata: &SnapshotProperties,
205 update: UpdateInfo<I>,
206 backup_path: Option<&'a str>,
207 num_updates_per_file: u16,
208 previous_info: Option<&'a str>,
209 config_bytes: Option<&[u8]>,
210 sorted_enabled_feature_flags: Option<EFFIt>,
211 sorted_disabled_feature_flags: Option<DFFIt>,
212 status: &RepoStatus,
213 ) -> IcechunkResult<Self> {
214 let mut snapshots: Vec<_> = snapshots.into_iter().collect();
215 snapshots.sort_by(|a, b| a.id.0.cmp(&b.id.0));
216 let tags = resolve_ref_iter(&snapshots, tags)?;
217 let branches = resolve_ref_iter(&snapshots, branches)?;
218 let mut deleted_tags: Vec<_> = deleted_tags.into_iter().collect();
219 deleted_tags.sort();
220 Self::from_parts(
221 spec_version,
222 tags,
223 branches,
224 deleted_tags,
225 snapshots,
226 metadata,
227 update,
228 backup_path,
229 num_updates_per_file,
230 previous_info,
231 config_bytes,
232 sorted_enabled_feature_flags,
233 sorted_disabled_feature_flags,
234 status,
235 )
236 }
237
238 #[expect(clippy::too_many_arguments)]
239 fn from_parts<
240 'a,
241 I: IntoIterator<Item = IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)>>,
242 EFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
243 DFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
244 >(
245 spec_version: SpecVersionBin,
246 sorted_tags: impl IntoIterator<Item = (&'a str, u32)>,
247 sorted_branches: impl IntoIterator<Item = (&'a str, u32)>,
248 sorted_deleted_tags: impl IntoIterator<Item = &'a str>,
249 sorted_snapshots: impl IntoIterator<Item = SnapshotInfo>,
250 metadata: &SnapshotProperties,
251 update: UpdateInfo<I>,
252 backup_path: Option<&'a str>,
253 num_updates_per_file: u16,
254 previous_info: Option<&'a str>,
255 config_bytes: Option<&[u8]>,
256 sorted_enabled_feature_flags: Option<EFFIt>,
257 sorted_disabled_feature_flags: Option<DFFIt>,
258 status: &RepoStatus,
259 ) -> IcechunkResult<Self> {
260 trace!("Creating new repo info from parts");
261 let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(4_096);
262 let tags = sorted_tags
263 .into_iter()
264 .map(|(name, offset)| {
265 let args = generated::RefArgs {
266 name: Some(builder.create_string(name)),
267 snapshot_index: offset,
268 };
269 generated::Ref::create(&mut builder, &args)
270 })
271 .collect::<Vec<_>>();
272 let tags = builder.create_vector(&tags);
273
274 let mut main_found = false;
275 let branches = sorted_branches
276 .into_iter()
277 .map(|(name, offset)| {
278 if name == icechunk_types::DEFAULT_BRANCH {
279 main_found = true;
280 }
281 let args = generated::RefArgs {
282 name: Some(builder.create_string(name)),
283 snapshot_index: offset,
284 };
285 generated::Ref::create(&mut builder, &args)
286 })
287 .collect::<Vec<_>>();
288 if !main_found {
289 return Err(IcechunkFormatErrorKind::BranchNotFound {
290 branch: icechunk_types::DEFAULT_BRANCH.to_string(),
291 })
292 .capture();
293 }
294
295 let branches = builder.create_vector(&branches);
296
297 let deleted_tags = sorted_deleted_tags
298 .into_iter()
299 .map(|name| builder.create_string(name))
300 .collect::<Vec<_>>();
301 let deleted_tags = builder.create_vector(&deleted_tags);
302
303 let snapshots: Vec<_> = sorted_snapshots.into_iter().collect();
304 debug_assert!(snapshots.is_sorted_by(|a, b| a.id.0 <= b.id.0));
305
306 let snapshot_index: HashMap<_, _> =
307 snapshots.iter().enumerate().map(|(ix, sn)| (sn.id.clone(), ix)).collect();
308
309 let snapshots: Vec<_> = snapshots
311 .iter()
312 .map(|snap| {
313 let id = &snap.id.0;
314 let id = generated::ObjectId12::new(id);
315 let parent_offset = match snap.parent_id.as_ref() {
316 Some(parent_id) => {
317 let index = snapshot_index
318 .get(parent_id)
319 .ok_or_else(|| IcechunkFormatErrorKind::SnapshotIdNotFound {
320 snapshot_id: snap.id.clone(),
321 })
322 .capture()?;
323 Ok(*index as i32)
324 }
325 None => Ok::<_, IcechunkFormatError>(-1),
326 }?;
327
328 let metadata_items: Vec<_> = snap
329 .metadata
330 .iter()
331 .map(|(k, v)| {
332 let name = builder.create_shared_string(k.as_str());
333 let serialized =
334 flexbuffers::to_vec(v).map_err(Box::new).capture()?;
335 let value = builder.create_vector(serialized.as_slice());
336 let item = generated::MetadataItem::create(
337 &mut builder,
338 &generated::MetadataItemArgs {
339 name: Some(name),
340 value: Some(value),
341 },
342 );
343 Ok::<_, IcechunkFormatError>(item)
344 })
345 .try_collect()?;
346
347 let metadata = builder.create_vector(metadata_items.as_slice());
348 let args = generated::SnapshotInfoArgs {
349 id: Some(&id),
350 parent_offset,
351 flushed_at: snap.flushed_at.timestamp_micros() as u64,
352 message: Some(builder.create_string(snap.message.as_str())),
353 metadata: Some(metadata),
354 };
355 Ok::<_, IcechunkFormatError>(generated::SnapshotInfo::create(
356 &mut builder,
357 &args,
358 ))
359 })
360 .try_collect()?;
361 let snapshots = builder.create_vector(&snapshots);
362
363 let limited_reason = status
364 .limited_availability_reason
365 .as_deref()
366 .map(|s| builder.create_string(s));
367 let status = generated::RepoStatus::create(
368 &mut builder,
369 &generated::RepoStatusArgs {
370 availability: status.availability.into(),
371 set_at: status.set_at.timestamp_micros() as u64,
372 limited_availability_reason: limited_reason,
373 },
374 );
375
376 let metadata_items: Vec<_> = metadata
377 .iter()
378 .map(|(k, v)| {
379 let name = builder.create_shared_string(k.as_str());
380 let serialized = flexbuffers::to_vec(v).map_err(Box::new).capture()?;
381 let value = builder.create_vector(serialized.as_slice());
382 let item = generated::MetadataItem::create(
383 &mut builder,
384 &generated::MetadataItemArgs { name: Some(name), value: Some(value) },
385 );
386 Ok::<_, IcechunkFormatError>(item)
387 })
388 .try_collect()?;
389
390 let metadata = builder.create_vector(metadata_items.as_slice());
391
392 let enabled_feature_flags =
393 sorted_enabled_feature_flags.map(|it| builder.create_vector_from_iter(it));
394 let disabled_feature_flags =
395 sorted_disabled_feature_flags.map(|it| builder.create_vector_from_iter(it));
396
397 let (latest_updates, repo_before_updates) = Self::mk_latest_updates(
398 &mut builder,
399 update,
400 backup_path,
401 num_updates_per_file,
402 previous_info,
403 )?;
404
405 let config = config_bytes.map(|bytes| builder.create_vector(bytes));
406
407 let repo_args = generated::RepoArgs {
409 tags: Some(tags),
410 branches: Some(branches),
411 deleted_tags: Some(deleted_tags),
412 snapshots: Some(snapshots),
413 spec_version: spec_version as u8,
414 status: Some(status),
415 metadata: Some(metadata),
416 latest_updates: Some(latest_updates),
417 repo_before_updates,
418 config,
419 enabled_feature_flags,
420 disabled_feature_flags,
421 ..Default::default()
422 };
423 let repo = generated::Repo::create(&mut builder, &repo_args);
424 builder.finish(repo, Some("Ichk"));
425 let (mut buffer, offset) = builder.collapse();
426 buffer.drain(0..offset);
427 buffer.shrink_to_fit();
428 Ok(Self { buffer })
429 }
430
431 #[expect(clippy::type_complexity)]
432 fn mk_latest_updates<
433 'bldr,
434 'a,
435 I: IntoIterator<Item = IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)>>,
436 >(
437 builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
438 update: UpdateInfo<I>,
439 backup_path: Option<&'a str>,
440 num_updates_per_file: u16,
441 previous_info: Option<&'a str>,
442 ) -> IcechunkResult<(
443 WIPOffset<
444 flatbuffers::Vector<
445 'bldr,
446 flatbuffers::ForwardsUOffset<generated::Update<'bldr>>,
447 >,
448 >,
449 Option<WIPOffset<&'bldr str>>,
450 )> {
451 let mut previous_updates = update.previous_updates.into_iter();
453 let last_update = previous_updates.next().map(|maybe_data| {
454 maybe_data.map(|(ut, dt, path)| {
455 assert!(
456 path.is_none(),
457 "Invalid latest update iterator, last element has backup path"
458 );
459 (ut, dt, backup_path)
460 })
461 });
462 assert!(
469 backup_path.is_none() || last_update.is_some(),
470 "A backup path must not be provided without previous updates"
471 );
472
473 if let Some(Ok((_, latest_time, _))) = &last_update
475 && update.update_time <= *latest_time
476 {
477 return Err(IcechunkFormatErrorKind::InvalidUpdateTimestamp {
478 latest_time: *latest_time,
479 new_time: update.update_time,
480 })
481 .capture();
482 }
483
484 let new_updates: Box<dyn Iterator<Item = _>> =
485 if let Some(last_update) = last_update {
486 Box::new(
487 [Ok((update.update_type, update.update_time, None)), last_update]
488 .into_iter(),
489 )
490 } else {
491 Box::new([Ok((update.update_type, update.update_time, None))].into_iter())
492 };
493
494 let all_updates = new_updates.into_iter().chain(previous_updates);
495 let mut repo_before_updates = previous_info;
498
499 let num_updates = num_updates_per_file as usize;
500 let mut updates = Vec::new();
501 for maybe_data in all_updates {
502 let (u_type, u_time, file) = maybe_data?;
503
504 if updates.len() >= num_updates
509 && let Some(bp) = file
510 {
511 repo_before_updates = Some(bp);
512 break;
513 }
514
515 let (update_type_type, update_type) = update_type_to_fb(builder, &u_type)?;
516 let file = file.map(|file| builder.create_string(file));
517 updates.push(generated::Update::create(
518 builder,
519 &generated::UpdateArgs {
520 update_type_type,
521 update_type: Some(update_type),
522 updated_at: u_time.timestamp_micros() as u64,
523 backup_path: file,
524 },
525 ));
526 }
527
528 debug_assert!(!updates.is_empty(), "Must have at least one update in repo file");
529
530 let updates = builder.create_vector(&updates);
531 let repo_before_updates = repo_before_updates.map(|s| builder.create_string(s));
532 Ok((updates, repo_before_updates))
533 }
534
535 pub fn initial<C: Serialize>(
536 spec_version: SpecVersionBin,
537 snapshot: SnapshotInfo,
538 num_updates_per_file: u16,
539 config: Option<&C>,
540 update_time: Option<DateTime<Utc>>,
541 ) -> Self {
542 #[expect(clippy::expect_used)]
543 let config_bytes =
544 config.map(|c| flexbuffers::to_vec(c).expect("Cannot serialize config"));
545 #[expect(clippy::expect_used)]
547 Self::from_parts(
548 spec_version,
549 [],
550 [("main", 0)],
551 [],
552 [snapshot],
553 &Default::default(),
554 UpdateInfo {
555 update_type: UpdateType::RepoInitializedUpdate,
556 update_time: update_time.unwrap_or_else(Utc::now),
557 previous_updates: [],
558 },
559 None,
560 num_updates_per_file,
561 None,
562 config_bytes.as_deref(),
563 None::<std::iter::Empty<u16>>,
564 None::<std::iter::Empty<u16>>,
565 &RepoStatus {
566 availability: RepoAvailability::Online,
567 set_at: update_time.unwrap_or_else(Utc::now),
568 limited_availability_reason: None,
569 },
570 )
571 .expect("Cannot generate initial snapshot")
572 }
573
574 pub fn config_bytes_raw(&self) -> IcechunkResult<Option<Vec<u8>>> {
576 Ok(self.root()?.config().map(|v| v.bytes().to_vec()))
577 }
578
579 pub fn config<C: serde::de::DeserializeOwned>(&self) -> IcechunkResult<Option<C>> {
583 match self.root()?.config() {
584 None => Ok(None),
585 Some(config_fb) => {
586 let config: C = flexbuffers::from_slice(config_fb.bytes())
587 .map_err(Box::new)
588 .capture()?;
589 Ok(Some(config))
590 }
591 }
592 }
593
594 pub fn metadata(&self) -> IcechunkResult<SnapshotProperties> {
595 self.root()?
596 .metadata()
597 .unwrap_or_default()
598 .iter()
599 .map(|item| {
600 let key = item.name().to_string();
601 let value = flexbuffers::from_slice(item.value().bytes())
602 .map_err(Box::new)
603 .capture()?;
604 Ok((key, value))
605 })
606 .try_collect()
607 }
608
609 pub fn status(&self) -> IcechunkResult<RepoStatus> {
610 let root = self.root()?;
611 let fb_status = root.status();
612 fb_status.try_into()
613 }
614
615 pub fn enabled_feature_flags(
616 &self,
617 ) -> IcechunkResult<Option<impl DoubleEndedIterator<Item = u16> + ExactSizeIterator>>
618 {
619 Ok(self.root()?.enabled_feature_flags().map(|v| v.iter()))
620 }
621
622 pub fn disabled_feature_flags(
623 &self,
624 ) -> IcechunkResult<Option<impl DoubleEndedIterator<Item = u16> + ExactSizeIterator>>
625 {
626 Ok(self.root()?.disabled_feature_flags().map(|v| v.iter()))
627 }
628
629 pub fn feature_flag_enabled(&self, id: u16) -> IcechunkResult<Option<bool>> {
633 let root = self.root()?;
634 if root
635 .enabled_feature_flags()
636 .and_then(|v| v.lookup_by_key(id, |this, key| this.cmp(key)))
637 .is_some()
638 {
639 return Ok(Some(true));
640 }
641 if root
642 .disabled_feature_flags()
643 .and_then(|v| v.lookup_by_key(id, |this, key| this.cmp(key)))
644 .is_some()
645 {
646 return Ok(Some(false));
647 }
648 Ok(None)
649 }
650
651 fn all_tags(&self) -> IcechunkResult<impl Iterator<Item = (&str, u32)>> {
652 Ok(self.root()?.tags().iter().map(|r| (r.name(), r.snapshot_index())))
653 }
654
655 fn all_branches(&self) -> IcechunkResult<impl Iterator<Item = (&str, u32)>> {
656 Ok(self.root()?.branches().iter().map(|r| (r.name(), r.snapshot_index())))
657 }
658
659 pub fn deleted_tags(&self) -> IcechunkResult<impl Iterator<Item = &str>> {
660 Ok(self.root()?.deleted_tags().iter())
661 }
662
663 pub fn all_snapshots(
664 &self,
665 ) -> IcechunkResult<impl Iterator<Item = IcechunkResult<SnapshotInfo>>> {
666 let root = self.root()?;
667 Ok(root.snapshots().iter().map(move |snap| mk_snapshot_info(&root, &snap)))
668 }
669
670 pub fn update_feature_flag(
672 &self,
673 spec_version: SpecVersionBin,
674 flag_id: u16,
675 enabled: Option<bool>,
676 previous_file: &str,
677 num_updates_per_file: u16,
678 ) -> IcechunkResult<Self> {
679 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
680 let (eff, dff): (Option<Vec<_>>, Option<Vec<_>>) = match enabled {
681 Some(false) => {
682 let e = self
683 .enabled_feature_flags()?
684 .map(|it| it.filter(|x| *x != flag_id).collect());
685
686 let mut d: BTreeSet<_> = self
687 .disabled_feature_flags()?
688 .map(|it| it.collect())
689 .unwrap_or_default();
690 d.insert(flag_id);
691 let d = d.into_iter().collect();
692 (e, Some(d))
693 }
694 Some(true) => {
695 let d = self
696 .disabled_feature_flags()?
697 .map(|it| it.filter(|x| *x != flag_id).collect());
698
699 let mut e: BTreeSet<_> = self
700 .enabled_feature_flags()?
701 .map(|it| it.collect())
702 .unwrap_or_default();
703 e.insert(flag_id);
704 let e = e.into_iter().collect();
705 (Some(e), d)
706 }
707 None => {
708 let e = self
709 .enabled_feature_flags()?
710 .map(|it| it.filter(|x| *x != flag_id).collect());
711 let d = self
712 .disabled_feature_flags()?
713 .map(|it| it.filter(|x| *x != flag_id).collect());
714 (e, d)
715 }
716 };
717
718 Self::from_parts(
719 spec_version,
720 self.all_tags()?,
721 self.all_branches()?,
722 self.deleted_tags()?,
723 snaps,
724 &self.metadata()?,
725 UpdateInfo {
726 update_type: UpdateType::FeatureFlagChanged {
727 id: flag_id,
728 new_value: enabled,
729 },
730 update_time: Utc::now(),
731 previous_updates: self.latest_updates()?,
732 },
733 Some(previous_file),
734 num_updates_per_file,
735 self.repo_before_updates()?,
736 self.config_bytes_raw()?.as_deref(),
737 eff.map(|it| it.into_iter()),
738 dff.map(|it| it.into_iter()),
739 &self.status()?,
740 )
741 }
742
743 #[expect(clippy::too_many_arguments)]
744 pub fn add_snapshot(
745 &self,
746 spec_version: SpecVersionBin,
747 snap: SnapshotInfo,
748 branch: Option<&str>,
749 update_type: UpdateType,
750 update_time: Option<DateTime<Utc>>, previous_file: &str,
752 num_updates_per_file: u16,
753 ) -> IcechunkResult<Self> {
754 let mut snapshots: Vec<_> = self.all_snapshots()?.try_collect()?;
755 let new_index = match snapshots.binary_search_by_key(&&snap.id, |snap| &snap.id) {
756 Ok(_) => Err(IcechunkFormatErrorKind::DuplicateSnapshotId {
757 snapshot_id: snap.id.clone(),
758 })
759 .capture(),
760 Err(idx) => Ok(idx),
761 }?;
762
763 snapshots.insert(new_index, snap);
764
765 let tags = self.all_tags()?.map(|(name, idx)| {
766 if idx as usize >= new_index { (name, idx + 1) } else { (name, idx) }
767 });
768 let branches = self.all_branches()?.map(|(name, idx)| {
769 if Some(name) == branch {
770 (name, new_index as u32)
771 } else if idx as usize >= new_index {
772 (name, idx + 1)
773 } else {
774 (name, idx)
775 }
776 });
777
778 let res = Self::from_parts(
779 spec_version,
780 tags,
781 branches,
782 self.deleted_tags()?,
783 snapshots,
784 &self.metadata()?,
785 UpdateInfo {
786 update_type,
787 update_time: update_time.unwrap_or_else(Utc::now),
788 previous_updates: self.latest_updates()?,
789 },
790 Some(previous_file),
791 num_updates_per_file,
792 self.repo_before_updates()?,
793 self.config_bytes_raw()?.as_deref(),
794 self.enabled_feature_flags()?,
795 self.disabled_feature_flags()?,
796 &self.status()?,
797 )?;
798 Ok(res)
799 }
800
801 pub fn add_branch(
802 &self,
803 spec_version: SpecVersionBin,
804 name: &str,
805 snap: &SnapshotId,
806 previous_file: &str,
807 num_updates_per_file: u16,
808 ) -> IcechunkResult<Self> {
809 if let Ok(snapshot_id) = self.resolve_branch(name) {
810 return Err(IcechunkFormatErrorKind::BranchAlreadyExists {
811 branch: name.to_string(),
812 snapshot_id,
813 })
814 .capture();
815 }
816
817 match self.resolve_snapshot_index(snap)? {
818 Some(snap_idx) => {
819 let mut branches: Vec<_> = self.all_branches()?.collect();
820 branches.push((name, snap_idx as u32));
821 branches.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
822 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
823 Ok(Self::from_parts(
824 spec_version,
825 self.all_tags()?,
826 branches,
827 self.deleted_tags()?,
828 snaps,
829 &self.metadata()?,
830 UpdateInfo {
831 update_type: UpdateType::BranchCreatedUpdate {
832 name: name.to_string(),
833 },
834 update_time: Utc::now(),
835 previous_updates: self.latest_updates()?,
836 },
837 Some(previous_file),
838 num_updates_per_file,
839 self.repo_before_updates()?,
840 self.config_bytes_raw()?.as_deref(),
841 self.enabled_feature_flags()?,
842 self.disabled_feature_flags()?,
843 &self.status()?,
844 )?)
845 }
846 None => Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
847 snapshot_id: snap.clone(),
848 })
849 .capture(),
850 }
851 }
852
853 pub fn delete_branch(
854 &self,
855 spec_version: SpecVersionBin,
856 name: &str,
857 previous_file: &str,
858 num_updates_per_file: u16,
859 ) -> IcechunkResult<Self> {
860 match self.resolve_branch(name) {
861 Ok(previous_snap_id) => {
862 let mut branches: Vec<_> = self.all_branches()?.collect();
863 branches.retain(|(n, _)| n != &name);
865 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
866 Self::from_parts(
867 spec_version,
868 self.all_tags()?,
869 branches,
870 self.deleted_tags()?,
871 snaps,
872 &self.metadata()?,
873 UpdateInfo {
874 update_type: UpdateType::BranchDeletedUpdate {
875 name: name.to_string(),
876 previous_snap_id,
877 },
878 update_time: Utc::now(),
879 previous_updates: self.latest_updates()?,
880 },
881 Some(previous_file),
882 num_updates_per_file,
883 self.repo_before_updates()?,
884 self.config_bytes_raw()?.as_deref(),
885 self.enabled_feature_flags()?,
886 self.disabled_feature_flags()?,
887 &self.status()?,
888 )
889 }
890 Err(IcechunkFormatError {
891 kind: IcechunkFormatErrorKind::BranchNotFound { .. },
892 ..
893 }) => {
894 Err(IcechunkFormatErrorKind::BranchNotFound { branch: name.to_string() })
895 .capture()
896 }
897 Err(err) => Err(err),
898 }
899 }
900
901 pub fn update_branch(
902 &self,
903 spec_version: SpecVersionBin,
904 name: &str,
905 new_snap: &SnapshotId,
906 previous_file: &str,
907 num_updates_per_file: u16,
908 ) -> IcechunkResult<Self> {
909 let previous_snap_id = self.resolve_branch(name)?;
910 match self.resolve_snapshot_index(new_snap)? {
911 Some(snap_idx) => {
912 let branches = self.all_branches()?.map(|(br, idx)| {
913 if br == name { (br, snap_idx as u32) } else { (br, idx) }
914 });
915 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
916 Ok(Self::from_parts(
917 spec_version,
918 self.all_tags()?,
919 branches,
920 self.deleted_tags()?,
921 snaps,
922 &self.metadata()?,
923 UpdateInfo {
924 update_type: UpdateType::BranchResetUpdate {
925 name: name.to_string(),
926 previous_snap_id,
927 },
928 update_time: Utc::now(),
929 previous_updates: self.latest_updates()?,
930 },
931 Some(previous_file),
932 num_updates_per_file,
933 self.repo_before_updates()?,
934 self.config_bytes_raw()?.as_deref(),
935 self.enabled_feature_flags()?,
936 self.disabled_feature_flags()?,
937 &self.status()?,
938 )?)
939 }
940 None => Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
941 snapshot_id: new_snap.clone(),
942 })
943 .capture(),
944 }
945 }
946
947 pub fn add_tag(
948 &self,
949 spec_version: SpecVersionBin,
950 name: &str,
951 snap: &SnapshotId,
952 previous_file: &str,
953 num_updates_per_file: u16,
954 ) -> IcechunkResult<Self> {
955 if self.resolve_tag(name).is_ok() {
956 return Err(IcechunkFormatErrorKind::TagAlreadyExists {
957 tag: name.to_string(),
958 })
959 .capture();
960 }
961 if self.tag_was_deleted(name)? {
962 return Err(IcechunkFormatErrorKind::TagPreviouslyDeleted {
963 tag: name.to_string(),
964 })
965 .capture();
966 }
967
968 match self.resolve_snapshot_index(snap)? {
969 Some(snap_idx) => {
970 let mut tags: Vec<_> = self.all_tags()?.collect();
971 tags.push((name, snap_idx as u32));
972 tags.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
973 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
974 Ok(Self::from_parts(
975 spec_version,
976 tags,
977 self.all_branches()?,
978 self.deleted_tags()?,
979 snaps,
980 &self.metadata()?,
981 UpdateInfo {
982 update_type: UpdateType::TagCreatedUpdate {
983 name: name.to_string(),
984 },
985 update_time: Utc::now(),
986 previous_updates: self.latest_updates()?,
987 },
988 Some(previous_file),
989 num_updates_per_file,
990 self.repo_before_updates()?,
991 self.config_bytes_raw()?.as_deref(),
992 self.enabled_feature_flags()?,
993 self.disabled_feature_flags()?,
994 &self.status()?,
995 )?)
996 }
997 None => Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
998 snapshot_id: snap.clone(),
999 })
1000 .capture(),
1001 }
1002 }
1003
1004 pub fn delete_tag(
1005 &self,
1006 spec_version: SpecVersionBin,
1007 name: &str,
1008 previous_file: &str,
1009 num_updates_per_file: u16,
1010 ) -> IcechunkResult<Self> {
1011 match self.resolve_tag(name) {
1012 Ok(previous_snap_id) => {
1013 let mut tags: Vec<_> = self.all_tags()?.collect();
1014 tags.retain(|(n, _)| n != &name);
1016
1017 let mut deleted_tags: BTreeSet<_> = self.deleted_tags()?.collect();
1018 debug_assert!(!deleted_tags.contains(name));
1019 deleted_tags.insert(name);
1020
1021 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1022 Self::from_parts(
1023 spec_version,
1024 tags,
1025 self.all_branches()?,
1026 deleted_tags,
1027 snaps,
1028 &self.metadata()?,
1029 UpdateInfo {
1030 update_type: UpdateType::TagDeletedUpdate {
1031 name: name.to_string(),
1032 previous_snap_id,
1033 },
1034 update_time: Utc::now(),
1035 previous_updates: self.latest_updates()?,
1036 },
1037 Some(previous_file),
1038 num_updates_per_file,
1039 self.repo_before_updates()?,
1040 self.config_bytes_raw()?.as_deref(),
1041 self.enabled_feature_flags()?,
1042 self.disabled_feature_flags()?,
1043 &self.status()?,
1044 )
1045 }
1046 Err(IcechunkFormatError {
1047 kind: IcechunkFormatErrorKind::TagNotFound { .. },
1048 ..
1049 }) => Err(IcechunkFormatErrorKind::TagNotFound { tag: name.to_string() })
1050 .capture(),
1051 Err(err) => Err(err),
1052 }
1053 }
1054
1055 pub fn set_metadata(
1056 &self,
1057 spec_version: SpecVersionBin,
1058 metadata: &SnapshotProperties,
1059 previous_file: &str,
1060 num_updates_per_file: u16,
1061 ) -> IcechunkResult<Self> {
1062 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1063 Self::from_parts(
1064 spec_version,
1065 self.all_tags()?,
1066 self.all_branches()?,
1067 self.deleted_tags()?,
1068 snaps,
1069 metadata,
1070 UpdateInfo {
1071 update_type: UpdateType::MetadataChangedUpdate,
1072 update_time: Utc::now(),
1073 previous_updates: self.latest_updates()?,
1074 },
1075 Some(previous_file),
1076 num_updates_per_file,
1077 self.repo_before_updates()?,
1078 self.config_bytes_raw()?.as_deref(),
1079 self.enabled_feature_flags()?,
1080 self.disabled_feature_flags()?,
1081 &self.status()?,
1082 )
1083 }
1084
1085 pub fn set_config<C: Serialize>(
1087 &self,
1088 spec_version: SpecVersionBin,
1089 config: &C,
1090 previous_file: &str,
1091 num_updates_per_file: u16,
1092 ) -> IcechunkResult<Self> {
1093 let config_bytes = flexbuffers::to_vec(config).map_err(Box::new).capture()?;
1094 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1095 Self::from_parts(
1096 spec_version,
1097 self.all_tags()?,
1098 self.all_branches()?,
1099 self.deleted_tags()?,
1100 snaps,
1101 &self.metadata()?,
1102 UpdateInfo {
1103 update_type: UpdateType::ConfigChangedUpdate,
1104 update_time: Utc::now(),
1105 previous_updates: self.latest_updates()?,
1106 },
1107 Some(previous_file),
1108 num_updates_per_file,
1109 self.repo_before_updates()?,
1110 Some(config_bytes.as_slice()),
1111 self.enabled_feature_flags()?,
1112 self.disabled_feature_flags()?,
1113 &self.status()?,
1114 )
1115 }
1116
1117 pub fn set_status(
1118 &self,
1119 spec_version: SpecVersionBin,
1120 status: &RepoStatus,
1121 previous_file: &str,
1122 num_updates_per_file: u16,
1123 ) -> IcechunkResult<Self> {
1124 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1125 Self::from_parts(
1126 spec_version,
1127 self.all_tags()?,
1128 self.all_branches()?,
1129 self.deleted_tags()?,
1130 snaps,
1131 &self.metadata()?,
1132 UpdateInfo {
1133 update_type: UpdateType::RepoStatusChangedUpdate {
1134 status: status.clone(),
1135 },
1136 update_time: Utc::now(),
1137 previous_updates: self.latest_updates()?,
1138 },
1139 Some(previous_file),
1140 num_updates_per_file,
1141 self.repo_before_updates()?,
1142 self.config_bytes_raw()?.as_deref(),
1143 self.enabled_feature_flags()?,
1144 self.disabled_feature_flags()?,
1145 status,
1146 )
1147 }
1148
1149 pub fn from_buffer(buffer: Vec<u8>) -> IcechunkResult<RepoInfo> {
1150 let _ = flatbuffers::root_with_opts::<generated::Repo<'_>>(
1151 &ROOT_OPTIONS,
1152 buffer.as_slice(),
1153 )
1154 .capture()?;
1155 Ok(RepoInfo { buffer })
1156 }
1157
1158 pub fn bytes(&self) -> &[u8] {
1159 self.buffer.as_slice()
1160 }
1161
1162 fn root(&self) -> IcechunkResult<generated::Repo<'_>> {
1163 flatbuffers::root::<generated::Repo<'_>>(&self.buffer).capture()
1164 }
1165
1166 pub fn tag_names(&self) -> IcechunkResult<impl Iterator<Item = &str>> {
1167 Ok(self.root()?.tags().iter().map(|r| r.name()))
1168 }
1169
1170 pub fn branch_names(&self) -> IcechunkResult<impl Iterator<Item = &str>> {
1171 Ok(self.root()?.branches().iter().map(|r| r.name()))
1172 }
1173
1174 pub fn tags(&self) -> IcechunkResult<impl Iterator<Item = (&str, SnapshotId)>> {
1175 let root = self.root()?;
1176 Ok(self.all_tags()?.map(move |(name, idx)| {
1177 (name, SnapshotId::new(root.snapshots().get(idx as usize).id().0))
1178 }))
1179 }
1180
1181 pub fn branches(&self) -> IcechunkResult<impl Iterator<Item = (&str, SnapshotId)>> {
1182 let root = self.root()?;
1183 Ok(self.all_branches()?.map(move |(name, idx)| {
1184 (name, SnapshotId::new(root.snapshots().get(idx as usize).id().0))
1185 }))
1186 }
1187
1188 pub fn resolve_tag(&self, name: &str) -> IcechunkResult<SnapshotId> {
1189 let root = self.root()?;
1190 let res = root
1191 .tags()
1192 .lookup_by_key(name, |r, key| r.name().cmp(key))
1193 .map(|r| {
1194 let index = r.snapshot_index();
1195 SnapshotId::new(root.snapshots().get(index as usize).id().0)
1196 })
1197 .ok_or_else(|| IcechunkFormatErrorKind::TagNotFound { tag: name.to_string() })
1198 .capture()?;
1199
1200 Ok(res)
1201 }
1202
1203 pub fn tag_was_deleted(&self, name: &str) -> IcechunkResult<bool> {
1204 let root = self.root()?;
1205 let res = root.deleted_tags().lookup_by_key(name, |name, key| name.cmp(key));
1206 Ok(res.is_some())
1207 }
1208
1209 pub fn resolve_branch(&self, name: &str) -> IcechunkResult<SnapshotId> {
1210 let root = self.root()?;
1211 let res = root
1212 .branches()
1213 .lookup_by_key(name, |r, key| r.name().cmp(key))
1214 .map(|r| {
1215 let index = r.snapshot_index();
1216 SnapshotId::new(root.snapshots().get(index as usize).id().0)
1217 })
1218 .ok_or_else(|| IcechunkFormatErrorKind::BranchNotFound {
1219 branch: name.to_string(),
1220 })
1221 .capture()?;
1222
1223 Ok(res)
1224 }
1225
1226 pub fn spec_version(&self) -> IcechunkResult<SpecVersionBin> {
1227 let raw = self.root()?.spec_version();
1228 raw.try_into()
1229 .map_err(|_| IcechunkFormatErrorKind::InvalidSpecVersion {
1230 found: raw,
1231 max_supported: SpecVersionBin::current() as u8,
1232 })
1233 .capture()
1234 }
1235
1236 pub fn latest_updates(
1237 &self,
1238 ) -> IcechunkResult<impl Iterator<Item = UpdateTuple<'_>>> {
1239 let res = self.root()?.latest_updates().iter().map(|up| self.update_to_tuple(up));
1240 Ok(res)
1241 }
1242
1243 fn update_to_tuple<'a>(
1244 &'a self,
1245 update: generated::Update<'a>,
1246 ) -> IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)> {
1247 let ty = self.mk_update_type(&update)?;
1248 let ts = timestamp_to_timestamp(update.updated_at())?;
1249 let bp = update.backup_path();
1250 Ok((ty, ts, bp))
1251 }
1252
1253 fn mk_update_type(
1254 &self,
1255 update: &generated::Update<'_>,
1256 ) -> IcechunkResult<UpdateType> {
1257 #[expect(clippy::unwrap_used)]
1258 match update.update_type_type() {
1259 generated::UpdateType::RepoInitializedUpdate => {
1260 Ok(UpdateType::RepoInitializedUpdate)
1261 }
1262 generated::UpdateType::RepoMigratedUpdate => {
1263 let up = update.update_type_as_repo_migrated_update().unwrap();
1264 let from_raw = up.from_version();
1265 let to_raw = up.to_version();
1266 Ok(UpdateType::RepoMigratedUpdate {
1267 from_version: from_raw
1268 .try_into()
1269 .map_err(|_| IcechunkFormatErrorKind::InvalidSpecVersion {
1270 found: from_raw,
1271 max_supported: SpecVersionBin::current() as u8,
1272 })
1273 .capture()?,
1274 to_version: to_raw
1275 .try_into()
1276 .map_err(|_| IcechunkFormatErrorKind::InvalidSpecVersion {
1277 found: to_raw,
1278 max_supported: SpecVersionBin::current() as u8,
1279 })
1280 .capture()?,
1281 })
1282 }
1283 generated::UpdateType::RepoStatusChangedUpdate => {
1284 let up = update.update_type_as_repo_status_changed_update().unwrap();
1285 let fb_status = up.status().unwrap();
1286 let status = fb_status.try_into()?;
1287
1288 Ok(UpdateType::RepoStatusChangedUpdate { status })
1289 }
1290 generated::UpdateType::ConfigChangedUpdate => {
1291 Ok(UpdateType::ConfigChangedUpdate)
1292 }
1293 generated::UpdateType::MetadataChangedUpdate => {
1294 Ok(UpdateType::MetadataChangedUpdate)
1295 }
1296 generated::UpdateType::TagCreatedUpdate => {
1297 let up = update.update_type_as_tag_created_update().unwrap();
1298 Ok(UpdateType::TagCreatedUpdate { name: up.name().to_string() })
1299 }
1300 generated::UpdateType::TagDeletedUpdate => {
1301 let up = update.update_type_as_tag_deleted_update().unwrap();
1302 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1303 Ok(UpdateType::TagDeletedUpdate {
1304 name: up.name().to_string(),
1305 previous_snap_id,
1306 })
1307 }
1308 generated::UpdateType::BranchCreatedUpdate => {
1309 let up = update.update_type_as_branch_created_update().unwrap();
1310 Ok(UpdateType::BranchCreatedUpdate { name: up.name().to_string() })
1311 }
1312 generated::UpdateType::BranchDeletedUpdate => {
1313 let up = update.update_type_as_branch_deleted_update().unwrap();
1314 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1315 Ok(UpdateType::BranchDeletedUpdate {
1316 name: up.name().to_string(),
1317 previous_snap_id,
1318 })
1319 }
1320 generated::UpdateType::BranchResetUpdate => {
1321 let up = update.update_type_as_branch_reset_update().unwrap();
1322 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1323 Ok(UpdateType::BranchResetUpdate {
1324 name: up.name().to_string(),
1325 previous_snap_id,
1326 })
1327 }
1328 generated::UpdateType::NewCommitUpdate => {
1329 let up = update.update_type_as_new_commit_update().unwrap();
1330 let new_snap_id = SnapshotId::new(up.new_snap_id().0);
1331 Ok(UpdateType::NewCommitUpdate {
1332 branch: up.branch().to_string(),
1333 new_snap_id,
1334 })
1335 }
1336 generated::UpdateType::CommitAmendedUpdate => {
1337 let up = update.update_type_as_commit_amended_update().unwrap();
1338 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1339 let new_snap_id = SnapshotId::new(up.new_snap_id().0);
1340 Ok(UpdateType::CommitAmendedUpdate {
1341 branch: up.branch().to_string(),
1342 previous_snap_id,
1343 new_snap_id,
1344 })
1345 }
1346 generated::UpdateType::NewDetachedSnapshotUpdate => {
1347 let up = update.update_type_as_new_detached_snapshot_update().unwrap();
1348 let new_snap_id = SnapshotId::new(up.new_snap_id().0);
1349 Ok(UpdateType::NewDetachedSnapshotUpdate { new_snap_id })
1350 }
1351 generated::UpdateType::GCRanUpdate => Ok(UpdateType::GCRanUpdate),
1352 generated::UpdateType::ExpirationRanUpdate => {
1353 Ok(UpdateType::ExpirationRanUpdate)
1354 }
1355 generated::UpdateType::FeatureFlagChangedUpdate => {
1356 let up = update.update_type_as_feature_flag_changed_update().unwrap();
1357 Ok(UpdateType::FeatureFlagChanged {
1358 id: up.id(),
1359 new_value: if up.is_set() { Some(up.new_value()) } else { None },
1360 })
1361 }
1362 _ => Err(IcechunkFormatErrorKind::InvalidFlatBuffer(
1363 flatbuffers::InvalidFlatbuffer::InconsistentUnion {
1364 field: Cow::Borrowed("latest_update_type"),
1365 field_type: Cow::Borrowed("UpdateType"),
1366 error_trace: Default::default(),
1367 },
1368 ))
1369 .capture(),
1370 }
1371 }
1372
1373 pub fn ancestry<'a>(
1374 &'a self,
1375 snapshot: &SnapshotId,
1376 ) -> IcechunkResult<impl Iterator<Item = IcechunkResult<SnapshotInfo>> + Send + use<'a>>
1377 {
1378 let root = self.root()?;
1379 if let Some(start) = self.resolve_snapshot_index(snapshot)? {
1380 let mut index = Some(start as i32);
1381 let iter = std::iter::from_fn(move || {
1382 if let Some(ix) = index {
1383 if ix >= 0 {
1384 let snap = root.snapshots().get(ix as usize);
1385 index = Some(snap.parent_offset());
1386 Some(mk_snapshot_info(&root, &snap))
1387 } else {
1388 index = None;
1389 None
1390 }
1391 } else {
1392 None
1393 }
1394 });
1395 Ok(iter)
1396 } else {
1397 Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
1398 snapshot_id: snapshot.clone(),
1399 })
1400 .capture()
1401 }
1402 }
1403
1404 pub fn find_snapshot(&self, id: &SnapshotId) -> IcechunkResult<SnapshotInfo> {
1405 let mut anc = self.ancestry(id)?;
1406 #[expect(clippy::panic)]
1407 match anc.next() {
1408 Some(snap) => snap,
1409 None => panic!("Ancestry head snapshot not found"),
1412 }
1413 }
1414
1415 pub fn repo_before_updates(&self) -> IcechunkResult<Option<&str>> {
1416 Ok(self.root()?.repo_before_updates())
1417 }
1418
1419 fn resolve_snapshot_index(&self, id: &SnapshotId) -> IcechunkResult<Option<usize>> {
1420 Ok(lookup_index_by_key(self.root()?.snapshots(), &id.0, |snap, key| {
1421 snap.id().0.cmp(key)
1422 }))
1423 }
1424}
1425
1426fn resolve_ref_iter<'a>(
1427 sorted_snapshots: &[SnapshotInfo],
1428 it: impl IntoIterator<Item = (&'a str, SnapshotId)>,
1429) -> IcechunkResult<Vec<(&'a str, u32)>> {
1430 let mut res: Vec<_> = it
1431 .into_iter()
1432 .map(|(name, id)| {
1433 let idx = sorted_snapshots
1434 .binary_search_by_key(&&id.0, |snap| &snap.id.0)
1435 .map_err(|_| IcechunkFormatErrorKind::SnapshotIdNotFound {
1436 snapshot_id: id.clone(),
1437 })
1438 .capture()? as u32;
1439 Ok::<_, IcechunkFormatError>((name, idx))
1440 })
1441 .try_collect()?;
1442 res.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
1443 Ok(res)
1444}
1445
1446fn timestamp_to_timestamp(ts: u64) -> IcechunkResult<DateTime<Utc>> {
1447 let ts: i64 =
1448 ts.try_into().map_err(|_| IcechunkFormatErrorKind::InvalidTimestamp).capture()?;
1449 DateTime::from_timestamp_micros(ts)
1450 .ok_or(IcechunkFormatErrorKind::InvalidTimestamp)
1451 .capture()
1452}
1453
1454fn mk_snapshot_info(
1455 repo: &generated::Repo<'_>,
1456 snap: &generated::SnapshotInfo<'_>,
1457) -> IcechunkResult<SnapshotInfo> {
1458 let flushed_at = timestamp_to_timestamp(snap.flushed_at())?;
1459 let parent_id = if snap.parent_offset() >= 0 {
1460 let parent = repo.snapshots().get(snap.parent_offset() as usize).id();
1461 Some(parent)
1462 } else {
1463 None
1464 };
1465 let metadata = snap
1466 .metadata()
1467 .map(|items| {
1468 let items = items
1469 .iter()
1470 .map(|item| {
1471 let name = item.name().to_string();
1472 let value = flexbuffers::from_slice(item.value().bytes())
1473 .map_err(Box::new)
1474 .capture()?;
1475 Ok::<_, IcechunkFormatError>((name, value))
1476 })
1477 .try_collect()?;
1478 Ok::<_, IcechunkFormatError>(items)
1479 })
1480 .transpose()?
1481 .unwrap_or_default();
1482
1483 Ok(SnapshotInfo {
1484 id: SnapshotId::new(snap.id().0),
1485 flushed_at,
1486 message: snap.message().to_string(),
1487 metadata,
1488 parent_id: parent_id.map(|buf| SnapshotId::new(buf.0)),
1489 })
1490}
1491
1492fn update_type_to_fb<'bldr>(
1493 builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
1494 update: &UpdateType,
1495) -> IcechunkResult<(generated::UpdateType, WIPOffset<UnionWIPOffset>)> {
1496 match update {
1497 UpdateType::RepoInitializedUpdate => Ok((
1498 generated::UpdateType::RepoInitializedUpdate,
1499 generated::RepoInitializedUpdate::create(
1500 builder,
1501 &generated::RepoInitializedUpdateArgs {},
1502 )
1503 .as_union_value(),
1504 )),
1505 UpdateType::RepoMigratedUpdate { from_version, to_version } => Ok((
1506 generated::UpdateType::RepoMigratedUpdate,
1507 generated::RepoMigratedUpdate::create(
1508 builder,
1509 &generated::RepoMigratedUpdateArgs {
1510 from_version: *from_version as u8,
1511 to_version: *to_version as u8,
1512 },
1513 )
1514 .as_union_value(),
1515 )),
1516 UpdateType::RepoStatusChangedUpdate { status } => {
1517 let limited_availability_reason = status
1518 .limited_availability_reason
1519 .as_ref()
1520 .map(|r| builder.create_string(r));
1521 let status = generated::RepoStatus::create(
1522 builder,
1523 &generated::RepoStatusArgs {
1524 availability: status.availability.into(),
1525 set_at: status.set_at.timestamp_micros() as u64,
1526 limited_availability_reason,
1527 },
1528 );
1529
1530 Ok((
1531 generated::UpdateType::RepoStatusChangedUpdate,
1532 generated::RepoStatusChangedUpdate::create(
1533 builder,
1534 &generated::RepoStatusChangedUpdateArgs { status: Some(status) },
1535 )
1536 .as_union_value(),
1537 ))
1538 }
1539 UpdateType::ConfigChangedUpdate => Ok((
1540 generated::UpdateType::ConfigChangedUpdate,
1541 generated::ConfigChangedUpdate::create(
1542 builder,
1543 &generated::ConfigChangedUpdateArgs {},
1544 )
1545 .as_union_value(),
1546 )),
1547 UpdateType::MetadataChangedUpdate => Ok((
1548 generated::UpdateType::MetadataChangedUpdate,
1549 generated::MetadataChangedUpdate::create(
1550 builder,
1551 &generated::MetadataChangedUpdateArgs {},
1552 )
1553 .as_union_value(),
1554 )),
1555 UpdateType::TagCreatedUpdate { name } => {
1556 let name = Some(builder.create_string(name));
1557 Ok((
1558 generated::UpdateType::TagCreatedUpdate,
1559 generated::TagCreatedUpdate::create(
1560 builder,
1561 &generated::TagCreatedUpdateArgs { name },
1562 )
1563 .as_union_value(),
1564 ))
1565 }
1566 UpdateType::TagDeletedUpdate { name, previous_snap_id } => {
1567 let name = Some(builder.create_string(name));
1568 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1569 let previous_snap_id = Some(&object_id12);
1570 Ok((
1571 generated::UpdateType::TagDeletedUpdate,
1572 generated::TagDeletedUpdate::create(
1573 builder,
1574 &generated::TagDeletedUpdateArgs { name, previous_snap_id },
1575 )
1576 .as_union_value(),
1577 ))
1578 }
1579 UpdateType::BranchCreatedUpdate { name } => {
1580 let name = Some(builder.create_string(name));
1581 Ok((
1582 generated::UpdateType::BranchCreatedUpdate,
1583 generated::BranchCreatedUpdate::create(
1584 builder,
1585 &generated::BranchCreatedUpdateArgs { name },
1586 )
1587 .as_union_value(),
1588 ))
1589 }
1590 UpdateType::BranchDeletedUpdate { name, previous_snap_id } => {
1591 let name = Some(builder.create_string(name));
1592 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1593 let previous_snap_id = Some(&object_id12);
1594 Ok((
1595 generated::UpdateType::BranchDeletedUpdate,
1596 generated::BranchDeletedUpdate::create(
1597 builder,
1598 &generated::BranchDeletedUpdateArgs { name, previous_snap_id },
1599 )
1600 .as_union_value(),
1601 ))
1602 }
1603 UpdateType::BranchResetUpdate { name, previous_snap_id } => {
1604 let name = Some(builder.create_string(name));
1605 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1606 let previous_snap_id = Some(&object_id12);
1607 Ok((
1608 generated::UpdateType::BranchResetUpdate,
1609 generated::BranchResetUpdate::create(
1610 builder,
1611 &generated::BranchResetUpdateArgs { name, previous_snap_id },
1612 )
1613 .as_union_value(),
1614 ))
1615 }
1616 UpdateType::NewCommitUpdate { branch, new_snap_id } => {
1617 let branch = Some(builder.create_string(branch));
1618 let object_id12 = generated::ObjectId12::new(&new_snap_id.0);
1619 let new_snap_id = Some(&object_id12);
1620 Ok((
1621 generated::UpdateType::NewCommitUpdate,
1622 generated::NewCommitUpdate::create(
1623 builder,
1624 &generated::NewCommitUpdateArgs { branch, new_snap_id },
1625 )
1626 .as_union_value(),
1627 ))
1628 }
1629 UpdateType::CommitAmendedUpdate { branch, previous_snap_id, new_snap_id } => {
1630 let branch = Some(builder.create_string(branch));
1631 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1632 let previous_snap_id = Some(&object_id12);
1633 let object_id12 = generated::ObjectId12::new(&new_snap_id.0);
1634 let new_snap_id = Some(&object_id12);
1635 Ok((
1636 generated::UpdateType::CommitAmendedUpdate,
1637 generated::CommitAmendedUpdate::create(
1638 builder,
1639 &generated::CommitAmendedUpdateArgs {
1640 branch,
1641 previous_snap_id,
1642 new_snap_id,
1643 },
1644 )
1645 .as_union_value(),
1646 ))
1647 }
1648 UpdateType::NewDetachedSnapshotUpdate { new_snap_id } => {
1649 let object_id12 = generated::ObjectId12::new(&new_snap_id.0);
1650 let new_snap_id = Some(&object_id12);
1651 Ok((
1652 generated::UpdateType::NewDetachedSnapshotUpdate,
1653 generated::NewDetachedSnapshotUpdate::create(
1654 builder,
1655 &generated::NewDetachedSnapshotUpdateArgs { new_snap_id },
1656 )
1657 .as_union_value(),
1658 ))
1659 }
1660 UpdateType::GCRanUpdate => Ok((
1661 generated::UpdateType::GCRanUpdate,
1662 generated::GCRanUpdate::create(builder, &generated::GCRanUpdateArgs {})
1663 .as_union_value(),
1664 )),
1665 UpdateType::ExpirationRanUpdate => Ok((
1666 generated::UpdateType::ExpirationRanUpdate,
1667 generated::ExpirationRanUpdate::create(
1668 builder,
1669 &generated::ExpirationRanUpdateArgs {},
1670 )
1671 .as_union_value(),
1672 )),
1673 UpdateType::FeatureFlagChanged { id, new_value } => Ok((
1674 generated::UpdateType::FeatureFlagChangedUpdate,
1675 generated::FeatureFlagChangedUpdate::create(
1676 builder,
1677 &generated::FeatureFlagChangedUpdateArgs {
1678 id: *id,
1679 new_value: new_value.unwrap_or_default(),
1680 is_set: new_value.is_some(),
1681 },
1682 )
1683 .as_union_value(),
1684 )),
1685 }
1686}
1687
1688#[cfg(test)]
1689mod tests {
1690
1691 use super::*;
1692 use crate::roundtrip_serialization_tests;
1693 use proptest::prelude::*;
1694 use std::collections::HashSet;
1695
1696 fn potentially_invalid_repo_info() -> impl Strategy<Value = RepoInfo> {
1698 any::<Vec<u8>>().prop_map(|buffer| RepoInfo { buffer })
1699 }
1700
1701 roundtrip_serialization_tests!(
1702 serialize_and_deserialize_repo_info - potentially_invalid_repo_info
1703 );
1704
1705 #[test]
1706 fn test_add_snapshot() -> Result<(), Box<dyn std::error::Error>> {
1707 let id1 = SnapshotId::random();
1708 let snap1 = SnapshotInfo {
1709 id: id1.clone(),
1710 parent_id: None,
1711 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
1713 message: "snap 1".to_string(),
1714 metadata: Default::default(),
1715 };
1716 let repo = RepoInfo::initial(
1717 SpecVersionBin::current(),
1718 snap1.clone(),
1719 100,
1720 None::<&()>,
1721 None,
1722 );
1723 assert_eq!(repo.all_snapshots()?.next().unwrap().unwrap(), snap1);
1724
1725 let id2 = SnapshotId::random();
1726 let snap2 = SnapshotInfo {
1727 id: id2.clone(),
1728 parent_id: Some(id1.clone()),
1729 flushed_at: DateTime::from_timestamp_micros(2_000_000).unwrap(),
1730 message: "snap 2".to_string(),
1731 ..snap1.clone()
1732 };
1733 let repo = repo.add_snapshot(
1734 SpecVersionBin::current(),
1735 snap2.clone(),
1736 Some("main"),
1737 UpdateType::NewCommitUpdate {
1738 branch: "main".to_string(),
1739 new_snap_id: snap2.id.clone(),
1740 },
1741 None,
1742 "foo/bar",
1743 100,
1744 )?;
1745 assert_eq!(&repo.resolve_branch("main")?, &snap2.id);
1746 assert_eq!(repo.repo_before_updates()?, None);
1747
1748 let all: HashSet<_> = repo.all_snapshots()?.try_collect()?;
1749 assert_eq!(all, HashSet::from_iter([snap1.clone(), snap2.clone()]));
1750
1751 let anc: Vec<_> = repo.ancestry(&id1)?.try_collect()?;
1752 assert_eq!(anc, std::slice::from_ref(&snap1));
1753
1754 let anc: Vec<_> = repo.ancestry(&id2)?.try_collect()?;
1755 assert_eq!(anc, [snap2.clone(), snap1.clone()]);
1756
1757 assert!(repo.ancestry(&SnapshotId::random()).is_err());
1758
1759 let id3 = SnapshotId::random();
1760 let snap3 = SnapshotInfo {
1761 id: id3.clone(),
1762 parent_id: Some(id2.clone()),
1763 flushed_at: DateTime::from_timestamp_micros(3_000_000).unwrap(),
1764 message: "snap 3".to_string(),
1765 ..snap2.clone()
1766 };
1767 let repo = repo.add_snapshot(
1768 SpecVersionBin::current(),
1769 snap3.clone(),
1770 Some("main"),
1771 UpdateType::NewCommitUpdate {
1772 branch: "main".to_string(),
1773 new_snap_id: snap3.id.clone(),
1774 },
1775 None,
1776 "foo",
1777 100,
1778 )?;
1779 assert_eq!(&repo.resolve_branch("main")?, &snap3.id);
1780 let all: HashSet<_> = repo.all_snapshots()?.try_collect()?;
1781 assert_eq!(
1782 all,
1783 HashSet::from_iter([snap1.clone(), snap2.clone(), snap3.clone()])
1784 );
1785
1786 let all: HashSet<_> = repo.all_snapshots()?.try_collect()?;
1787 assert_eq!(
1788 all,
1789 HashSet::from_iter([snap1.clone(), snap2.clone(), snap3.clone()])
1790 );
1791
1792 let anc: Vec<_> = repo.ancestry(&id3)?.try_collect()?;
1793 assert_eq!(anc, [snap3.clone(), snap2.clone(), snap1.clone()]);
1794 Ok(())
1795 }
1796
1797 #[test]
1798 fn test_tags_and_branches() -> Result<(), Box<dyn std::error::Error>> {
1799 let id1 = SnapshotId::random();
1800 let snap1 = SnapshotInfo {
1801 id: id1.clone(),
1802 parent_id: None,
1803 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
1805 message: "snap 1".to_string(),
1806 metadata: Default::default(),
1807 };
1808 let repo = RepoInfo::initial(
1809 SpecVersionBin::current(),
1810 snap1.clone(),
1811 100,
1812 None::<&()>,
1813 None,
1814 );
1815 let repo = repo.add_branch(SpecVersionBin::current(), "foo", &id1, "foo", 100)?;
1816 let repo = repo.add_branch(SpecVersionBin::current(), "bar", &id1, "bar", 100)?;
1817 assert!(matches!(
1818 repo.add_branch(
1819 SpecVersionBin::current(),
1820 "bad-snap",
1821 &SnapshotId::random(),
1822 "bad",
1823 100
1824 ),
1825 Err(IcechunkFormatError {
1826 kind: IcechunkFormatErrorKind::SnapshotIdNotFound { .. },
1827 ..
1828 })
1829 ));
1830 assert!(matches!(
1832 repo.add_branch(SpecVersionBin::current(), "bar", &id1, "/foo/bar", 100),
1833 Err(IcechunkFormatError {
1834 kind: IcechunkFormatErrorKind::BranchAlreadyExists { .. },
1835 ..
1836 })
1837 ));
1838
1839 assert_eq!(
1840 repo.all_branches()?.collect::<HashSet<_>>(),
1841 [("main", 0), ("foo", 0), ("bar", 0)].into()
1842 );
1843
1844 let id2 = SnapshotId::random();
1845 let snap2 = SnapshotInfo {
1846 id: id2.clone(),
1847 parent_id: Some(id1.clone()),
1848 flushed_at: Utc::now(),
1849 message: "snap 2".to_string(),
1850 ..snap1.clone()
1851 };
1852 let repo = repo.add_snapshot(
1853 SpecVersionBin::current(),
1854 snap2.clone(),
1855 Some("main"),
1856 UpdateType::NewCommitUpdate {
1857 branch: "main".to_string(),
1858 new_snap_id: snap2.id.clone(),
1859 },
1860 None,
1861 "foo",
1862 100,
1863 )?;
1864 let repo =
1865 repo.add_branch(SpecVersionBin::current(), "baz", &id2, "/foo/bar", 100)?;
1866 assert_eq!(repo.resolve_branch("main")?, id2.clone());
1867 assert_eq!(repo.resolve_branch("foo")?, id1.clone());
1868 assert_eq!(repo.resolve_branch("bar")?, id1.clone());
1869 assert_eq!(repo.resolve_branch("baz")?, id2.clone());
1870
1871 let repo = repo.delete_branch(SpecVersionBin::current(), "bar", "bar", 100)?;
1872 assert!(repo.resolve_branch("bar").is_err());
1873 assert_eq!(
1874 repo.all_branches()?.map(|(n, _)| n).collect::<HashSet<_>>(),
1875 ["main", "foo", "baz"].into()
1876 );
1877
1878 assert!(
1879 repo.delete_branch(SpecVersionBin::current(), "bad-branch", "bad", 100)
1880 .is_err()
1881 );
1882
1883 let repo = repo.add_tag(SpecVersionBin::current(), "tag1", &id1, "tag1", 100)?;
1885 let repo = repo.add_tag(SpecVersionBin::current(), "tag2", &id2, "tag2", 100)?;
1886 assert!(
1887 repo.add_tag(
1888 SpecVersionBin::current(),
1889 "bad-snap",
1890 &SnapshotId::random(),
1891 "bad",
1892 100
1893 )
1894 .is_err()
1895 );
1896 assert!(
1897 repo.add_tag(SpecVersionBin::current(), "tag1", &id1, "tag1-again", 100)
1898 .is_err()
1899 );
1900 assert_eq!(repo.resolve_tag("tag1")?, id1.clone());
1901 assert_eq!(repo.resolve_tag("tag2")?, id2.clone());
1902 assert_eq!(
1903 repo.all_tags()?.map(|(n, _)| n).collect::<HashSet<_>>(),
1904 ["tag1", "tag2"].into()
1905 );
1906
1907 let repo = repo.add_tag(SpecVersionBin::current(), "tag3", &id1, "tag3", 100)?;
1909 let repo =
1910 repo.delete_tag(SpecVersionBin::current(), "tag3", "delete-tag3", 100)?;
1911 assert_eq!(
1912 repo.all_tags()?.map(|(n, _)| n).collect::<HashSet<_>>(),
1913 ["tag1", "tag2"].into()
1914 );
1915 assert!(
1917 repo.add_tag(SpecVersionBin::current(), "tag3", &id1, "tag3-again", 100)
1918 .is_err()
1919 );
1920 assert!(
1922 repo.delete_tag(SpecVersionBin::current(), "tag3", "delete-tag3-again", 100)
1923 .is_err()
1924 );
1925 assert_eq!(
1926 repo.all_tags()?.map(|(n, _)| n).collect::<HashSet<_>>(),
1927 ["tag1", "tag2"].into()
1928 );
1929 Ok(())
1930 }
1931
1932 #[test]
1933 fn test_repo_info_updates() -> Result<(), Box<dyn std::error::Error>> {
1934 let id1 = SnapshotId::random();
1935 let snap1 = SnapshotInfo {
1936 id: id1.clone(),
1937 parent_id: None,
1938 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
1940 message: "snap 1".to_string(),
1941 metadata: Default::default(),
1942 };
1943
1944 let num_updates_per_file: u16 = 10;
1945 let n = num_updates_per_file as usize;
1946
1947 let mut repo = RepoInfo::initial(
1949 SpecVersionBin::current(),
1950 snap1,
1951 num_updates_per_file,
1952 None::<&()>,
1953 None,
1954 );
1955 assert_eq!(repo.latest_updates()?.count(), 1);
1956 let (last_update, _, file) = repo.latest_updates()?.next().unwrap()?;
1957 assert!(file.is_none());
1958 assert_eq!(last_update, UpdateType::RepoInitializedUpdate);
1959 assert!(repo.repo_before_updates()?.is_none());
1960 for i in 1..=(n - 1) {
1963 repo = repo.add_branch(
1964 SpecVersionBin::current(),
1965 i.to_string().as_str(),
1966 &id1,
1967 (i - 1).to_string().as_str(),
1968 num_updates_per_file,
1969 )?;
1970 }
1971
1972 assert_eq!(repo.latest_updates()?.count(), n);
1973 let updates = repo.latest_updates()?;
1974
1975 for (idx, update) in updates.enumerate() {
1977 let (update, _, file) = update?;
1978 if idx == n - 1 {
1979 assert_eq!(update, UpdateType::RepoInitializedUpdate);
1980 assert_eq!(file, Some("0"));
1981 } else {
1982 assert_eq!(
1983 update,
1984 UpdateType::BranchCreatedUpdate { name: (n - 1 - idx).to_string() }
1985 );
1986 if idx == 0 {
1987 assert!(file.is_none());
1988 } else {
1989 assert_eq!(file, Some((n - 1 - idx).to_string().as_str()));
1990 }
1991 }
1992 }
1993 assert!(repo.repo_before_updates()?.is_none());
1994
1995 repo = repo.add_tag(
1997 SpecVersionBin::current(),
1998 "tag",
1999 &id1,
2000 "first-branches",
2001 num_updates_per_file,
2002 )?;
2003 assert_eq!(repo.latest_updates()?.count(), n);
2005 assert_eq!(repo.repo_before_updates()?, Some("0"));
2007 let mut updates = repo.latest_updates()?;
2008 let (last_update, _, file) = updates.next().unwrap()?;
2010 assert_eq!(last_update, UpdateType::TagCreatedUpdate { name: "tag".to_string() });
2011 assert!(file.is_none());
2012
2013 let (last_update, _, file) = updates.next().unwrap()?;
2015 assert_eq!(
2016 last_update,
2017 UpdateType::BranchCreatedUpdate { name: (n - 1).to_string() }
2018 );
2019 assert_eq!(file, Some("first-branches"));
2020
2021 for (idx, update) in updates.enumerate() {
2023 let (update, _, file) = update?;
2024 assert_eq!(file, Some((n - 2 - idx).to_string().as_str()));
2025 assert_eq!(
2026 update,
2027 UpdateType::BranchCreatedUpdate { name: (n - 2 - idx).to_string() }
2028 );
2029 }
2030
2031 Ok(())
2032 }
2033
2034 #[test]
2035 fn test_update_timestamp_ordering_rejected() -> Result<(), Box<dyn std::error::Error>>
2036 {
2037 let flushed_at = DateTime::from_timestamp_micros(1_000_000).unwrap();
2038 let id1 = SnapshotId::random();
2039 let snap1 = SnapshotInfo {
2040 id: id1.clone(),
2041 parent_id: None,
2042 flushed_at,
2043 message: "snap 1".to_string(),
2044 metadata: Default::default(),
2045 };
2046 let repo = RepoInfo::initial(
2047 SpecVersionBin::current(),
2048 snap1,
2049 100,
2050 None::<&()>,
2051 Some(flushed_at),
2052 );
2053
2054 let id2 = SnapshotId::random();
2057 let snap2 = SnapshotInfo {
2058 id: id2.clone(),
2059 parent_id: Some(id1.clone()),
2060 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
2061 message: "snap 2".to_string(),
2062 metadata: Default::default(),
2063 };
2064 let result = repo.add_snapshot(
2065 SpecVersionBin::current(),
2066 snap2,
2067 Some("main"),
2068 UpdateType::NewCommitUpdate {
2069 branch: "main".to_string(),
2070 new_snap_id: id2.clone(),
2071 },
2072 Some(flushed_at),
2073 "backup",
2074 100,
2075 );
2076 assert!(matches!(
2077 result,
2078 Err(IcechunkFormatError {
2079 kind: IcechunkFormatErrorKind::InvalidUpdateTimestamp { .. },
2080 ..
2081 })
2082 ));
2083
2084 let flushed_at = DateTime::from_timestamp_micros(500_000).unwrap();
2087 let id3 = SnapshotId::random();
2088 let snap3 = SnapshotInfo {
2089 id: id3.clone(),
2090 parent_id: Some(id1.clone()),
2091 flushed_at,
2092 message: "snap 3".to_string(),
2093 metadata: Default::default(),
2094 };
2095 let result = repo.add_snapshot(
2096 SpecVersionBin::current(),
2097 snap3,
2098 Some("main"),
2099 UpdateType::NewCommitUpdate {
2100 branch: "main".to_string(),
2101 new_snap_id: id3.clone(),
2102 },
2103 Some(flushed_at),
2104 "backup",
2105 100,
2106 );
2107 assert!(matches!(
2108 result,
2109 Err(IcechunkFormatError {
2110 kind: IcechunkFormatErrorKind::InvalidUpdateTimestamp { .. },
2111 ..
2112 })
2113 ));
2114
2115 let flushed_at = DateTime::from_timestamp_micros(2_000_000).unwrap();
2117 let id4 = SnapshotId::random();
2118 let snap4 = SnapshotInfo {
2119 id: id4.clone(),
2120 parent_id: Some(id1.clone()),
2121 flushed_at,
2122 message: "snap 4".to_string(),
2123 metadata: Default::default(),
2124 };
2125 let result = repo.add_snapshot(
2126 SpecVersionBin::current(),
2127 snap4,
2128 Some("main"),
2129 UpdateType::NewCommitUpdate {
2130 branch: "main".to_string(),
2131 new_snap_id: id4.clone(),
2132 },
2133 Some(flushed_at),
2134 "backup",
2135 100,
2136 );
2137 assert!(result.is_ok());
2138
2139 Ok(())
2140 }
2141}