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)]
90#[serde(try_from = "RepoInfoDeserializer")]
91pub struct RepoInfo {
92 buffer: Vec<u8>,
93}
94
95#[derive(Deserialize)]
98struct RepoInfoDeserializer {
99 buffer: Vec<u8>,
100}
101
102impl TryFrom<RepoInfoDeserializer> for RepoInfo {
103 type Error = IcechunkFormatError;
104
105 fn try_from(value: RepoInfoDeserializer) -> Result<Self, Self::Error> {
106 RepoInfo::from_buffer(value.buffer)
107 }
108}
109
110impl std::fmt::Debug for RepoInfo {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 let tags = self.tags().map(Vec::from_iter).unwrap_or_default();
113 let tags =
114 tags.into_iter().map(|(name, snap)| format!("{name} -> {snap}")).join(", ");
115 let branches = self.branches().map(Vec::from_iter).unwrap_or_default();
116 let branches = branches
117 .into_iter()
118 .map(|(name, snap)| format!("{name} -> {snap}"))
119 .join(", ");
120 let snaps =
121 self.snapshot_and_parent_ids().map(Vec::from_iter).unwrap_or_default();
122 let snaps = snaps
123 .into_iter()
124 .map(|(id, parent_id)| {
125 format!(
126 "{} -> {}",
127 id,
128 parent_id.map(|s| s.to_string()).unwrap_or_default()
129 )
130 })
131 .join(", ");
132 f.debug_struct("RepoInfo")
134 .field("tags", &tags)
135 .field("branches", &branches)
136 .field("snapshots", &snaps)
137 .finish_non_exhaustive()
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum UpdateType {
143 RepoInitializedUpdate,
144 RepoMigratedUpdate {
145 from_version: SpecVersionBin,
146 to_version: SpecVersionBin,
147 },
148 RepoStatusChangedUpdate {
149 status: RepoStatus,
150 },
151 ConfigChangedUpdate,
152 MetadataChangedUpdate,
153 TagCreatedUpdate {
154 name: String,
155 },
156 TagDeletedUpdate {
157 name: String,
158 previous_snap_id: SnapshotId,
159 },
160 BranchCreatedUpdate {
161 name: String,
162 },
163 BranchDeletedUpdate {
164 name: String,
165 previous_snap_id: SnapshotId,
166 },
167 BranchResetUpdate {
168 name: String,
169 previous_snap_id: SnapshotId,
170 },
171 NewCommitUpdate {
172 branch: String,
173 new_snap_id: SnapshotId,
174 },
175 CommitAmendedUpdate {
176 branch: String,
177 previous_snap_id: SnapshotId,
178 new_snap_id: SnapshotId,
179 },
180 NewDetachedSnapshotUpdate {
181 new_snap_id: SnapshotId,
182 },
183 GCRanUpdate,
184 ExpirationRanUpdate,
185 FeatureFlagChanged {
186 id: u16,
187 new_value: Option<bool>,
188 },
189}
190
191static ROOT_OPTIONS: VerifierOptions = VerifierOptions {
192 max_depth: 10,
193 max_tables: 50_000_000,
194 max_apparent_size: crate::serializers::MAX_APPARENT_SIZE_INFLATION
200 .saturating_mul(flatbuffers::FLATBUFFERS_MAX_BUFFER_SIZE),
201 ignore_missing_null_terminator: true,
202};
203
204#[cfg(test)]
206pub(crate) fn root_options() -> &'static VerifierOptions {
207 &ROOT_OPTIONS
208}
209
210#[derive(Debug, Clone)]
211pub struct UpdateInfo<I> {
212 pub update_type: UpdateType,
213 pub update_time: DateTime<Utc>,
214 pub previous_updates: I,
215}
216
217pub(crate) type UpdateTuple<'a> =
218 IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)>;
219
220type LatestUpdatesResult<'bldr> = IcechunkResult<(
221 WIPOffset<
222 flatbuffers::Vector<
223 'bldr,
224 flatbuffers::ForwardsUOffset<generated::Update<'bldr>>,
225 >,
226 >,
227 Option<WIPOffset<&'bldr str>>,
228)>;
229
230impl RepoInfo {
231 #[expect(clippy::too_many_arguments)]
232 pub fn new<
233 'a,
234 I: IntoIterator<Item = IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)>>,
235 EFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
236 DFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
237 >(
238 spec_version: SpecVersionBin,
239 tags: impl IntoIterator<Item = (&'a str, SnapshotId)>,
240 branches: impl IntoIterator<Item = (&'a str, SnapshotId)>,
241 deleted_tags: impl IntoIterator<Item = &'a str>,
242 snapshots: impl IntoIterator<Item = SnapshotInfo>,
243 metadata: &SnapshotProperties,
244 update: UpdateInfo<I>,
245 backup_path: Option<&'a str>,
246 num_updates_per_file: u16,
247 previous_info: Option<&'a str>,
248 config_bytes: Option<&[u8]>,
249 sorted_enabled_feature_flags: Option<EFFIt>,
250 sorted_disabled_feature_flags: Option<DFFIt>,
251 status: &RepoStatus,
252 ) -> IcechunkResult<Self> {
253 let mut snapshots: Vec<_> = snapshots.into_iter().collect();
254 snapshots.sort_by_key(|a| a.id.0);
255 let tags = resolve_ref_iter(&snapshots, tags)?;
256 let branches = resolve_ref_iter(&snapshots, branches)?;
257 let mut deleted_tags: Vec<_> = deleted_tags.into_iter().collect();
258 deleted_tags.sort();
259 Self::from_parts(
260 spec_version,
261 tags,
262 branches,
263 deleted_tags,
264 snapshots,
265 metadata,
266 |builder| {
267 Self::mk_latest_updates(
268 builder,
269 update,
270 backup_path,
271 num_updates_per_file,
272 previous_info,
273 )
274 },
275 config_bytes,
276 sorted_enabled_feature_flags,
277 sorted_disabled_feature_flags,
278 status,
279 )
280 }
281
282 #[expect(clippy::too_many_arguments)]
283 fn from_parts<
284 'a,
285 F: for<'bldr> FnOnce(
286 &mut flatbuffers::FlatBufferBuilder<'bldr>,
287 ) -> LatestUpdatesResult<'bldr>,
288 EFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
289 DFFIt: DoubleEndedIterator<Item = u16> + ExactSizeIterator,
290 >(
291 spec_version: SpecVersionBin,
292 sorted_tags: impl IntoIterator<Item = (&'a str, u32)>,
293 sorted_branches: impl IntoIterator<Item = (&'a str, u32)>,
294 sorted_deleted_tags: impl IntoIterator<Item = &'a str>,
295 sorted_snapshots: impl IntoIterator<Item = SnapshotInfo>,
296 metadata: &SnapshotProperties,
297 build_updates: F,
298 config_bytes: Option<&[u8]>,
299 sorted_enabled_feature_flags: Option<EFFIt>,
300 sorted_disabled_feature_flags: Option<DFFIt>,
301 status: &RepoStatus,
302 ) -> IcechunkResult<Self> {
303 trace!("Creating new repo info from parts");
304 let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(4_096);
305 let tags = sorted_tags
306 .into_iter()
307 .map(|(name, offset)| {
308 let args = generated::RefArgs {
309 name: Some(builder.create_string(name)),
310 snapshot_index: offset,
311 };
312 generated::Ref::create(&mut builder, &args)
313 })
314 .collect::<Vec<_>>();
315 let tags = builder.create_vector(&tags);
316
317 let mut main_found = false;
318 let branches = sorted_branches
319 .into_iter()
320 .map(|(name, offset)| {
321 if name == icechunk_types::DEFAULT_BRANCH {
322 main_found = true;
323 }
324 let args = generated::RefArgs {
325 name: Some(builder.create_string(name)),
326 snapshot_index: offset,
327 };
328 generated::Ref::create(&mut builder, &args)
329 })
330 .collect::<Vec<_>>();
331 if !main_found {
332 return Err(IcechunkFormatErrorKind::BranchNotFound {
333 branch: icechunk_types::DEFAULT_BRANCH.to_string(),
334 })
335 .capture();
336 }
337
338 let branches = builder.create_vector(&branches);
339
340 let deleted_tags = sorted_deleted_tags
341 .into_iter()
342 .map(|name| builder.create_string(name))
343 .collect::<Vec<_>>();
344 let deleted_tags = builder.create_vector(&deleted_tags);
345
346 let snapshots: Vec<_> = sorted_snapshots.into_iter().collect();
347 debug_assert!(snapshots.is_sorted_by(|a, b| a.id.0 <= b.id.0));
348
349 let snapshot_index: HashMap<_, _> =
350 snapshots.iter().enumerate().map(|(ix, sn)| (sn.id.clone(), ix)).collect();
351
352 let snapshots: Vec<_> = snapshots
354 .iter()
355 .map(|snap| {
356 let id = &snap.id.0;
357 let id = generated::ObjectId12::new(id);
358 let parent_offset = match snap.parent_id.as_ref() {
359 Some(parent_id) => {
360 let index = snapshot_index
361 .get(parent_id)
362 .ok_or_else(|| IcechunkFormatErrorKind::SnapshotIdNotFound {
363 snapshot_id: snap.id.clone(),
364 })
365 .capture()?;
366 Ok(*index as i32)
367 }
368 None => Ok::<_, IcechunkFormatError>(-1),
369 }?;
370
371 let metadata_items: Vec<_> = snap
372 .metadata
373 .iter()
374 .map(|(k, v)| {
375 let name = builder.create_shared_string(k.as_str());
376 let serialized =
377 flexbuffers::to_vec(v).map_err(Box::new).capture()?;
378 let value = builder.create_vector(serialized.as_slice());
379 let item = generated::MetadataItem::create(
380 &mut builder,
381 &generated::MetadataItemArgs {
382 name: Some(name),
383 value: Some(value),
384 },
385 );
386 Ok::<_, IcechunkFormatError>(item)
387 })
388 .try_collect()?;
389
390 let metadata = builder.create_vector(metadata_items.as_slice());
391
392 let pruned_ancestor_tx_logs = if snap.pruned_ancestor_tx_logs.is_empty() {
395 None
396 } else {
397 let ids: Vec<_> = snap
398 .pruned_ancestor_tx_logs
399 .iter()
400 .map(|id| generated::ObjectId12::new(&id.0))
401 .collect();
402 Some(builder.create_vector(ids.as_slice()))
403 };
404 debug_assert_eq!(
405 pruned_ancestor_tx_logs.is_none(),
406 snap.pruned_ancestor_tx_logs.is_empty(),
407 "empty pruned_ancestor_tx_logs must serialize as a null vector, \
408 not a present empty one"
409 );
410
411 let args = generated::SnapshotInfoArgs {
412 id: Some(&id),
413 parent_offset,
414 flushed_at: snap.flushed_at.timestamp_micros() as u64,
415 message: Some(builder.create_string(snap.message.as_str())),
416 metadata: Some(metadata),
417 pruned_ancestor_tx_logs,
418 };
419 Ok::<_, IcechunkFormatError>(generated::SnapshotInfo::create(
420 &mut builder,
421 &args,
422 ))
423 })
424 .try_collect()?;
425 let snapshots = builder.create_vector(&snapshots);
426
427 let limited_reason = status
428 .limited_availability_reason
429 .as_deref()
430 .map(|s| builder.create_string(s));
431 let status = generated::RepoStatus::create(
432 &mut builder,
433 &generated::RepoStatusArgs {
434 availability: status.availability.into(),
435 set_at: status.set_at.timestamp_micros() as u64,
436 limited_availability_reason: limited_reason,
437 },
438 );
439
440 let metadata_items: Vec<_> = metadata
441 .iter()
442 .map(|(k, v)| {
443 let name = builder.create_shared_string(k.as_str());
444 let serialized = flexbuffers::to_vec(v).map_err(Box::new).capture()?;
445 let value = builder.create_vector(serialized.as_slice());
446 let item = generated::MetadataItem::create(
447 &mut builder,
448 &generated::MetadataItemArgs { name: Some(name), value: Some(value) },
449 );
450 Ok::<_, IcechunkFormatError>(item)
451 })
452 .try_collect()?;
453
454 let metadata = builder.create_vector(metadata_items.as_slice());
455
456 let enabled_feature_flags =
457 sorted_enabled_feature_flags.map(|it| builder.create_vector_from_iter(it));
458 let disabled_feature_flags =
459 sorted_disabled_feature_flags.map(|it| builder.create_vector_from_iter(it));
460
461 let (latest_updates, repo_before_updates) = build_updates(&mut builder)?;
462
463 let config = config_bytes.map(|bytes| builder.create_vector(bytes));
464
465 let repo_args = generated::RepoArgs {
467 tags: Some(tags),
468 branches: Some(branches),
469 deleted_tags: Some(deleted_tags),
470 snapshots: Some(snapshots),
471 spec_version: spec_version as u8,
472 status: Some(status),
473 metadata: Some(metadata),
474 latest_updates: Some(latest_updates),
475 repo_before_updates,
476 config,
477 enabled_feature_flags,
478 disabled_feature_flags,
479 ..Default::default()
480 };
481 let repo = generated::Repo::create(&mut builder, &repo_args);
482 builder.finish(repo, Some("Ichk"));
483 let (mut buffer, offset) = builder.collapse();
484 buffer.drain(0..offset);
485 buffer.shrink_to_fit();
486 Ok(Self { buffer })
487 }
488
489 fn mk_latest_updates<
490 'bldr,
491 'a,
492 I: IntoIterator<Item = IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)>>,
493 >(
494 builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
495 update: UpdateInfo<I>,
496 backup_path: Option<&'a str>,
497 num_updates_per_file: u16,
498 previous_info: Option<&'a str>,
499 ) -> LatestUpdatesResult<'bldr> {
500 let mut previous_updates = update.previous_updates.into_iter();
502 let last_update = previous_updates.next().map(|maybe_data| {
503 maybe_data.map(|(ut, dt, path)| {
504 assert!(
505 path.is_none(),
506 "Invalid latest update iterator, last element has backup path"
507 );
508 (ut, dt, backup_path)
509 })
510 });
511 assert!(
518 backup_path.is_none() || last_update.is_some(),
519 "A backup path must not be provided without previous updates"
520 );
521
522 if let Some(Ok((_, latest_time, _))) = &last_update
524 && update.update_time <= *latest_time
525 {
526 return Err(IcechunkFormatErrorKind::InvalidUpdateTimestamp {
527 latest_time: *latest_time,
528 new_time: update.update_time,
529 })
530 .capture();
531 }
532
533 let new_updates: Box<dyn Iterator<Item = _>> =
534 if let Some(last_update) = last_update {
535 Box::new(
536 [Ok((update.update_type, update.update_time, None)), last_update]
537 .into_iter(),
538 )
539 } else {
540 Box::new([Ok((update.update_type, update.update_time, None))].into_iter())
541 };
542
543 let all_updates = new_updates.into_iter().chain(previous_updates);
544 let mut repo_before_updates = previous_info;
547
548 let num_updates = num_updates_per_file as usize;
549 let mut updates = Vec::new();
550 for maybe_data in all_updates {
551 let (u_type, u_time, file) = maybe_data?;
552
553 if updates.len() >= num_updates
558 && let Some(bp) = file
559 {
560 repo_before_updates = Some(bp);
561 break;
562 }
563
564 let (update_type_type, update_type) = update_type_to_fb(builder, &u_type)?;
565 let file = file.map(|file| builder.create_string(file));
566 updates.push(generated::Update::create(
567 builder,
568 &generated::UpdateArgs {
569 update_type_type,
570 update_type: Some(update_type),
571 updated_at: u_time.timestamp_micros() as u64,
572 backup_path: file,
573 },
574 ));
575 }
576
577 debug_assert!(!updates.is_empty(), "Must have at least one update in repo file");
578
579 let updates = builder.create_vector(&updates);
580 let repo_before_updates = repo_before_updates.map(|s| builder.create_string(s));
581 Ok((updates, repo_before_updates))
582 }
583
584 fn mk_latest_updates_passthrough<'bldr, 'a>(
585 builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
586 updates: impl IntoIterator<Item = UpdateTuple<'a>>,
587 repo_before_updates: Option<&'a str>,
588 ) -> LatestUpdatesResult<'bldr> {
589 let mut fb_updates = Vec::new();
590 for maybe_data in updates {
591 let (u_type, u_time, file) = maybe_data?;
592 let (update_type_type, update_type) = update_type_to_fb(builder, &u_type)?;
593 let file = file.map(|file| builder.create_string(file));
594 fb_updates.push(generated::Update::create(
595 builder,
596 &generated::UpdateArgs {
597 update_type_type,
598 update_type: Some(update_type),
599 updated_at: u_time.timestamp_micros() as u64,
600 backup_path: file,
601 },
602 ));
603 }
604 debug_assert!(
605 !fb_updates.is_empty(),
606 "Must have at least one update in repo file"
607 );
608 let updates = builder.create_vector(&fb_updates);
609 let repo_before_updates = repo_before_updates.map(|s| builder.create_string(s));
610 Ok((updates, repo_before_updates))
611 }
612
613 pub fn initial<C: Serialize>(
614 spec_version: SpecVersionBin,
615 snapshot: SnapshotInfo,
616 num_updates_per_file: u16,
617 config: Option<&C>,
618 update_time: Option<DateTime<Utc>>,
619 ) -> Self {
620 #[expect(clippy::expect_used)]
621 let config_bytes =
622 config.map(|c| flexbuffers::to_vec(c).expect("Cannot serialize config"));
623 #[expect(clippy::expect_used)]
625 Self::from_parts(
626 spec_version,
627 [],
628 [("main", 0)],
629 [],
630 [snapshot],
631 &Default::default(),
632 |builder| {
633 Self::mk_latest_updates(
634 builder,
635 UpdateInfo {
636 update_type: UpdateType::RepoInitializedUpdate,
637 update_time: update_time.unwrap_or_else(Utc::now),
638 previous_updates: [],
639 },
640 None,
641 num_updates_per_file,
642 None,
643 )
644 },
645 config_bytes.as_deref(),
646 None::<std::iter::Empty<u16>>,
647 None::<std::iter::Empty<u16>>,
648 &RepoStatus {
649 availability: RepoAvailability::Online,
650 set_at: update_time.unwrap_or_else(Utc::now),
651 limited_availability_reason: None,
652 },
653 )
654 .expect("Cannot generate initial snapshot")
655 }
656
657 pub fn config_bytes_raw(&self) -> IcechunkResult<Option<Vec<u8>>> {
659 Ok(self.root()?.config().map(|v| v.bytes().to_vec()))
660 }
661
662 pub fn config<C: serde::de::DeserializeOwned>(&self) -> IcechunkResult<Option<C>> {
666 match self.root()?.config() {
667 None => Ok(None),
668 Some(config_fb) => {
669 let config: C = flexbuffers::from_slice(config_fb.bytes())
670 .map_err(Box::new)
671 .capture()?;
672 Ok(Some(config))
673 }
674 }
675 }
676
677 pub fn metadata(&self) -> IcechunkResult<SnapshotProperties> {
678 self.root()?
679 .metadata()
680 .unwrap_or_default()
681 .iter()
682 .map(|item| {
683 let key = item.name().to_string();
684 let value = flexbuffers::from_slice(item.value().bytes())
685 .map_err(Box::new)
686 .capture()?;
687 Ok((key, value))
688 })
689 .try_collect()
690 }
691
692 pub fn status(&self) -> IcechunkResult<RepoStatus> {
693 let root = self.root()?;
694 let fb_status = root.status();
695 fb_status.try_into()
696 }
697
698 pub fn enabled_feature_flags(
699 &self,
700 ) -> IcechunkResult<Option<impl DoubleEndedIterator<Item = u16> + ExactSizeIterator>>
701 {
702 Ok(self.root()?.enabled_feature_flags().map(|v| v.iter()))
703 }
704
705 pub fn disabled_feature_flags(
706 &self,
707 ) -> IcechunkResult<Option<impl DoubleEndedIterator<Item = u16> + ExactSizeIterator>>
708 {
709 Ok(self.root()?.disabled_feature_flags().map(|v| v.iter()))
710 }
711
712 pub fn feature_flag_enabled(&self, id: u16) -> IcechunkResult<Option<bool>> {
716 let root = self.root()?;
717 if root
718 .enabled_feature_flags()
719 .and_then(|v| v.lookup_by_key(id, |this, key| this.cmp(key)))
720 .is_some()
721 {
722 return Ok(Some(true));
723 }
724 if root
725 .disabled_feature_flags()
726 .and_then(|v| v.lookup_by_key(id, |this, key| this.cmp(key)))
727 .is_some()
728 {
729 return Ok(Some(false));
730 }
731 Ok(None)
732 }
733
734 fn all_tags(&self) -> IcechunkResult<impl Iterator<Item = (&str, u32)>> {
735 Ok(self.root()?.tags().iter().map(|r| (r.name(), r.snapshot_index())))
736 }
737
738 fn all_branches(&self) -> IcechunkResult<impl Iterator<Item = (&str, u32)>> {
739 Ok(self.root()?.branches().iter().map(|r| (r.name(), r.snapshot_index())))
740 }
741
742 pub fn deleted_tags(&self) -> IcechunkResult<impl Iterator<Item = &str>> {
743 Ok(self.root()?.deleted_tags().iter())
744 }
745
746 pub fn all_snapshots(
747 &self,
748 ) -> IcechunkResult<impl Iterator<Item = IcechunkResult<SnapshotInfo>>> {
749 let root = self.root()?;
750 Ok(root.snapshots().iter().map(move |snap| mk_snapshot_info(&root, &snap)))
751 }
752
753 fn snapshot_and_parent_ids(
755 &self,
756 ) -> IcechunkResult<impl Iterator<Item = (SnapshotId, Option<SnapshotId>)>> {
757 let root = self.root()?;
758 Ok(root.snapshots().iter().map(move |snap| {
759 let parent_id = if snap.parent_offset() >= 0 {
760 let parent = root.snapshots().get(snap.parent_offset() as usize).id();
761 Some(SnapshotId::new(parent.0))
762 } else {
763 None
764 };
765 (SnapshotId::new(snap.id().0), parent_id)
766 }))
767 }
768
769 pub fn update_feature_flag(
771 &self,
772 spec_version: SpecVersionBin,
773 flag_id: u16,
774 enabled: Option<bool>,
775 previous_file: &str,
776 num_updates_per_file: u16,
777 ) -> IcechunkResult<Self> {
778 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
779 let (eff, dff): (Option<Vec<_>>, Option<Vec<_>>) = match enabled {
780 Some(false) => {
781 let e = self
782 .enabled_feature_flags()?
783 .map(|it| it.filter(|x| *x != flag_id).collect());
784
785 let mut d: BTreeSet<_> = self
786 .disabled_feature_flags()?
787 .map(|it| it.collect())
788 .unwrap_or_default();
789 d.insert(flag_id);
790 let d = d.into_iter().collect();
791 (e, Some(d))
792 }
793 Some(true) => {
794 let d = self
795 .disabled_feature_flags()?
796 .map(|it| it.filter(|x| *x != flag_id).collect());
797
798 let mut e: BTreeSet<_> = self
799 .enabled_feature_flags()?
800 .map(|it| it.collect())
801 .unwrap_or_default();
802 e.insert(flag_id);
803 let e = e.into_iter().collect();
804 (Some(e), d)
805 }
806 None => {
807 let e = self
808 .enabled_feature_flags()?
809 .map(|it| it.filter(|x| *x != flag_id).collect());
810 let d = self
811 .disabled_feature_flags()?
812 .map(|it| it.filter(|x| *x != flag_id).collect());
813 (e, d)
814 }
815 };
816
817 let latest_updates = self.latest_updates()?;
818 let rbu = self.repo_before_updates()?;
819 Self::from_parts(
820 spec_version,
821 self.all_tags()?,
822 self.all_branches()?,
823 self.deleted_tags()?,
824 snaps,
825 &self.metadata()?,
826 |builder| {
827 Self::mk_latest_updates(
828 builder,
829 UpdateInfo {
830 update_type: UpdateType::FeatureFlagChanged {
831 id: flag_id,
832 new_value: enabled,
833 },
834 update_time: Utc::now(),
835 previous_updates: latest_updates,
836 },
837 Some(previous_file),
838 num_updates_per_file,
839 rbu,
840 )
841 },
842 self.config_bytes_raw()?.as_deref(),
843 eff.map(|it| it.into_iter()),
844 dff.map(|it| it.into_iter()),
845 &self.status()?,
846 )
847 }
848
849 #[expect(clippy::too_many_arguments)]
850 pub fn add_snapshot(
851 &self,
852 spec_version: SpecVersionBin,
853 snap: SnapshotInfo,
854 branch: Option<&str>,
855 update_type: UpdateType,
856 update_time: Option<DateTime<Utc>>, previous_file: &str,
858 num_updates_per_file: u16,
859 ) -> IcechunkResult<Self> {
860 let mut snapshots: Vec<_> = self.all_snapshots()?.try_collect()?;
861 let new_index = match snapshots.binary_search_by_key(&&snap.id, |snap| &snap.id) {
862 Ok(_) => Err(IcechunkFormatErrorKind::DuplicateSnapshotId {
863 snapshot_id: snap.id.clone(),
864 })
865 .capture(),
866 Err(idx) => Ok(idx),
867 }?;
868
869 snapshots.insert(new_index, snap);
870
871 let tags = self.all_tags()?.map(|(name, idx)| {
872 if idx as usize >= new_index { (name, idx + 1) } else { (name, idx) }
873 });
874 let branches = self.all_branches()?.map(|(name, idx)| {
875 if Some(name) == branch {
876 (name, new_index as u32)
877 } else if idx as usize >= new_index {
878 (name, idx + 1)
879 } else {
880 (name, idx)
881 }
882 });
883
884 let latest_updates = self.latest_updates()?;
885 let rbu = self.repo_before_updates()?;
886 let res = Self::from_parts(
887 spec_version,
888 tags,
889 branches,
890 self.deleted_tags()?,
891 snapshots,
892 &self.metadata()?,
893 |builder| {
894 Self::mk_latest_updates(
895 builder,
896 UpdateInfo {
897 update_type,
898 update_time: update_time.unwrap_or_else(Utc::now),
899 previous_updates: latest_updates,
900 },
901 Some(previous_file),
902 num_updates_per_file,
903 rbu,
904 )
905 },
906 self.config_bytes_raw()?.as_deref(),
907 self.enabled_feature_flags()?,
908 self.disabled_feature_flags()?,
909 &self.status()?,
910 )?;
911 Ok(res)
912 }
913
914 pub fn add_branch(
915 &self,
916 spec_version: SpecVersionBin,
917 name: &str,
918 snap: &SnapshotId,
919 previous_file: &str,
920 num_updates_per_file: u16,
921 ) -> IcechunkResult<Self> {
922 if let Ok(snapshot_id) = self.resolve_branch(name) {
923 return Err(IcechunkFormatErrorKind::BranchAlreadyExists {
924 branch: name.to_string(),
925 snapshot_id,
926 })
927 .capture();
928 }
929
930 match self.resolve_snapshot_index(snap)? {
931 Some(snap_idx) => {
932 let mut branches: Vec<_> = self.all_branches()?.collect();
933 branches.push((name, snap_idx as u32));
934 branches.sort_by_key(|(name1, _)| *name1);
935 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
936 let latest_updates = self.latest_updates()?;
937 let rbu = self.repo_before_updates()?;
938 Ok(Self::from_parts(
939 spec_version,
940 self.all_tags()?,
941 branches,
942 self.deleted_tags()?,
943 snaps,
944 &self.metadata()?,
945 |builder| {
946 Self::mk_latest_updates(
947 builder,
948 UpdateInfo {
949 update_type: UpdateType::BranchCreatedUpdate {
950 name: name.to_string(),
951 },
952 update_time: Utc::now(),
953 previous_updates: latest_updates,
954 },
955 Some(previous_file),
956 num_updates_per_file,
957 rbu,
958 )
959 },
960 self.config_bytes_raw()?.as_deref(),
961 self.enabled_feature_flags()?,
962 self.disabled_feature_flags()?,
963 &self.status()?,
964 )?)
965 }
966 None => Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
967 snapshot_id: snap.clone(),
968 })
969 .capture(),
970 }
971 }
972
973 pub fn delete_branch(
974 &self,
975 spec_version: SpecVersionBin,
976 name: &str,
977 previous_file: &str,
978 num_updates_per_file: u16,
979 ) -> IcechunkResult<Self> {
980 match self.resolve_branch(name) {
981 Ok(previous_snap_id) => {
982 let mut branches: Vec<_> = self.all_branches()?.collect();
983 branches.retain(|(n, _)| n != &name);
985 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
986 let latest_updates = self.latest_updates()?;
987 let rbu = self.repo_before_updates()?;
988 Self::from_parts(
989 spec_version,
990 self.all_tags()?,
991 branches,
992 self.deleted_tags()?,
993 snaps,
994 &self.metadata()?,
995 |builder| {
996 Self::mk_latest_updates(
997 builder,
998 UpdateInfo {
999 update_type: UpdateType::BranchDeletedUpdate {
1000 name: name.to_string(),
1001 previous_snap_id,
1002 },
1003 update_time: Utc::now(),
1004 previous_updates: latest_updates,
1005 },
1006 Some(previous_file),
1007 num_updates_per_file,
1008 rbu,
1009 )
1010 },
1011 self.config_bytes_raw()?.as_deref(),
1012 self.enabled_feature_flags()?,
1013 self.disabled_feature_flags()?,
1014 &self.status()?,
1015 )
1016 }
1017 Err(IcechunkFormatError {
1018 kind: IcechunkFormatErrorKind::BranchNotFound { .. },
1019 ..
1020 }) => {
1021 Err(IcechunkFormatErrorKind::BranchNotFound { branch: name.to_string() })
1022 .capture()
1023 }
1024 Err(err) => Err(err),
1025 }
1026 }
1027
1028 pub fn update_branch(
1029 &self,
1030 spec_version: SpecVersionBin,
1031 name: &str,
1032 new_snap: &SnapshotId,
1033 previous_file: &str,
1034 num_updates_per_file: u16,
1035 ) -> IcechunkResult<Self> {
1036 let previous_snap_id = self.resolve_branch(name)?;
1037 match self.resolve_snapshot_index(new_snap)? {
1038 Some(snap_idx) => {
1039 let branches = self.all_branches()?.map(|(br, idx)| {
1040 if br == name { (br, snap_idx as u32) } else { (br, idx) }
1041 });
1042 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1043 let latest_updates = self.latest_updates()?;
1044 let rbu = self.repo_before_updates()?;
1045 Ok(Self::from_parts(
1046 spec_version,
1047 self.all_tags()?,
1048 branches,
1049 self.deleted_tags()?,
1050 snaps,
1051 &self.metadata()?,
1052 |builder| {
1053 Self::mk_latest_updates(
1054 builder,
1055 UpdateInfo {
1056 update_type: UpdateType::BranchResetUpdate {
1057 name: name.to_string(),
1058 previous_snap_id,
1059 },
1060 update_time: Utc::now(),
1061 previous_updates: latest_updates,
1062 },
1063 Some(previous_file),
1064 num_updates_per_file,
1065 rbu,
1066 )
1067 },
1068 self.config_bytes_raw()?.as_deref(),
1069 self.enabled_feature_flags()?,
1070 self.disabled_feature_flags()?,
1071 &self.status()?,
1072 )?)
1073 }
1074 None => Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
1075 snapshot_id: new_snap.clone(),
1076 })
1077 .capture(),
1078 }
1079 }
1080
1081 pub fn add_tag(
1082 &self,
1083 spec_version: SpecVersionBin,
1084 name: &str,
1085 snap: &SnapshotId,
1086 previous_file: &str,
1087 num_updates_per_file: u16,
1088 ) -> IcechunkResult<Self> {
1089 if self.resolve_tag(name).is_ok() {
1090 return Err(IcechunkFormatErrorKind::TagAlreadyExists {
1091 tag: name.to_string(),
1092 })
1093 .capture();
1094 }
1095 if self.tag_was_deleted(name)? {
1096 return Err(IcechunkFormatErrorKind::TagPreviouslyDeleted {
1097 tag: name.to_string(),
1098 })
1099 .capture();
1100 }
1101
1102 match self.resolve_snapshot_index(snap)? {
1103 Some(snap_idx) => {
1104 let mut tags: Vec<_> = self.all_tags()?.collect();
1105 tags.push((name, snap_idx as u32));
1106 tags.sort_by_key(|(name1, _)| *name1);
1107 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1108 let latest_updates = self.latest_updates()?;
1109 let rbu = self.repo_before_updates()?;
1110 Ok(Self::from_parts(
1111 spec_version,
1112 tags,
1113 self.all_branches()?,
1114 self.deleted_tags()?,
1115 snaps,
1116 &self.metadata()?,
1117 |builder| {
1118 Self::mk_latest_updates(
1119 builder,
1120 UpdateInfo {
1121 update_type: UpdateType::TagCreatedUpdate {
1122 name: name.to_string(),
1123 },
1124 update_time: Utc::now(),
1125 previous_updates: latest_updates,
1126 },
1127 Some(previous_file),
1128 num_updates_per_file,
1129 rbu,
1130 )
1131 },
1132 self.config_bytes_raw()?.as_deref(),
1133 self.enabled_feature_flags()?,
1134 self.disabled_feature_flags()?,
1135 &self.status()?,
1136 )?)
1137 }
1138 None => Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
1139 snapshot_id: snap.clone(),
1140 })
1141 .capture(),
1142 }
1143 }
1144
1145 pub fn delete_tag(
1146 &self,
1147 spec_version: SpecVersionBin,
1148 name: &str,
1149 previous_file: &str,
1150 num_updates_per_file: u16,
1151 ) -> IcechunkResult<Self> {
1152 match self.resolve_tag(name) {
1153 Ok(previous_snap_id) => {
1154 let mut tags: Vec<_> = self.all_tags()?.collect();
1155 tags.retain(|(n, _)| n != &name);
1157
1158 let mut deleted_tags: BTreeSet<_> = self.deleted_tags()?.collect();
1159 debug_assert!(!deleted_tags.contains(name));
1160 deleted_tags.insert(name);
1161
1162 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1163 let latest_updates = self.latest_updates()?;
1164 let rbu = self.repo_before_updates()?;
1165 Self::from_parts(
1166 spec_version,
1167 tags,
1168 self.all_branches()?,
1169 deleted_tags,
1170 snaps,
1171 &self.metadata()?,
1172 |builder| {
1173 Self::mk_latest_updates(
1174 builder,
1175 UpdateInfo {
1176 update_type: UpdateType::TagDeletedUpdate {
1177 name: name.to_string(),
1178 previous_snap_id,
1179 },
1180 update_time: Utc::now(),
1181 previous_updates: latest_updates,
1182 },
1183 Some(previous_file),
1184 num_updates_per_file,
1185 rbu,
1186 )
1187 },
1188 self.config_bytes_raw()?.as_deref(),
1189 self.enabled_feature_flags()?,
1190 self.disabled_feature_flags()?,
1191 &self.status()?,
1192 )
1193 }
1194 Err(IcechunkFormatError {
1195 kind: IcechunkFormatErrorKind::TagNotFound { .. },
1196 ..
1197 }) => Err(IcechunkFormatErrorKind::TagNotFound { tag: name.to_string() })
1198 .capture(),
1199 Err(err) => Err(err),
1200 }
1201 }
1202
1203 pub fn set_metadata(
1204 &self,
1205 spec_version: SpecVersionBin,
1206 metadata: &SnapshotProperties,
1207 previous_file: &str,
1208 num_updates_per_file: u16,
1209 ) -> IcechunkResult<Self> {
1210 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1211 let latest_updates = self.latest_updates()?;
1212 let rbu = self.repo_before_updates()?;
1213 Self::from_parts(
1214 spec_version,
1215 self.all_tags()?,
1216 self.all_branches()?,
1217 self.deleted_tags()?,
1218 snaps,
1219 metadata,
1220 |builder| {
1221 Self::mk_latest_updates(
1222 builder,
1223 UpdateInfo {
1224 update_type: UpdateType::MetadataChangedUpdate,
1225 update_time: Utc::now(),
1226 previous_updates: latest_updates,
1227 },
1228 Some(previous_file),
1229 num_updates_per_file,
1230 rbu,
1231 )
1232 },
1233 self.config_bytes_raw()?.as_deref(),
1234 self.enabled_feature_flags()?,
1235 self.disabled_feature_flags()?,
1236 &self.status()?,
1237 )
1238 }
1239
1240 pub fn set_config<C: Serialize>(
1242 &self,
1243 spec_version: SpecVersionBin,
1244 config: &C,
1245 previous_file: &str,
1246 num_updates_per_file: u16,
1247 ) -> IcechunkResult<Self> {
1248 let config_bytes = flexbuffers::to_vec(config).map_err(Box::new).capture()?;
1249 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1250 let latest_updates = self.latest_updates()?;
1251 let rbu = self.repo_before_updates()?;
1252 Self::from_parts(
1253 spec_version,
1254 self.all_tags()?,
1255 self.all_branches()?,
1256 self.deleted_tags()?,
1257 snaps,
1258 &self.metadata()?,
1259 |builder| {
1260 Self::mk_latest_updates(
1261 builder,
1262 UpdateInfo {
1263 update_type: UpdateType::ConfigChangedUpdate,
1264 update_time: Utc::now(),
1265 previous_updates: latest_updates,
1266 },
1267 Some(previous_file),
1268 num_updates_per_file,
1269 rbu,
1270 )
1271 },
1272 Some(config_bytes.as_slice()),
1273 self.enabled_feature_flags()?,
1274 self.disabled_feature_flags()?,
1275 &self.status()?,
1276 )
1277 }
1278
1279 pub fn set_status(
1287 &self,
1288 spec_version: SpecVersionBin,
1289 status: &RepoStatus,
1290 update_config: Option<(&str, u16)>,
1291 ) -> IcechunkResult<Self> {
1292 let snaps: Vec<_> = self.all_snapshots()?.try_collect()?;
1293 let latest_updates = self.latest_updates()?;
1294 let rbu = self.repo_before_updates()?;
1295 if let Some((previous_file, num_updates_per_file)) = update_config {
1296 let status_update = status.clone();
1297 Self::from_parts(
1298 spec_version,
1299 self.all_tags()?,
1300 self.all_branches()?,
1301 self.deleted_tags()?,
1302 snaps,
1303 &self.metadata()?,
1304 |builder| {
1305 Self::mk_latest_updates(
1306 builder,
1307 UpdateInfo {
1308 update_type: UpdateType::RepoStatusChangedUpdate {
1309 status: status_update,
1310 },
1311 update_time: Utc::now(),
1312 previous_updates: latest_updates,
1313 },
1314 Some(previous_file),
1315 num_updates_per_file,
1316 rbu,
1317 )
1318 },
1319 self.config_bytes_raw()?.as_deref(),
1320 self.enabled_feature_flags()?,
1321 self.disabled_feature_flags()?,
1322 status,
1323 )
1324 } else {
1325 Self::from_parts(
1326 spec_version,
1327 self.all_tags()?,
1328 self.all_branches()?,
1329 self.deleted_tags()?,
1330 snaps,
1331 &self.metadata()?,
1332 |builder| {
1333 Self::mk_latest_updates_passthrough(builder, latest_updates, rbu)
1334 },
1335 self.config_bytes_raw()?.as_deref(),
1336 self.enabled_feature_flags()?,
1337 self.disabled_feature_flags()?,
1338 status,
1339 )
1340 }
1341 }
1342
1343 pub fn verify_buffer(buffer: &[u8]) -> IcechunkResult<()> {
1345 let _ = flatbuffers::root_with_opts::<generated::Repo<'_>>(&ROOT_OPTIONS, buffer)
1346 .capture()?;
1347 Ok(())
1348 }
1349
1350 pub fn from_buffer(buffer: Vec<u8>) -> IcechunkResult<RepoInfo> {
1351 Self::verify_buffer(buffer.as_slice())?;
1352 Ok(RepoInfo { buffer })
1353 }
1354
1355 pub fn bytes(&self) -> &[u8] {
1356 self.buffer.as_slice()
1357 }
1358
1359 #[expect(unsafe_code)]
1360 fn root(&self) -> IcechunkResult<generated::Repo<'_>> {
1361 Ok(unsafe { flatbuffers::root_unchecked::<generated::Repo<'_>>(&self.buffer) })
1366 }
1367
1368 pub fn tag_names(&self) -> IcechunkResult<impl Iterator<Item = &str>> {
1369 Ok(self.root()?.tags().iter().map(|r| r.name()))
1370 }
1371
1372 pub fn branch_names(&self) -> IcechunkResult<impl Iterator<Item = &str>> {
1373 Ok(self.root()?.branches().iter().map(|r| r.name()))
1374 }
1375
1376 pub fn tags(&self) -> IcechunkResult<impl Iterator<Item = (&str, SnapshotId)>> {
1377 let root = self.root()?;
1378 Ok(self.all_tags()?.map(move |(name, idx)| {
1379 (name, SnapshotId::new(root.snapshots().get(idx as usize).id().0))
1380 }))
1381 }
1382
1383 pub fn branches(&self) -> IcechunkResult<impl Iterator<Item = (&str, SnapshotId)>> {
1384 let root = self.root()?;
1385 Ok(self.all_branches()?.map(move |(name, idx)| {
1386 (name, SnapshotId::new(root.snapshots().get(idx as usize).id().0))
1387 }))
1388 }
1389
1390 pub fn resolve_tag(&self, name: &str) -> IcechunkResult<SnapshotId> {
1391 let root = self.root()?;
1392 let res = root
1393 .tags()
1394 .lookup_by_key(name, |r, key| r.name().cmp(key))
1395 .map(|r| {
1396 let index = r.snapshot_index();
1397 SnapshotId::new(root.snapshots().get(index as usize).id().0)
1398 })
1399 .ok_or_else(|| IcechunkFormatErrorKind::TagNotFound { tag: name.to_string() })
1400 .capture()?;
1401
1402 Ok(res)
1403 }
1404
1405 pub fn tag_was_deleted(&self, name: &str) -> IcechunkResult<bool> {
1406 let root = self.root()?;
1407 let res = root.deleted_tags().lookup_by_key(name, |name, key| name.cmp(key));
1408 Ok(res.is_some())
1409 }
1410
1411 pub fn resolve_branch(&self, name: &str) -> IcechunkResult<SnapshotId> {
1412 let root = self.root()?;
1413 let res = root
1414 .branches()
1415 .lookup_by_key(name, |r, key| r.name().cmp(key))
1416 .map(|r| {
1417 let index = r.snapshot_index();
1418 SnapshotId::new(root.snapshots().get(index as usize).id().0)
1419 })
1420 .ok_or_else(|| IcechunkFormatErrorKind::BranchNotFound {
1421 branch: name.to_string(),
1422 })
1423 .capture()?;
1424
1425 Ok(res)
1426 }
1427
1428 pub fn spec_version(&self) -> IcechunkResult<SpecVersionBin> {
1429 let raw = self.root()?.spec_version();
1430 raw.try_into()
1431 .map_err(|_| IcechunkFormatErrorKind::InvalidSpecVersion {
1432 found: raw,
1433 max_supported: SpecVersionBin::current() as u8,
1434 })
1435 .capture()
1436 }
1437
1438 pub fn latest_updates(
1439 &self,
1440 ) -> IcechunkResult<impl Iterator<Item = UpdateTuple<'_>>> {
1441 let res = self.root()?.latest_updates().iter().map(|up| self.update_to_tuple(up));
1442 Ok(res)
1443 }
1444
1445 fn update_to_tuple<'a>(
1446 &'a self,
1447 update: generated::Update<'a>,
1448 ) -> IcechunkResult<(UpdateType, DateTime<Utc>, Option<&'a str>)> {
1449 let ty = self.mk_update_type(&update)?;
1450 let ts = timestamp_to_timestamp(update.updated_at())?;
1451 let bp = update.backup_path();
1452 Ok((ty, ts, bp))
1453 }
1454
1455 fn mk_update_type(
1456 &self,
1457 update: &generated::Update<'_>,
1458 ) -> IcechunkResult<UpdateType> {
1459 #[expect(clippy::unwrap_used)]
1460 match update.update_type_type() {
1461 generated::UpdateType::RepoInitializedUpdate => {
1462 Ok(UpdateType::RepoInitializedUpdate)
1463 }
1464 generated::UpdateType::RepoMigratedUpdate => {
1465 let up = update.update_type_as_repo_migrated_update().unwrap();
1466 let from_raw = up.from_version();
1467 let to_raw = up.to_version();
1468 Ok(UpdateType::RepoMigratedUpdate {
1469 from_version: from_raw
1470 .try_into()
1471 .map_err(|_| IcechunkFormatErrorKind::InvalidSpecVersion {
1472 found: from_raw,
1473 max_supported: SpecVersionBin::current() as u8,
1474 })
1475 .capture()?,
1476 to_version: to_raw
1477 .try_into()
1478 .map_err(|_| IcechunkFormatErrorKind::InvalidSpecVersion {
1479 found: to_raw,
1480 max_supported: SpecVersionBin::current() as u8,
1481 })
1482 .capture()?,
1483 })
1484 }
1485 generated::UpdateType::RepoStatusChangedUpdate => {
1486 let up = update.update_type_as_repo_status_changed_update().unwrap();
1487 let fb_status = up.status().unwrap();
1488 let status = fb_status.try_into()?;
1489
1490 Ok(UpdateType::RepoStatusChangedUpdate { status })
1491 }
1492 generated::UpdateType::ConfigChangedUpdate => {
1493 Ok(UpdateType::ConfigChangedUpdate)
1494 }
1495 generated::UpdateType::MetadataChangedUpdate => {
1496 Ok(UpdateType::MetadataChangedUpdate)
1497 }
1498 generated::UpdateType::TagCreatedUpdate => {
1499 let up = update.update_type_as_tag_created_update().unwrap();
1500 Ok(UpdateType::TagCreatedUpdate { name: up.name().to_string() })
1501 }
1502 generated::UpdateType::TagDeletedUpdate => {
1503 let up = update.update_type_as_tag_deleted_update().unwrap();
1504 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1505 Ok(UpdateType::TagDeletedUpdate {
1506 name: up.name().to_string(),
1507 previous_snap_id,
1508 })
1509 }
1510 generated::UpdateType::BranchCreatedUpdate => {
1511 let up = update.update_type_as_branch_created_update().unwrap();
1512 Ok(UpdateType::BranchCreatedUpdate { name: up.name().to_string() })
1513 }
1514 generated::UpdateType::BranchDeletedUpdate => {
1515 let up = update.update_type_as_branch_deleted_update().unwrap();
1516 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1517 Ok(UpdateType::BranchDeletedUpdate {
1518 name: up.name().to_string(),
1519 previous_snap_id,
1520 })
1521 }
1522 generated::UpdateType::BranchResetUpdate => {
1523 let up = update.update_type_as_branch_reset_update().unwrap();
1524 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1525 Ok(UpdateType::BranchResetUpdate {
1526 name: up.name().to_string(),
1527 previous_snap_id,
1528 })
1529 }
1530 generated::UpdateType::NewCommitUpdate => {
1531 let up = update.update_type_as_new_commit_update().unwrap();
1532 let new_snap_id = SnapshotId::new(up.new_snap_id().0);
1533 Ok(UpdateType::NewCommitUpdate {
1534 branch: up.branch().to_string(),
1535 new_snap_id,
1536 })
1537 }
1538 generated::UpdateType::CommitAmendedUpdate => {
1539 let up = update.update_type_as_commit_amended_update().unwrap();
1540 let previous_snap_id = SnapshotId::new(up.previous_snap_id().0);
1541 let new_snap_id = SnapshotId::new(up.new_snap_id().0);
1542 Ok(UpdateType::CommitAmendedUpdate {
1543 branch: up.branch().to_string(),
1544 previous_snap_id,
1545 new_snap_id,
1546 })
1547 }
1548 generated::UpdateType::NewDetachedSnapshotUpdate => {
1549 let up = update.update_type_as_new_detached_snapshot_update().unwrap();
1550 let new_snap_id = SnapshotId::new(up.new_snap_id().0);
1551 Ok(UpdateType::NewDetachedSnapshotUpdate { new_snap_id })
1552 }
1553 generated::UpdateType::GCRanUpdate => Ok(UpdateType::GCRanUpdate),
1554 generated::UpdateType::ExpirationRanUpdate => {
1555 Ok(UpdateType::ExpirationRanUpdate)
1556 }
1557 generated::UpdateType::FeatureFlagChangedUpdate => {
1558 let up = update.update_type_as_feature_flag_changed_update().unwrap();
1559 Ok(UpdateType::FeatureFlagChanged {
1560 id: up.id(),
1561 new_value: if up.is_set() { Some(up.new_value()) } else { None },
1562 })
1563 }
1564 _ => Err(IcechunkFormatErrorKind::InvalidFlatBuffer(
1565 flatbuffers::InvalidFlatbuffer::InconsistentUnion {
1566 field: Cow::Borrowed("latest_update_type"),
1567 field_type: Cow::Borrowed("UpdateType"),
1568 error_trace: Default::default(),
1569 },
1570 ))
1571 .capture(),
1572 }
1573 }
1574
1575 pub fn ancestry<'a>(
1576 &'a self,
1577 snapshot: &SnapshotId,
1578 ) -> IcechunkResult<impl Iterator<Item = IcechunkResult<SnapshotInfo>> + Send + use<'a>>
1579 {
1580 let root = self.root()?;
1581 if let Some(start) = self.resolve_snapshot_index(snapshot)? {
1582 let mut index = Some(start as i32);
1583 let iter = std::iter::from_fn(move || {
1584 if let Some(ix) = index {
1585 if ix >= 0 {
1586 let snap = root.snapshots().get(ix as usize);
1587 index = Some(snap.parent_offset());
1588 Some(mk_snapshot_info(&root, &snap))
1589 } else {
1590 index = None;
1591 None
1592 }
1593 } else {
1594 None
1595 }
1596 });
1597 Ok(iter)
1598 } else {
1599 Err(IcechunkFormatErrorKind::SnapshotIdNotFound {
1600 snapshot_id: snapshot.clone(),
1601 })
1602 .capture()
1603 }
1604 }
1605
1606 pub fn find_snapshot(&self, id: &SnapshotId) -> IcechunkResult<SnapshotInfo> {
1607 let mut anc = self.ancestry(id)?;
1608 #[expect(clippy::panic)]
1609 match anc.next() {
1610 Some(snap) => snap,
1611 None => panic!("Ancestry head snapshot not found"),
1614 }
1615 }
1616
1617 pub fn repo_before_updates(&self) -> IcechunkResult<Option<&str>> {
1618 Ok(self.root()?.repo_before_updates())
1619 }
1620
1621 fn resolve_snapshot_index(&self, id: &SnapshotId) -> IcechunkResult<Option<usize>> {
1622 Ok(lookup_index_by_key(self.root()?.snapshots(), &id.0, |snap, key| {
1623 snap.id().0.cmp(key)
1624 }))
1625 }
1626}
1627
1628fn resolve_ref_iter<'a>(
1629 sorted_snapshots: &[SnapshotInfo],
1630 it: impl IntoIterator<Item = (&'a str, SnapshotId)>,
1631) -> IcechunkResult<Vec<(&'a str, u32)>> {
1632 let mut res: Vec<_> = it
1633 .into_iter()
1634 .map(|(name, id)| {
1635 let idx = sorted_snapshots
1636 .binary_search_by_key(&&id.0, |snap| &snap.id.0)
1637 .map_err(|_| IcechunkFormatErrorKind::SnapshotIdNotFound {
1638 snapshot_id: id.clone(),
1639 })
1640 .capture()? as u32;
1641 Ok::<_, IcechunkFormatError>((name, idx))
1642 })
1643 .try_collect()?;
1644 res.sort_by_key(|(name1, _)| *name1);
1645 Ok(res)
1646}
1647
1648fn timestamp_to_timestamp(ts: u64) -> IcechunkResult<DateTime<Utc>> {
1649 let ts: i64 =
1650 ts.try_into().map_err(|_| IcechunkFormatErrorKind::InvalidTimestamp).capture()?;
1651 DateTime::from_timestamp_micros(ts)
1652 .ok_or(IcechunkFormatErrorKind::InvalidTimestamp)
1653 .capture()
1654}
1655
1656fn mk_snapshot_info(
1657 repo: &generated::Repo<'_>,
1658 snap: &generated::SnapshotInfo<'_>,
1659) -> IcechunkResult<SnapshotInfo> {
1660 let flushed_at = timestamp_to_timestamp(snap.flushed_at())?;
1661 let parent_id = if snap.parent_offset() >= 0 {
1662 let parent = repo.snapshots().get(snap.parent_offset() as usize).id();
1663 Some(parent)
1664 } else {
1665 None
1666 };
1667 let metadata = snap
1668 .metadata()
1669 .map(|items| {
1670 let items = items
1671 .iter()
1672 .map(|item| {
1673 let name = item.name().to_string();
1674 let value = flexbuffers::from_slice(item.value().bytes())
1675 .map_err(Box::new)
1676 .capture()?;
1677 Ok::<_, IcechunkFormatError>((name, value))
1678 })
1679 .try_collect()?;
1680 Ok::<_, IcechunkFormatError>(items)
1681 })
1682 .transpose()?
1683 .unwrap_or_default();
1684
1685 let pruned_ancestor_tx_logs = snap
1686 .pruned_ancestor_tx_logs()
1687 .map(|ids| ids.iter().map(|oid| SnapshotId::new(oid.0)).collect())
1688 .unwrap_or_default();
1689
1690 Ok(SnapshotInfo {
1691 id: SnapshotId::new(snap.id().0),
1692 flushed_at,
1693 message: snap.message().to_string(),
1694 metadata,
1695 parent_id: parent_id.map(|buf| SnapshotId::new(buf.0)),
1696 pruned_ancestor_tx_logs,
1697 })
1698}
1699
1700fn update_type_to_fb<'bldr>(
1701 builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
1702 update: &UpdateType,
1703) -> IcechunkResult<(generated::UpdateType, WIPOffset<UnionWIPOffset>)> {
1704 match update {
1705 UpdateType::RepoInitializedUpdate => Ok((
1706 generated::UpdateType::RepoInitializedUpdate,
1707 generated::RepoInitializedUpdate::create(
1708 builder,
1709 &generated::RepoInitializedUpdateArgs {},
1710 )
1711 .as_union_value(),
1712 )),
1713 UpdateType::RepoMigratedUpdate { from_version, to_version } => Ok((
1714 generated::UpdateType::RepoMigratedUpdate,
1715 generated::RepoMigratedUpdate::create(
1716 builder,
1717 &generated::RepoMigratedUpdateArgs {
1718 from_version: *from_version as u8,
1719 to_version: *to_version as u8,
1720 },
1721 )
1722 .as_union_value(),
1723 )),
1724 UpdateType::RepoStatusChangedUpdate { status } => {
1725 let limited_availability_reason = status
1726 .limited_availability_reason
1727 .as_ref()
1728 .map(|r| builder.create_string(r));
1729 let status = generated::RepoStatus::create(
1730 builder,
1731 &generated::RepoStatusArgs {
1732 availability: status.availability.into(),
1733 set_at: status.set_at.timestamp_micros() as u64,
1734 limited_availability_reason,
1735 },
1736 );
1737
1738 Ok((
1739 generated::UpdateType::RepoStatusChangedUpdate,
1740 generated::RepoStatusChangedUpdate::create(
1741 builder,
1742 &generated::RepoStatusChangedUpdateArgs { status: Some(status) },
1743 )
1744 .as_union_value(),
1745 ))
1746 }
1747 UpdateType::ConfigChangedUpdate => Ok((
1748 generated::UpdateType::ConfigChangedUpdate,
1749 generated::ConfigChangedUpdate::create(
1750 builder,
1751 &generated::ConfigChangedUpdateArgs {},
1752 )
1753 .as_union_value(),
1754 )),
1755 UpdateType::MetadataChangedUpdate => Ok((
1756 generated::UpdateType::MetadataChangedUpdate,
1757 generated::MetadataChangedUpdate::create(
1758 builder,
1759 &generated::MetadataChangedUpdateArgs {},
1760 )
1761 .as_union_value(),
1762 )),
1763 UpdateType::TagCreatedUpdate { name } => {
1764 let name = Some(builder.create_string(name));
1765 Ok((
1766 generated::UpdateType::TagCreatedUpdate,
1767 generated::TagCreatedUpdate::create(
1768 builder,
1769 &generated::TagCreatedUpdateArgs { name },
1770 )
1771 .as_union_value(),
1772 ))
1773 }
1774 UpdateType::TagDeletedUpdate { name, previous_snap_id } => {
1775 let name = Some(builder.create_string(name));
1776 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1777 let previous_snap_id = Some(&object_id12);
1778 Ok((
1779 generated::UpdateType::TagDeletedUpdate,
1780 generated::TagDeletedUpdate::create(
1781 builder,
1782 &generated::TagDeletedUpdateArgs { name, previous_snap_id },
1783 )
1784 .as_union_value(),
1785 ))
1786 }
1787 UpdateType::BranchCreatedUpdate { name } => {
1788 let name = Some(builder.create_string(name));
1789 Ok((
1790 generated::UpdateType::BranchCreatedUpdate,
1791 generated::BranchCreatedUpdate::create(
1792 builder,
1793 &generated::BranchCreatedUpdateArgs { name },
1794 )
1795 .as_union_value(),
1796 ))
1797 }
1798 UpdateType::BranchDeletedUpdate { name, previous_snap_id } => {
1799 let name = Some(builder.create_string(name));
1800 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1801 let previous_snap_id = Some(&object_id12);
1802 Ok((
1803 generated::UpdateType::BranchDeletedUpdate,
1804 generated::BranchDeletedUpdate::create(
1805 builder,
1806 &generated::BranchDeletedUpdateArgs { name, previous_snap_id },
1807 )
1808 .as_union_value(),
1809 ))
1810 }
1811 UpdateType::BranchResetUpdate { name, previous_snap_id } => {
1812 let name = Some(builder.create_string(name));
1813 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1814 let previous_snap_id = Some(&object_id12);
1815 Ok((
1816 generated::UpdateType::BranchResetUpdate,
1817 generated::BranchResetUpdate::create(
1818 builder,
1819 &generated::BranchResetUpdateArgs { name, previous_snap_id },
1820 )
1821 .as_union_value(),
1822 ))
1823 }
1824 UpdateType::NewCommitUpdate { branch, new_snap_id } => {
1825 let branch = Some(builder.create_string(branch));
1826 let object_id12 = generated::ObjectId12::new(&new_snap_id.0);
1827 let new_snap_id = Some(&object_id12);
1828 Ok((
1829 generated::UpdateType::NewCommitUpdate,
1830 generated::NewCommitUpdate::create(
1831 builder,
1832 &generated::NewCommitUpdateArgs { branch, new_snap_id },
1833 )
1834 .as_union_value(),
1835 ))
1836 }
1837 UpdateType::CommitAmendedUpdate { branch, previous_snap_id, new_snap_id } => {
1838 let branch = Some(builder.create_string(branch));
1839 let object_id12 = generated::ObjectId12::new(&previous_snap_id.0);
1840 let previous_snap_id = Some(&object_id12);
1841 let object_id12 = generated::ObjectId12::new(&new_snap_id.0);
1842 let new_snap_id = Some(&object_id12);
1843 Ok((
1844 generated::UpdateType::CommitAmendedUpdate,
1845 generated::CommitAmendedUpdate::create(
1846 builder,
1847 &generated::CommitAmendedUpdateArgs {
1848 branch,
1849 previous_snap_id,
1850 new_snap_id,
1851 },
1852 )
1853 .as_union_value(),
1854 ))
1855 }
1856 UpdateType::NewDetachedSnapshotUpdate { new_snap_id } => {
1857 let object_id12 = generated::ObjectId12::new(&new_snap_id.0);
1858 let new_snap_id = Some(&object_id12);
1859 Ok((
1860 generated::UpdateType::NewDetachedSnapshotUpdate,
1861 generated::NewDetachedSnapshotUpdate::create(
1862 builder,
1863 &generated::NewDetachedSnapshotUpdateArgs { new_snap_id },
1864 )
1865 .as_union_value(),
1866 ))
1867 }
1868 UpdateType::GCRanUpdate => Ok((
1869 generated::UpdateType::GCRanUpdate,
1870 generated::GCRanUpdate::create(builder, &generated::GCRanUpdateArgs {})
1871 .as_union_value(),
1872 )),
1873 UpdateType::ExpirationRanUpdate => Ok((
1874 generated::UpdateType::ExpirationRanUpdate,
1875 generated::ExpirationRanUpdate::create(
1876 builder,
1877 &generated::ExpirationRanUpdateArgs {},
1878 )
1879 .as_union_value(),
1880 )),
1881 UpdateType::FeatureFlagChanged { id, new_value } => Ok((
1882 generated::UpdateType::FeatureFlagChangedUpdate,
1883 generated::FeatureFlagChangedUpdate::create(
1884 builder,
1885 &generated::FeatureFlagChangedUpdateArgs {
1886 id: *id,
1887 new_value: new_value.unwrap_or_default(),
1888 is_set: new_value.is_some(),
1889 },
1890 )
1891 .as_union_value(),
1892 )),
1893 }
1894}
1895
1896#[cfg(test)]
1897mod tests {
1898
1899 use super::*;
1900 use crate::roundtrip_serialization_tests;
1901 use proptest::prelude::*;
1902 use std::collections::HashSet;
1903
1904 fn a_snapshot_info(message: &str) -> SnapshotInfo {
1905 SnapshotInfo {
1906 id: SnapshotId::random(),
1907 parent_id: None,
1908 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
1909 message: message.to_string(),
1910 metadata: Default::default(),
1911 pruned_ancestor_tx_logs: vec![],
1912 }
1913 }
1914
1915 fn valid_repo_info() -> impl Strategy<Value = RepoInfo> {
1918 proptest::collection::vec(any::<String>(), 1..5).prop_map(|messages| {
1919 RepoInfo::initial(
1920 SpecVersionBin::current(),
1921 a_snapshot_info(messages.first().map_or("initial", |m| m.as_str())),
1922 100,
1923 None::<&()>,
1924 None,
1925 )
1926 })
1927 }
1928
1929 roundtrip_serialization_tests!(serialize_and_deserialize_repo_info - valid_repo_info);
1930
1931 #[test]
1932 fn deserializing_an_unverified_buffer_fails() {
1933 let bytes = rmp_serde::to_vec(&RepoInfo { buffer: vec![0u8; 64] }).unwrap();
1935 assert!(
1936 rmp_serde::from_slice::<RepoInfo>(bytes.as_slice()).is_err(),
1937 "deserialization must not produce a RepoInfo with an unverified buffer"
1938 );
1939 }
1940
1941 #[test]
1942 fn repo_info_with_more_than_ten_million_snapshots_is_usable()
1943 -> Result<(), Box<dyn std::error::Error>> {
1944 let n = 10_000_001;
1947 let snapshots: Vec<_> = (0..n).map(|_| a_snapshot_info("")).collect();
1948 let branch_target = snapshots[0].id.clone();
1949 let info = RepoInfo::new(
1950 SpecVersionBin::current(),
1951 [],
1952 [("main", branch_target)],
1953 [],
1954 snapshots,
1955 &Default::default(),
1956 UpdateInfo {
1957 update_type: UpdateType::RepoInitializedUpdate,
1958 update_time: Utc::now(),
1959 previous_updates: Vec::<UpdateTuple<'_>>::new(),
1960 },
1961 None,
1962 10,
1963 None,
1964 None,
1965 None::<std::iter::Empty<u16>>,
1966 None::<std::iter::Empty<u16>>,
1967 &RepoStatus {
1968 availability: RepoAvailability::Online,
1969 set_at: Utc::now(),
1970 limited_availability_reason: None,
1971 },
1972 )?;
1973
1974 let info = RepoInfo::from_buffer(info.bytes().to_vec())?;
1975 assert_eq!(info.all_snapshots()?.count(), n);
1976 assert!(info.branch_names()?.any(|name| name == "main"));
1977 Ok(())
1978 }
1979
1980 #[test]
1981 fn test_add_snapshot() -> Result<(), Box<dyn std::error::Error>> {
1982 let id1 = SnapshotId::random();
1983 let snap1 = SnapshotInfo {
1984 id: id1.clone(),
1985 parent_id: None,
1986 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
1988 message: "snap 1".to_string(),
1989 metadata: Default::default(),
1990 pruned_ancestor_tx_logs: vec![],
1991 };
1992 let repo = RepoInfo::initial(
1993 SpecVersionBin::current(),
1994 snap1.clone(),
1995 100,
1996 None::<&()>,
1997 None,
1998 );
1999 assert_eq!(repo.all_snapshots()?.next().unwrap().unwrap(), snap1);
2000
2001 let id2 = SnapshotId::random();
2002 let snap2 = SnapshotInfo {
2003 id: id2.clone(),
2004 parent_id: Some(id1.clone()),
2005 flushed_at: DateTime::from_timestamp_micros(2_000_000).unwrap(),
2006 message: "snap 2".to_string(),
2007 ..snap1.clone()
2008 };
2009 let repo = repo.add_snapshot(
2010 SpecVersionBin::current(),
2011 snap2.clone(),
2012 Some("main"),
2013 UpdateType::NewCommitUpdate {
2014 branch: "main".to_string(),
2015 new_snap_id: snap2.id.clone(),
2016 },
2017 None,
2018 "foo/bar",
2019 100,
2020 )?;
2021 assert_eq!(&repo.resolve_branch("main")?, &snap2.id);
2022 assert_eq!(repo.repo_before_updates()?, None);
2023
2024 let all: HashSet<_> = repo.all_snapshots()?.try_collect()?;
2025 assert_eq!(all, HashSet::from_iter([snap1.clone(), snap2.clone()]));
2026
2027 let anc: Vec<_> = repo.ancestry(&id1)?.try_collect()?;
2028 assert_eq!(anc, std::slice::from_ref(&snap1));
2029
2030 let anc: Vec<_> = repo.ancestry(&id2)?.try_collect()?;
2031 assert_eq!(anc, [snap2.clone(), snap1.clone()]);
2032
2033 assert!(repo.ancestry(&SnapshotId::random()).is_err());
2034
2035 let id3 = SnapshotId::random();
2036 let snap3 = SnapshotInfo {
2037 id: id3.clone(),
2038 parent_id: Some(id2.clone()),
2039 flushed_at: DateTime::from_timestamp_micros(3_000_000).unwrap(),
2040 message: "snap 3".to_string(),
2041 ..snap2.clone()
2042 };
2043 let repo = repo.add_snapshot(
2044 SpecVersionBin::current(),
2045 snap3.clone(),
2046 Some("main"),
2047 UpdateType::NewCommitUpdate {
2048 branch: "main".to_string(),
2049 new_snap_id: snap3.id.clone(),
2050 },
2051 None,
2052 "foo",
2053 100,
2054 )?;
2055 assert_eq!(&repo.resolve_branch("main")?, &snap3.id);
2056 let all: HashSet<_> = repo.all_snapshots()?.try_collect()?;
2057 assert_eq!(
2058 all,
2059 HashSet::from_iter([snap1.clone(), snap2.clone(), snap3.clone()])
2060 );
2061
2062 let all: HashSet<_> = repo.all_snapshots()?.try_collect()?;
2063 assert_eq!(
2064 all,
2065 HashSet::from_iter([snap1.clone(), snap2.clone(), snap3.clone()])
2066 );
2067
2068 let anc: Vec<_> = repo.ancestry(&id3)?.try_collect()?;
2069 assert_eq!(anc, [snap3.clone(), snap2.clone(), snap1.clone()]);
2070 Ok(())
2071 }
2072
2073 #[test]
2074 fn test_pruned_ancestor_tx_logs_roundtrip() -> Result<(), Box<dyn std::error::Error>>
2075 {
2076 let root_id = SnapshotId::random();
2077 let root = SnapshotInfo {
2078 id: root_id.clone(),
2079 parent_id: None,
2080 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
2081 message: "root".to_string(),
2082 metadata: Default::default(),
2083 pruned_ancestor_tx_logs: vec![],
2084 };
2085 let repo = RepoInfo::initial(
2086 SpecVersionBin::current(),
2087 root.clone(),
2088 100,
2089 None::<&()>,
2090 None,
2091 );
2092
2093 let pruned =
2095 vec![SnapshotId::random(), SnapshotId::random(), SnapshotId::random()];
2096 let tip_id = SnapshotId::random();
2097 let tip = SnapshotInfo {
2098 id: tip_id.clone(),
2099 parent_id: Some(root_id.clone()),
2100 flushed_at: DateTime::from_timestamp_micros(2_000_000).unwrap(),
2101 message: "tip".to_string(),
2102 metadata: Default::default(),
2103 pruned_ancestor_tx_logs: pruned.clone(),
2104 };
2105 let repo = repo.add_snapshot(
2106 SpecVersionBin::current(),
2107 tip.clone(),
2108 Some("main"),
2109 UpdateType::NewCommitUpdate {
2110 branch: "main".to_string(),
2111 new_snap_id: tip_id.clone(),
2112 },
2113 None,
2114 "foo",
2115 100,
2116 )?;
2117
2118 let read_tip = repo.find_snapshot(&tip_id)?;
2120 assert_eq!(read_tip.pruned_ancestor_tx_logs, pruned);
2121
2122 let read_root = repo.find_snapshot(&root_id)?;
2124 assert!(read_root.pruned_ancestor_tx_logs.is_empty());
2125 Ok(())
2126 }
2127
2128 #[test]
2129 fn test_tags_and_branches() -> Result<(), Box<dyn std::error::Error>> {
2130 let id1 = SnapshotId::random();
2131 let snap1 = SnapshotInfo {
2132 id: id1.clone(),
2133 parent_id: None,
2134 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
2136 message: "snap 1".to_string(),
2137 metadata: Default::default(),
2138 pruned_ancestor_tx_logs: vec![],
2139 };
2140 let repo = RepoInfo::initial(
2141 SpecVersionBin::current(),
2142 snap1.clone(),
2143 100,
2144 None::<&()>,
2145 None,
2146 );
2147 let repo = repo.add_branch(SpecVersionBin::current(), "foo", &id1, "foo", 100)?;
2148 let repo = repo.add_branch(SpecVersionBin::current(), "bar", &id1, "bar", 100)?;
2149 assert!(matches!(
2150 repo.add_branch(
2151 SpecVersionBin::current(),
2152 "bad-snap",
2153 &SnapshotId::random(),
2154 "bad",
2155 100
2156 ),
2157 Err(IcechunkFormatError {
2158 kind: IcechunkFormatErrorKind::SnapshotIdNotFound { .. },
2159 ..
2160 })
2161 ));
2162 assert!(matches!(
2164 repo.add_branch(SpecVersionBin::current(), "bar", &id1, "/foo/bar", 100),
2165 Err(IcechunkFormatError {
2166 kind: IcechunkFormatErrorKind::BranchAlreadyExists { .. },
2167 ..
2168 })
2169 ));
2170
2171 assert_eq!(
2172 repo.all_branches()?.collect::<HashSet<_>>(),
2173 [("main", 0), ("foo", 0), ("bar", 0)].into()
2174 );
2175
2176 let id2 = SnapshotId::random();
2177 let snap2 = SnapshotInfo {
2178 id: id2.clone(),
2179 parent_id: Some(id1.clone()),
2180 flushed_at: Utc::now(),
2181 message: "snap 2".to_string(),
2182 ..snap1.clone()
2183 };
2184 let repo = repo.add_snapshot(
2185 SpecVersionBin::current(),
2186 snap2.clone(),
2187 Some("main"),
2188 UpdateType::NewCommitUpdate {
2189 branch: "main".to_string(),
2190 new_snap_id: snap2.id.clone(),
2191 },
2192 None,
2193 "foo",
2194 100,
2195 )?;
2196 let repo =
2197 repo.add_branch(SpecVersionBin::current(), "baz", &id2, "/foo/bar", 100)?;
2198 assert_eq!(repo.resolve_branch("main")?, id2.clone());
2199 assert_eq!(repo.resolve_branch("foo")?, id1.clone());
2200 assert_eq!(repo.resolve_branch("bar")?, id1.clone());
2201 assert_eq!(repo.resolve_branch("baz")?, id2.clone());
2202
2203 let repo = repo.delete_branch(SpecVersionBin::current(), "bar", "bar", 100)?;
2204 assert!(repo.resolve_branch("bar").is_err());
2205 assert_eq!(
2206 repo.all_branches()?.map(|(n, _)| n).collect::<HashSet<_>>(),
2207 ["main", "foo", "baz"].into()
2208 );
2209
2210 assert!(
2211 repo.delete_branch(SpecVersionBin::current(), "bad-branch", "bad", 100)
2212 .is_err()
2213 );
2214
2215 let repo = repo.add_tag(SpecVersionBin::current(), "tag1", &id1, "tag1", 100)?;
2217 let repo = repo.add_tag(SpecVersionBin::current(), "tag2", &id2, "tag2", 100)?;
2218 assert!(
2219 repo.add_tag(
2220 SpecVersionBin::current(),
2221 "bad-snap",
2222 &SnapshotId::random(),
2223 "bad",
2224 100
2225 )
2226 .is_err()
2227 );
2228 assert!(
2229 repo.add_tag(SpecVersionBin::current(), "tag1", &id1, "tag1-again", 100)
2230 .is_err()
2231 );
2232 assert_eq!(repo.resolve_tag("tag1")?, id1.clone());
2233 assert_eq!(repo.resolve_tag("tag2")?, id2.clone());
2234 assert_eq!(
2235 repo.all_tags()?.map(|(n, _)| n).collect::<HashSet<_>>(),
2236 ["tag1", "tag2"].into()
2237 );
2238
2239 let repo = repo.add_tag(SpecVersionBin::current(), "tag3", &id1, "tag3", 100)?;
2241 let repo =
2242 repo.delete_tag(SpecVersionBin::current(), "tag3", "delete-tag3", 100)?;
2243 assert_eq!(
2244 repo.all_tags()?.map(|(n, _)| n).collect::<HashSet<_>>(),
2245 ["tag1", "tag2"].into()
2246 );
2247 assert!(
2249 repo.add_tag(SpecVersionBin::current(), "tag3", &id1, "tag3-again", 100)
2250 .is_err()
2251 );
2252 assert!(
2254 repo.delete_tag(SpecVersionBin::current(), "tag3", "delete-tag3-again", 100)
2255 .is_err()
2256 );
2257 assert_eq!(
2258 repo.all_tags()?.map(|(n, _)| n).collect::<HashSet<_>>(),
2259 ["tag1", "tag2"].into()
2260 );
2261 Ok(())
2262 }
2263
2264 #[test]
2265 fn test_repo_info_updates() -> Result<(), Box<dyn std::error::Error>> {
2266 let id1 = SnapshotId::random();
2267 let snap1 = SnapshotInfo {
2268 id: id1.clone(),
2269 parent_id: None,
2270 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
2272 message: "snap 1".to_string(),
2273 metadata: Default::default(),
2274 pruned_ancestor_tx_logs: vec![],
2275 };
2276
2277 let num_updates_per_file: u16 = 10;
2278 let n = num_updates_per_file as usize;
2279
2280 let mut repo = RepoInfo::initial(
2282 SpecVersionBin::current(),
2283 snap1,
2284 num_updates_per_file,
2285 None::<&()>,
2286 None,
2287 );
2288 assert_eq!(repo.latest_updates()?.count(), 1);
2289 let (last_update, _, file) = repo.latest_updates()?.next().unwrap()?;
2290 assert!(file.is_none());
2291 assert_eq!(last_update, UpdateType::RepoInitializedUpdate);
2292 assert!(repo.repo_before_updates()?.is_none());
2293 for i in 1..=(n - 1) {
2296 repo = repo.add_branch(
2297 SpecVersionBin::current(),
2298 i.to_string().as_str(),
2299 &id1,
2300 (i - 1).to_string().as_str(),
2301 num_updates_per_file,
2302 )?;
2303 }
2304
2305 assert_eq!(repo.latest_updates()?.count(), n);
2306 let updates = repo.latest_updates()?;
2307
2308 for (idx, update) in updates.enumerate() {
2310 let (update, _, file) = update?;
2311 if idx == n - 1 {
2312 assert_eq!(update, UpdateType::RepoInitializedUpdate);
2313 assert_eq!(file, Some("0"));
2314 } else {
2315 assert_eq!(
2316 update,
2317 UpdateType::BranchCreatedUpdate { name: (n - 1 - idx).to_string() }
2318 );
2319 if idx == 0 {
2320 assert!(file.is_none());
2321 } else {
2322 assert_eq!(file, Some((n - 1 - idx).to_string().as_str()));
2323 }
2324 }
2325 }
2326 assert!(repo.repo_before_updates()?.is_none());
2327
2328 repo = repo.add_tag(
2330 SpecVersionBin::current(),
2331 "tag",
2332 &id1,
2333 "first-branches",
2334 num_updates_per_file,
2335 )?;
2336 assert_eq!(repo.latest_updates()?.count(), n);
2338 assert_eq!(repo.repo_before_updates()?, Some("0"));
2340 let mut updates = repo.latest_updates()?;
2341 let (last_update, _, file) = updates.next().unwrap()?;
2343 assert_eq!(last_update, UpdateType::TagCreatedUpdate { name: "tag".to_string() });
2344 assert!(file.is_none());
2345
2346 let (last_update, _, file) = updates.next().unwrap()?;
2348 assert_eq!(
2349 last_update,
2350 UpdateType::BranchCreatedUpdate { name: (n - 1).to_string() }
2351 );
2352 assert_eq!(file, Some("first-branches"));
2353
2354 for (idx, update) in updates.enumerate() {
2356 let (update, _, file) = update?;
2357 assert_eq!(file, Some((n - 2 - idx).to_string().as_str()));
2358 assert_eq!(
2359 update,
2360 UpdateType::BranchCreatedUpdate { name: (n - 2 - idx).to_string() }
2361 );
2362 }
2363
2364 Ok(())
2365 }
2366
2367 #[test]
2368 fn test_update_timestamp_ordering_rejected() -> Result<(), Box<dyn std::error::Error>>
2369 {
2370 let flushed_at = DateTime::from_timestamp_micros(1_000_000).unwrap();
2371 let id1 = SnapshotId::random();
2372 let snap1 = SnapshotInfo {
2373 id: id1.clone(),
2374 parent_id: None,
2375 flushed_at,
2376 message: "snap 1".to_string(),
2377 metadata: Default::default(),
2378 pruned_ancestor_tx_logs: vec![],
2379 };
2380 let repo = RepoInfo::initial(
2381 SpecVersionBin::current(),
2382 snap1,
2383 100,
2384 None::<&()>,
2385 Some(flushed_at),
2386 );
2387
2388 let id2 = SnapshotId::random();
2391 let snap2 = SnapshotInfo {
2392 id: id2.clone(),
2393 parent_id: Some(id1.clone()),
2394 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
2395 message: "snap 2".to_string(),
2396 metadata: Default::default(),
2397 pruned_ancestor_tx_logs: vec![],
2398 };
2399 let result = repo.add_snapshot(
2400 SpecVersionBin::current(),
2401 snap2,
2402 Some("main"),
2403 UpdateType::NewCommitUpdate {
2404 branch: "main".to_string(),
2405 new_snap_id: id2.clone(),
2406 },
2407 Some(flushed_at),
2408 "backup",
2409 100,
2410 );
2411 assert!(matches!(
2412 result,
2413 Err(IcechunkFormatError {
2414 kind: IcechunkFormatErrorKind::InvalidUpdateTimestamp { .. },
2415 ..
2416 })
2417 ));
2418
2419 let flushed_at = DateTime::from_timestamp_micros(500_000).unwrap();
2422 let id3 = SnapshotId::random();
2423 let snap3 = SnapshotInfo {
2424 id: id3.clone(),
2425 parent_id: Some(id1.clone()),
2426 flushed_at,
2427 message: "snap 3".to_string(),
2428 metadata: Default::default(),
2429 pruned_ancestor_tx_logs: vec![],
2430 };
2431 let result = repo.add_snapshot(
2432 SpecVersionBin::current(),
2433 snap3,
2434 Some("main"),
2435 UpdateType::NewCommitUpdate {
2436 branch: "main".to_string(),
2437 new_snap_id: id3.clone(),
2438 },
2439 Some(flushed_at),
2440 "backup",
2441 100,
2442 );
2443 assert!(matches!(
2444 result,
2445 Err(IcechunkFormatError {
2446 kind: IcechunkFormatErrorKind::InvalidUpdateTimestamp { .. },
2447 ..
2448 })
2449 ));
2450
2451 let flushed_at = DateTime::from_timestamp_micros(2_000_000).unwrap();
2453 let id4 = SnapshotId::random();
2454 let snap4 = SnapshotInfo {
2455 id: id4.clone(),
2456 parent_id: Some(id1.clone()),
2457 flushed_at,
2458 message: "snap 4".to_string(),
2459 metadata: Default::default(),
2460 pruned_ancestor_tx_logs: vec![],
2461 };
2462 let result = repo.add_snapshot(
2463 SpecVersionBin::current(),
2464 snap4,
2465 Some("main"),
2466 UpdateType::NewCommitUpdate {
2467 branch: "main".to_string(),
2468 new_snap_id: id4.clone(),
2469 },
2470 Some(flushed_at),
2471 "backup",
2472 100,
2473 );
2474 assert!(result.is_ok());
2475
2476 Ok(())
2477 }
2478
2479 #[test]
2482 fn test_set_status_silent() -> Result<(), Box<dyn std::error::Error>> {
2483 let id1 = SnapshotId::random();
2484 let snap1 = SnapshotInfo {
2485 id: id1.clone(),
2486 parent_id: None,
2487 flushed_at: DateTime::from_timestamp_micros(1_000_000).unwrap(),
2488 message: "snap 1".to_string(),
2489 metadata: Default::default(),
2490 pruned_ancestor_tx_logs: vec![],
2491 };
2492
2493 let repo =
2494 RepoInfo::initial(SpecVersionBin::current(), snap1, 100, None::<&()>, None);
2495
2496 let repo =
2498 repo.add_branch(SpecVersionBin::current(), "dev", &id1, "backup0", 100)?;
2499
2500 let updates_before: Vec<_> =
2501 repo.latest_updates()?.collect::<Result<Vec<_>, _>>()?;
2502 let rbu_before = repo.repo_before_updates()?;
2503 let num_updates_before = updates_before.len();
2504
2505 let new_status = RepoStatus {
2507 availability: RepoAvailability::ReadOnly,
2508 set_at: Utc::now(),
2509 limited_availability_reason: Some("maintenance".to_string()),
2510 };
2511 let repo_normal = repo.set_status(
2512 SpecVersionBin::current(),
2513 &new_status,
2514 Some(("backup1", 100)),
2515 )?;
2516 let updates_after_normal: Vec<_> =
2517 repo_normal.latest_updates()?.collect::<Result<Vec<_>, _>>()?;
2518 assert_eq!(updates_after_normal.len(), num_updates_before + 1);
2519 assert!(matches!(
2520 &updates_after_normal[0].0,
2521 UpdateType::RepoStatusChangedUpdate { .. }
2522 ));
2523 let normal_status = repo_normal.status()?;
2524 assert_eq!(normal_status.availability, RepoAvailability::ReadOnly);
2525 assert_eq!(
2526 normal_status.limited_availability_reason.as_deref(),
2527 Some("maintenance")
2528 );
2529
2530 let silent_status = RepoStatus {
2532 availability: RepoAvailability::ReadOnly,
2533 set_at: Utc::now(),
2534 limited_availability_reason: None,
2535 };
2536 let repo_silent =
2537 repo.set_status(SpecVersionBin::current(), &silent_status, None)?;
2538
2539 let updates_after_silent: Vec<_> =
2540 repo_silent.latest_updates()?.collect::<Result<Vec<_>, _>>()?;
2541 assert_eq!(updates_after_silent.len(), num_updates_before);
2543 for (before, after) in updates_before.iter().zip(updates_after_silent.iter()) {
2544 assert_eq!(before.0, after.0);
2545 assert_eq!(before.1, after.1);
2546 assert_eq!(before.2, after.2);
2547 }
2548 assert_eq!(repo_silent.repo_before_updates()?, rbu_before);
2549
2550 let silent_result_status = repo_silent.status()?;
2552 assert_eq!(silent_result_status.availability, RepoAvailability::ReadOnly);
2553 assert!(silent_result_status.limited_availability_reason.is_none());
2554
2555 Ok(())
2556 }
2557}