Skip to main content

icechunk_format/
repo_info.rs

1//! Version info, branches, and tags for a repository.
2
3use 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    // Offline is defined in the flatbuffers, but we won't support it
28    // before better specs on how we want to use it
29}
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// TODO: should we not implement serialize and let the session fetch the repo info?
89#[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        // FIXME: add other fields
117        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, // taken from the default
179    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        // TODO: we should check no loops
310        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        // TODO: provide accessors for last_updated_at, status, metadata, etc.
408        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        // replace the backup path in the last update, that must be None, by the new backup path
452        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        // A backup_path points to the previous repo info file. It is only meaningful
463        // when there are previous updates (you can't back up what doesn't exist).
464        // However, previous updates CAN exist without a backup_path: during migration,
465        // synthetic ops log entries are generated with no prior repo info file to
466        // reference. (This differs from RepoInitializedUpdate, which has neither
467        // previous updates nor a backup path.)
468        assert!(
469            backup_path.is_none() || last_update.is_some(),
470            "A backup path must not be provided without previous updates"
471        );
472
473        // Reject updates whose timestamp is not strictly newer than the top of the ops log
474        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        // If we didn't overflow (all previous updates fit in the new file),
496        // preserve the old file's chain pointer so older history remains reachable.
497        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            // Once we've reached the target file size, look for a valid overflow
505            // point: an entry whose backup_path we can use as the chain pointer.
506            // Entries without a backup_path (e.g. synthetic migration entries)
507            // can't serve as overflow — keep them in the file instead.
508            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        // This method is basically constant, so it's OK to unwrap in it
546        #[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    /// Read the raw config bytes from the `FlatBuffer` (for pass-through in mutations).
575    pub fn config_bytes_raw(&self) -> IcechunkResult<Option<Vec<u8>>> {
576        Ok(self.root()?.config().map(|v| v.bytes().to_vec()))
577    }
578
579    /// Read the repository configuration from the repo info.
580    /// Returns `None` for repos created before config was embedded,
581    /// or for repos using the default configuration.
582    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    /// None means not set, use the default value
630    /// Some(true) means enabled
631    /// Some(false) means disabled
632    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    /// Doesn't check the validity of `flag_id`
671    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>>, // for testing
751        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                // retain preserves order
864                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                // retain preserves order
1015                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    /// Update the embedded configuration and record a `ConfigChangedUpdate` in the op log.
1086    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            // It's OK to panic here because ancestry already found the snapshot, and
1410            // it's always the first element of the ancestry
1411            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    // Generates an instance of RepoInfo which may not deserialize to a valid repository
1697    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            // needs to be micro second rounded
1712            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            // needs to be micro second rounded
1804            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        // cannot add existing
1831        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        // tags
1884        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        // delete tags
1908        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        // cannot add deleted
1916        assert!(
1917            repo.add_tag(SpecVersionBin::current(), "tag3", &id1, "tag3-again", 100)
1918                .is_err()
1919        );
1920        // cannot delete deleted
1921        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            // needs to be micro second rounded
1939            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        // check updates for a new repo
1948        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        // check updates after num_updates_per_file changes
1961        // fill the first page of updates by adding branches
1962        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        // check all other updates
1976        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        // Now, if we add another change, it won't fit in the first "page" of repo updates
1996        repo = repo.add_tag(
1997            SpecVersionBin::current(),
1998            "tag",
1999            &id1,
2000            "first-branches",
2001            num_updates_per_file,
2002        )?;
2003        // the file only contains the first "page" worth of updates
2004        assert_eq!(repo.latest_updates()?.count(), n);
2005        // next file is the oldest change
2006        assert_eq!(repo.repo_before_updates()?, Some("0"));
2007        let mut updates = repo.latest_updates()?;
2008        // last change is the tag creation
2009        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        // next change is a branch creation backed up to first-branches
2014        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        // all other changes are branch creation (repo creation is in the next page)
2022        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        // Attempting add_snapshot with a timestamp equal to the top of the ops log
2055        // should fail
2056        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        // Attempting add_snapshot with a timestamp older than the top of the ops log
2085        // should also fail
2086        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        // Attempting add_snapshot with a strictly newer timestamp should succeed
2116        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}