Skip to main content

icechunk_format/
snapshot.rs

1//! Repository state at a point in time (arrays, groups, and manifest references).
2
3use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, ops::Range, sync::Arc};
4
5use bytes::Bytes;
6use chrono::{DateTime, Utc};
7use flatbuffers::{
8    FlatBufferBuilder, ForwardsUOffset, UnionWIPOffset, Vector, VerifierOptions,
9    WIPOffset,
10};
11use itertools::{Either, Itertools as _};
12use quick_cache::sync::{Cache, GuardResult};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16pub use crate::NodeType;
17use crate::{
18    AttributesId, ChunkIndices, IcechunkFormatError, IcechunkFormatErrorKind,
19    IcechunkResult, ManifestId, NodeId, Path, SnapshotId,
20    flatbuffers::generated,
21    format_constants::SpecVersionBin,
22    lookup_index_by_key,
23    manifest::{Manifest, ManifestExtents, ManifestRef},
24};
25use icechunk_types::{ICResultExt as _, error::ICResultCtxExt as _};
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct DimensionShape {
29    dim_length: u64,
30    num_chunks: u32,
31}
32
33impl DimensionShape {
34    pub fn new(array_length: u64, num_chunks: u32) -> Self {
35        Self { dim_length: array_length, num_chunks }
36    }
37    pub fn array_length(&self) -> u64 {
38        self.dim_length
39    }
40    pub fn num_chunks(&self) -> u32 {
41        self.num_chunks
42    }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct ArrayShape(Vec<DimensionShape>);
47
48impl ArrayShape {
49    pub fn len(&self) -> usize {
50        self.0.len()
51    }
52    pub fn is_empty(&self) -> bool {
53        self.0.is_empty()
54    }
55
56    pub fn get(&self, ax: usize) -> Option<DimensionShape> {
57        if ax > self.len() - 1 { None } else { Some(self.0[ax].clone()) }
58    }
59
60    pub fn iter(&self) -> impl Iterator<Item = &DimensionShape> {
61        self.0.iter()
62    }
63
64    pub fn num_chunks(&self) -> impl Iterator<Item = u32> {
65        self.0.iter().map(|x| x.num_chunks())
66    }
67
68    pub fn new<I>(it: I) -> Option<Self>
69    where
70        I: IntoIterator<Item = (u64, u32)>,
71    {
72        let v = it.into_iter().map(|(al, nc)| Some(DimensionShape::new(al, nc)));
73        v.collect::<Option<Vec<_>>>().map(Self)
74    }
75
76    /// Validates the provided chunk coordinates for the array.
77    ///
78    /// This function checks if the provided chunk indices are valid for the array.
79    ///
80    /// # Arguments
81    ///
82    /// * `coord` - The chunk indices to validate.
83    ///
84    /// # Returns
85    ///
86    /// An bool indicating whether the chunk coordinates are valid.
87    ///
88    /// # Errors
89    ///
90    /// Returns false if the chunk coordinates are invalid.
91    pub fn valid_chunk_coord(&self, coord: &ChunkIndices) -> bool {
92        coord
93            .0
94            .iter()
95            .zip(self.num_chunks())
96            .all(|(index, index_permitted)| *index <= (index_permitted.max(1) - 1))
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub enum DimensionName {
102    NotSpecified,
103    Name(String),
104}
105
106impl From<Option<&str>> for DimensionName {
107    fn from(value: Option<&str>) -> Self {
108        match value {
109            Some(s) => s.into(),
110            None => DimensionName::NotSpecified,
111        }
112    }
113}
114
115impl From<DimensionName> for Option<String> {
116    fn from(value: DimensionName) -> Option<String> {
117        match value {
118            DimensionName::NotSpecified => None,
119            DimensionName::Name(name) => Some(name),
120        }
121    }
122}
123
124impl From<&str> for DimensionName {
125    fn from(value: &str) -> Self {
126        if value.is_empty() {
127            DimensionName::NotSpecified
128        } else {
129            DimensionName::Name(value.to_string())
130        }
131    }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub enum NodeData {
136    Array {
137        shape: ArrayShape,
138        dimension_names: Option<Vec<DimensionName>>,
139        manifests: Vec<ManifestRef>,
140    },
141    Group,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct NodeSnapshot {
146    pub id: NodeId,
147    pub path: Path,
148    pub user_data: Bytes,
149    pub node_data: NodeData,
150}
151
152impl NodeSnapshot {
153    pub fn node_type(&self) -> NodeType {
154        match &self.node_data {
155            NodeData::Group => NodeType::Group,
156            NodeData::Array { .. } => NodeType::Array,
157        }
158    }
159}
160
161impl From<&generated::ObjectId8> for NodeId {
162    fn from(value: &generated::ObjectId8) -> Self {
163        NodeId::new(value.0)
164    }
165}
166
167impl From<&generated::ObjectId12> for ManifestId {
168    fn from(value: &generated::ObjectId12) -> Self {
169        ManifestId::new(value.0)
170    }
171}
172
173impl From<&generated::ObjectId12> for AttributesId {
174    fn from(value: &generated::ObjectId12) -> Self {
175        AttributesId::new(value.0)
176    }
177}
178
179impl<'a> From<generated::ManifestRef<'a>> for ManifestRef {
180    fn from(value: generated::ManifestRef<'a>) -> Self {
181        let extents = ManifestExtents::from_ranges_iter(
182            value
183                .extents()
184                .iter()
185                .map(|range| Range { start: range.from(), end: range.to() }),
186        );
187        ManifestRef { object_id: value.object_id().into(), extents }
188    }
189}
190
191impl TryFrom<&generated::DimensionShape> for DimensionShape {
192    type Error = IcechunkFormatError;
193
194    fn try_from(value: &generated::DimensionShape) -> Result<Self, Self::Error> {
195        if value.chunk_length() == 0 && value.array_length() != 0 {
196            return Err(IcechunkFormatErrorKind::InvalidArrayMetadata(format!(
197                "Array metadata has chunk_length = 0 while array_length={:?}",
198                value.array_length()
199            )))
200            .capture();
201        }
202        let num_chunks = if value.chunk_length() == 0 {
203            0
204        } else {
205            value.array_length().div_ceil(value.chunk_length()) as u32
206        };
207        Ok(DimensionShape { dim_length: value.array_length(), num_chunks })
208    }
209}
210
211impl<'a> From<&generated::DimensionShapeV2<'a>> for DimensionShape {
212    fn from(value: &generated::DimensionShapeV2<'a>) -> Self {
213        DimensionShape {
214            dim_length: value.array_length(),
215            num_chunks: value.num_chunks(),
216        }
217    }
218}
219
220impl<'a> TryFrom<generated::ArrayNodeData<'a>> for NodeData {
221    type Error = IcechunkFormatError;
222
223    fn try_from(value: generated::ArrayNodeData<'a>) -> Result<Self, Self::Error> {
224        let dimension_names = value
225            .dimension_names()
226            .map(|dn| dn.iter().map(|name| name.name().into()).collect());
227        // In our flatbuffers the V1 `shape` is required and will be an empty `[]` for V2 repos
228        // Note that `shape` is *also* `[]` for scalars in V1 repos.
229        // So we branch on `shape_v2` which is optional, and thus None, on V1 repos.
230        let shape = ArrayShape(match value.shape_v2() {
231            None => value
232                .shape()
233                .iter()
234                .map(|dim| dim.try_into())
235                .collect::<Result<Vec<_>, IcechunkFormatError>>()?,
236            Some(x) => x.iter().map(|dim| (&dim).into()).collect(),
237        });
238        let manifests = value.manifests().iter().map(|m| m.into()).collect();
239        Ok(Self::Array { shape, dimension_names, manifests })
240    }
241}
242
243impl<'a> From<generated::GroupNodeData<'a>> for NodeData {
244    fn from(_: generated::GroupNodeData<'a>) -> Self {
245        Self::Group
246    }
247}
248
249impl<'a> TryFrom<generated::NodeSnapshot<'a>> for NodeSnapshot {
250    type Error = IcechunkFormatError;
251
252    fn try_from(value: generated::NodeSnapshot<'a>) -> Result<Self, Self::Error> {
253        #[expect(clippy::expect_used, clippy::panic)]
254        let node_data: NodeData = match value.node_data_type() {
255            generated::NodeData::Array => value
256                .node_data_as_array()
257                .expect("Bug in flatbuffers library")
258                .try_into()?,
259            generated::NodeData::Group => {
260                value.node_data_as_group().expect("Bug in flatbuffers library").into()
261            }
262            x => panic!("Invalid node data type in flatbuffers file {x:?}"),
263        };
264        let res = NodeSnapshot {
265            id: value.id().into(),
266            path: Path::from_trusted(value.path()),
267            node_data,
268            user_data: Bytes::copy_from_slice(value.user_data().bytes()),
269        };
270        Ok(res)
271    }
272}
273
274impl From<&generated::ManifestFileInfo> for ManifestFileInfo {
275    fn from(value: &generated::ManifestFileInfo) -> Self {
276        Self {
277            id: value.id().into(),
278            size_bytes: value.size_bytes(),
279            num_chunk_refs: value.num_chunk_refs(),
280        }
281    }
282}
283
284impl TryFrom<&generated::ManifestFileInfoV2<'_>> for ManifestFileInfo {
285    type Error = IcechunkFormatError;
286
287    fn try_from(value: &generated::ManifestFileInfoV2<'_>) -> Result<Self, Self::Error> {
288        let id = value.id().map(|id| id.into()).ok_or_else(|| {
289            IcechunkFormatError::capture(IcechunkFormatErrorKind::InvalidFlatBuffer(
290                flatbuffers::InvalidFlatbuffer::MissingRequiredField {
291                    required: Cow::Borrowed("id"),
292                    error_trace: Default::default(),
293                },
294            ))
295        })?;
296        Ok(Self {
297            id,
298            size_bytes: value.size_bytes(),
299            num_chunk_refs: value.num_chunk_refs(),
300        })
301    }
302}
303
304pub type SnapshotProperties = BTreeMap<String, Value>;
305
306/// Insert a key-value pair into the `__icechunk` namespace object within snapshot properties.
307/// Creates the `__icechunk` object if it doesn't exist.
308pub fn inject_icechunk_metadata(
309    properties: &mut SnapshotProperties,
310    key: &str,
311    value: Value,
312) {
313    match properties.get_mut("__icechunk") {
314        Some(Value::Object(map)) => {
315            map.insert(key.to_string(), value);
316        }
317        _ => {
318            properties
319                .insert("__icechunk".to_string(), serde_json::json!({ key: value }));
320        }
321    }
322}
323
324#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Eq, Hash)]
325pub struct ManifestFileInfo {
326    pub id: ManifestId,
327    pub size_bytes: u64,
328    pub num_chunk_refs: u32,
329}
330
331impl ManifestFileInfo {
332    pub fn new(manifest: &Manifest, size_bytes: u64) -> Self {
333        Self {
334            id: manifest.id().clone(),
335            num_chunk_refs: manifest.len() as u32,
336            size_bytes,
337        }
338    }
339}
340
341// It is _extremely_ common to set/get chunks in large batches sequentially.
342// Without a cache we incur the cost of repeatedly decoding the flatbuffer.
343// This cache is very small (size 2 currently), because we could cache a large number of
344// snapshots. The size of 2 is fine for typically workloads that iterate sequentially
345// through nodes.
346// We cannot choose 1 because of a bug in the `quick_cache` dependency
347// where a size-1 cache is effectively no cache
348// https://github.com/arthurprs/quick-cache/issues/105
349const SNAPSHOT_NODE_CACHE_SIZE: usize = 2;
350
351pub struct Snapshot {
352    buffer: Vec<u8>,
353    spec_version: SpecVersionBin,
354    node_cache: Cache<Path, Arc<NodeSnapshot>>,
355}
356
357impl std::fmt::Debug for Snapshot {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        let nodes =
360            self.iter().map(|n| n.map(|n| n.path.to_string())).collect::<Vec<_>>();
361        #[expect(deprecated)]
362        f.debug_struct("Snapshot")
363            .field("id", &self.id())
364            .field("parent_id", &self.parent_id())
365            .field("flushed_at", &self.flushed_at())
366            .field("nodes", &nodes)
367            .field(
368                "manifests",
369                &self.manifest_files().collect::<IcechunkResult<Vec<_>>>(),
370            )
371            .field("message", &self.message())
372            .field("metadata", &self.metadata())
373            .finish_non_exhaustive()
374    }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Hash)]
378pub struct SnapshotInfo {
379    pub id: SnapshotId,
380    pub parent_id: Option<SnapshotId>,
381    pub flushed_at: DateTime<Utc>,
382    pub message: String,
383    pub metadata: SnapshotProperties,
384    /// Tx-log ids of ancestor commits expiration removed, oldest first.
385    /// Empty unless this snapshot was edited by expiration.
386    /// Notice: icechunk V1 and older versions of icechunk V2
387    /// didn't generate this field, so those repositories will
388    /// have [] until expiration is ran using a version > 2.0.6
389    pub pruned_ancestor_tx_logs: Vec<SnapshotId>,
390}
391
392impl SnapshotInfo {
393    /// Partial constructor: the snapshot file doesn't store
394    /// `pruned_ancestor_tx_logs` (it lives on `RepoInfo`), so it's left empty.
395    /// Only valid for V1 or fresh snapshots; V2 reads must use
396    /// `RepoInfo::find_snapshot` instead.
397    pub fn from_snapshot_file(snapshot: &Snapshot) -> Result<Self, IcechunkFormatError> {
398        #[expect(deprecated)]
399        Ok(Self {
400            id: snapshot.id().clone(),
401            parent_id: snapshot.parent_id().clone(),
402            flushed_at: snapshot.flushed_at()?,
403            message: snapshot.message().clone(),
404            metadata: snapshot.metadata()?.clone(),
405            // Left empty on purpose: not stored in the snapshot file.
406            pruned_ancestor_tx_logs: Vec::new(),
407        })
408    }
409
410    pub fn is_initial(&self) -> bool {
411        self.id == Snapshot::INITIAL_SNAPSHOT_ID
412    }
413}
414
415impl SnapshotId {
416    pub fn is_initial(&self) -> bool {
417        *self == Snapshot::INITIAL_SNAPSHOT_ID
418    }
419}
420
421static ROOT_OPTIONS: VerifierOptions = VerifierOptions {
422    max_depth: 64,
423    max_tables: 50_000_000,
424    // Large enough that any buffer the builder can produce is readable: the
425    // builder refuses to grow past FLATBUFFERS_MAX_BUFFER_SIZE, and the
426    // verifier's counter runs at most MAX_APPARENT_SIZE_INFLATION times the
427    // buffer it walks. Saturating because the product doesn't fit a 32 bit
428    // usize (wasm32), where it lands on usize::MAX instead.
429    max_apparent_size: crate::serializers::MAX_APPARENT_SIZE_INFLATION
430        .saturating_mul(flatbuffers::FLATBUFFERS_MAX_BUFFER_SIZE),
431    ignore_missing_null_terminator: true,
432};
433
434/// The verifier limits applied to every snapshot buffer we read.
435#[cfg(test)]
436pub(crate) fn root_options() -> &'static VerifierOptions {
437    &ROOT_OPTIONS
438}
439
440impl Snapshot {
441    pub const INITIAL_COMMIT_MESSAGE: &'static str = "Repository initialized";
442    pub const INITIAL_SNAPSHOT_ID: SnapshotId = SnapshotId::new([
443        0x0b, 0x1c, 0xc8, 0xd6, 0x78, 0x75, 0x80, 0xf0, 0xe3, 0x3a, 0x65,
444        0x34, // Decodes as 1CECHNKREP0F1RSTCMT0
445    ]);
446
447    /// Check that `buffer` passes every check [`Snapshot::from_buffer`] applies.
448    pub fn verify_buffer(buffer: &[u8]) -> IcechunkResult<()> {
449        let _ =
450            flatbuffers::root_with_opts::<generated::Snapshot<'_>>(&ROOT_OPTIONS, buffer)
451                .capture()?;
452        Ok(())
453    }
454
455    pub fn from_buffer(
456        spec_version: SpecVersionBin,
457        buffer: Vec<u8>,
458    ) -> IcechunkResult<Snapshot> {
459        Self::verify_buffer(buffer.as_slice())?;
460        Ok(Snapshot {
461            buffer,
462            spec_version,
463            // this number is low because we cache a very large number of snapshot nodes.
464            node_cache: Cache::new(SNAPSHOT_NODE_CACHE_SIZE),
465        })
466    }
467
468    pub fn bytes(&self) -> &[u8] {
469        self.buffer.as_slice()
470    }
471
472    #[expect(clippy::too_many_arguments)]
473    pub fn from_iter<I>(
474        id: Option<SnapshotId>,
475        parent_id: Option<SnapshotId>,
476        spec_version: SpecVersionBin,
477        message: &str,
478        properties: Option<SnapshotProperties>,
479        mut manifest_files: Vec<ManifestFileInfo>,
480        flushed_at: Option<DateTime<Utc>>,
481        sorted_iter: I,
482    ) -> IcechunkResult<Self>
483    where
484        I: IntoIterator<Item = IcechunkResult<NodeSnapshot>>,
485    {
486        // TODO: what's a good capacity?
487        let mut builder = FlatBufferBuilder::with_capacity(4_096);
488
489        manifest_files.sort_by(|a, b| a.id.cmp(&b.id));
490        let (manifest_files_v1, manifest_files_v2) = match spec_version {
491            SpecVersionBin::V1 => {
492                let ms1 = manifest_files
493                    .iter()
494                    .map(|mfi| {
495                        let id = generated::ObjectId12::new(&mfi.id.0);
496                        generated::ManifestFileInfo::new(
497                            &id,
498                            mfi.size_bytes,
499                            mfi.num_chunk_refs,
500                        )
501                    })
502                    .collect::<Vec<_>>();
503                let ms1 = builder.create_vector(&ms1);
504                let ms2 = None;
505                (ms1, ms2)
506            }
507            SpecVersionBin::V2 => {
508                let ms2 = manifest_files
509                    .iter()
510                    .map(|mfi| {
511                        let id = generated::ObjectId12::new(&mfi.id.0);
512
513                        let args = generated::ManifestFileInfoV2Args {
514                            id: Some(&id),
515                            size_bytes: mfi.size_bytes,
516                            num_chunk_refs: mfi.num_chunk_refs,
517                            extra: None,
518                        };
519                        generated::ManifestFileInfoV2::create(&mut builder, &args)
520                    })
521                    .collect::<Vec<_>>();
522                let ms2 = builder.create_vector(&ms2);
523                let ms1 = builder.create_vector::<generated::ManifestFileInfo>(&[]);
524                (ms1, Some(ms2))
525            }
526        };
527
528        let metadata_items: Vec<_> = properties
529            .unwrap_or_default()
530            .iter()
531            .map(|(k, v)| {
532                let name = builder.create_shared_string(k.as_str());
533                let serialized = if spec_version == SpecVersionBin::V1 {
534                    rmp_serde::to_vec(v).map_err(Box::new).capture()?
535                } else {
536                    flexbuffers::to_vec(v).map_err(Box::new).capture()?
537                };
538
539                let value = builder.create_vector(serialized.as_slice());
540                Ok::<_, IcechunkFormatError>(generated::MetadataItem::create(
541                    &mut builder,
542                    &generated::MetadataItemArgs { name: Some(name), value: Some(value) },
543                ))
544            })
545            .try_collect()?;
546        let metadata_items = builder.create_vector(metadata_items.as_slice());
547
548        let message = builder.create_string(message);
549        // Icechunk 2.0 no longer uses this field
550        let parent_id = parent_id.map(|oid| generated::ObjectId12::new(&oid.0));
551        let flushed_at = flushed_at.unwrap_or_else(Utc::now).timestamp_micros() as u64;
552        let id = generated::ObjectId12::new(&id.unwrap_or_else(SnapshotId::random).0);
553
554        let nodes: Vec<_> = sorted_iter
555            .into_iter()
556            .map(|node| node.and_then(|node| mk_node(&mut builder, &node, spec_version)))
557            .try_collect()?;
558        let nodes = builder.create_vector(&nodes);
559
560        let snap = generated::Snapshot::create(
561            &mut builder,
562            &generated::SnapshotArgs {
563                id: Some(&id),
564                parent_id: parent_id.as_ref(),
565                nodes: Some(nodes),
566                flushed_at,
567                message: Some(message),
568                metadata: Some(metadata_items),
569                manifest_files: Some(manifest_files_v1),
570                manifest_files_v2,
571                ..Default::default()
572            },
573        );
574
575        builder.finish(snap, Some("Ichk"));
576        let (mut buffer, offset) = builder.collapse();
577        buffer.drain(0..offset);
578        buffer.shrink_to_fit();
579        Ok(Snapshot {
580            buffer,
581            spec_version,
582            // this number is low because we cache a very large number of snapshot nodes.
583            node_cache: Cache::new(SNAPSHOT_NODE_CACHE_SIZE),
584        })
585    }
586
587    pub fn initial(spec_version: SpecVersionBin) -> IcechunkResult<Self> {
588        let mut properties = SnapshotProperties::default();
589        inject_icechunk_metadata(&mut properties, "is_root", Value::from(true));
590        let nodes = Vec::<IcechunkResult<NodeSnapshot>>::new();
591        Self::from_iter(
592            Some(Self::INITIAL_SNAPSHOT_ID),
593            None,
594            spec_version,
595            Self::INITIAL_COMMIT_MESSAGE,
596            Some(properties),
597            Default::default(),
598            None,
599            nodes,
600        )
601    }
602
603    #[expect(unsafe_code)]
604    fn root(&self) -> generated::Snapshot<'_> {
605        // SAFETY: self.buffer was serialized by our own flatbuffers serialization code.
606        // We skip validation for performance; a corrupt buffer here indicates
607        // file corruption or a bad Icechunk implementation, not a caller error.
608        unsafe { flatbuffers::root_unchecked::<generated::Snapshot<'_>>(&self.buffer) }
609    }
610
611    pub fn id(&self) -> SnapshotId {
612        SnapshotId::new(self.root().id().0)
613    }
614
615    #[deprecated(
616        since = "2.0.0",
617        note = "New versions of icechunk don't use this field and initialize it to None"
618    )]
619    pub fn parent_id(&self) -> Option<SnapshotId> {
620        self.root().parent_id().map(|pid| SnapshotId::new(pid.0))
621    }
622
623    pub fn metadata(&self) -> IcechunkResult<SnapshotProperties> {
624        self.root()
625            .metadata()
626            .iter()
627            .map(|item| {
628                let key = item.name().to_string();
629                let value = if self.spec_version == SpecVersionBin::V1 {
630                    rmp_serde::from_slice(item.value().bytes())
631                        .map_err(Box::new)
632                        .capture()?
633                } else {
634                    flexbuffers::from_slice(item.value().bytes())
635                        .map_err(Box::new)
636                        .capture()?
637                };
638                Ok((key, value))
639            })
640            .try_collect()
641    }
642
643    pub fn flushed_at(&self) -> IcechunkResult<DateTime<Utc>> {
644        let ts = self.root().flushed_at();
645        let ts: i64 = ts
646            .try_into()
647            .map_err(|_| IcechunkFormatErrorKind::InvalidTimestamp)
648            .capture()?;
649        DateTime::from_timestamp_micros(ts)
650            .ok_or(IcechunkFormatErrorKind::InvalidTimestamp)
651            .capture()
652    }
653
654    pub fn message(&self) -> String {
655        self.root().message().to_string()
656    }
657
658    pub fn manifest_files(
659        &self,
660    ) -> impl Iterator<Item = IcechunkResult<ManifestFileInfo>> + '_ {
661        let root = self.root();
662        if let Some(mf2) = root.manifest_files_v2() {
663            Either::Left(mf2.iter().map(|mf| (&mf).try_into()))
664        } else {
665            Either::Right(root.manifest_files().iter().map(|mf| Ok(mf.into())))
666        }
667    }
668
669    /// Cretase a new `Snapshot` with all the same data as `new_child` but `self` as parent
670    #[deprecated(
671        since = "2.0.0",
672        note = "Shouldn't be necessary after 2.0, only to support Icechunk 1 repos"
673    )]
674    pub fn adopt(&self, new_child: &Snapshot) -> IcechunkResult<Self> {
675        // Rust flatbuffers implementation doesn't allow mutation of scalars, so we need to
676        // create a whole new buffer and write to it in full
677
678        Snapshot::from_iter(
679            Some(new_child.id()),
680            Some(self.id()),
681            SpecVersionBin::V1, // 2.0 doesn't use adopt
682            &new_child.message(),
683            Some(new_child.metadata()?.clone()),
684            new_child.manifest_files().try_collect()?,
685            Some(new_child.flushed_at()?),
686            new_child.iter(),
687        )
688    }
689
690    fn _get_node(&self, path: &Path) -> IcechunkResult<NodeSnapshot> {
691        let res = self
692            .root()
693            .nodes()
694            .lookup_by_key(path.to_string().as_str(), |node, path| node.path().cmp(path))
695            .ok_or_else(|| IcechunkFormatErrorKind::NodeNotFound { path: path.clone() })
696            .capture()?;
697        res.try_into()
698    }
699
700    pub fn get_node(&self, path: &Path) -> IcechunkResult<Arc<NodeSnapshot>> {
701        use GuardResult::*;
702        match self.node_cache.get_value_or_guard(path, None) {
703            Value(node) => Ok(node),
704            Timeout => Ok(Arc::new(self._get_node(path)?)),
705            Guard(guard) => {
706                let node = self._get_node(path)?;
707                let node = Arc::new(node);
708                let _ = guard.insert(Arc::clone(&node));
709                Ok(node)
710            }
711        }
712    }
713
714    pub fn get_node_index(&self, path: &Path) -> IcechunkResult<usize> {
715        let path_str = path.to_string();
716        let res =
717            lookup_index_by_key(self.root().nodes(), path_str.as_str(), |node, path| {
718                node.path().cmp(path)
719            })
720            .ok_or_else(|| IcechunkFormatErrorKind::NodeNotFound { path: path.clone() })
721            .capture()?;
722        Ok(res)
723    }
724
725    pub fn iter(&self) -> impl Iterator<Item = IcechunkResult<NodeSnapshot>> + '_ {
726        self.root().nodes().iter().map(|node| node.try_into().inject())
727    }
728
729    pub fn iter_arc(
730        self: Arc<Self>,
731        parent_group: &Path,
732    ) -> impl Iterator<Item = IcechunkResult<NodeSnapshot>> + use<> {
733        NodeIterator::new(self, parent_group)
734    }
735
736    pub fn len(&self) -> usize {
737        self.root().nodes().len()
738    }
739
740    #[must_use]
741    pub fn is_empty(&self) -> bool {
742        self.len() == 0
743    }
744
745    pub fn manifest_info(
746        &self,
747        id: &ManifestId,
748    ) -> IcechunkResult<Option<ManifestFileInfo>> {
749        let root = self.root();
750        // Both manifest vectors are sorted by id in from_iter, as required by snapshot.fbs.
751        if let Some(mf2) = root.manifest_files_v2() {
752            lookup_first_manifest_index(mf2.len(), |index| {
753                mf2.get(index).id().map(|mid| mid.0).cmp(&Some(id.0))
754            })
755            .map(|index| (&mf2.get(index)).try_into())
756            .transpose()
757        } else {
758            let mf1 = root.manifest_files();
759            Ok(lookup_first_manifest_index(mf1.len(), |index| {
760                mf1.get(index).id().0.cmp(&id.0)
761            })
762            .map(|index| mf1.get(index).into()))
763        }
764    }
765}
766
767// Return the first matching entry when multiple manifests have the same ID.
768// Continuing left after a match keeps even an all-equal vector logarithmic.
769fn lookup_first_manifest_index(
770    len: usize,
771    compare: impl Fn(usize) -> Ordering,
772) -> Option<usize> {
773    let (mut left, mut right) = (0, len);
774    let mut found = None;
775    while left < right {
776        let mid = left + (right - left) / 2;
777        match compare(mid) {
778            Ordering::Less => left = mid + 1,
779            Ordering::Equal => {
780                found = Some(mid);
781                right = mid;
782            }
783            Ordering::Greater => right = mid,
784        }
785    }
786    found
787}
788
789struct NodeIterator {
790    snapshot: Arc<Snapshot>,
791    next_index: usize,
792    prefix: String,
793}
794
795impl NodeIterator {
796    fn new(snapshot: Arc<Snapshot>, parent_group: &Path) -> Self {
797        let next_index = snapshot.get_node_index(parent_group).unwrap_or_default();
798        let prefix = parent_group.to_string();
799        let prefix = if prefix == "/" { String::new() } else { prefix };
800        NodeIterator { snapshot, next_index, prefix }
801    }
802}
803
804impl Iterator for NodeIterator {
805    type Item = IcechunkResult<NodeSnapshot>;
806
807    fn next(&mut self) -> Option<Self::Item> {
808        let nodes = self.snapshot.root().nodes();
809        loop {
810            // we need to loop over all nodes starting from the index
811            // until we are sure we'll see no more children
812            if self.next_index >= nodes.len() {
813                return None;
814            }
815
816            let node: IcechunkResult<NodeSnapshot> =
817                nodes.get(self.next_index).try_into();
818
819            match node {
820                Ok(res) => {
821                    let node_path = res.path.to_string();
822                    if let Some(after_prefix) =
823                        node_path.strip_prefix(self.prefix.as_str())
824                        && (after_prefix.is_empty() || after_prefix.starts_with('/'))
825                    {
826                        self.next_index += 1;
827                        return Some(Ok(res));
828                    } else if node_path.as_str() > self.prefix.as_str()
829                        && !node_path.starts_with(self.prefix.as_str())
830                    {
831                        // We've passed all possible children of the prefix
832                        return None;
833                    } else {
834                        // Not a match but there may be matches later (foo-bar" comes before "foo/bar")
835                        self.next_index += 1;
836                    }
837                }
838                Err(err) => return Some(Err(err)),
839            }
840        }
841    }
842}
843
844fn mk_node<'bldr>(
845    builder: &mut FlatBufferBuilder<'bldr>,
846    node: &NodeSnapshot,
847    spec_version: SpecVersionBin,
848) -> IcechunkResult<WIPOffset<generated::NodeSnapshot<'bldr>>> {
849    let id = generated::ObjectId8::new(&node.id.0);
850    let path = builder.create_string(node.path.to_string().as_str());
851    let (node_data_type, node_data) =
852        mk_node_data(builder, &node.node_data, spec_version)?;
853    let user_data = Some(builder.create_vector(&node.user_data));
854    Ok(generated::NodeSnapshot::create(
855        builder,
856        &generated::NodeSnapshotArgs {
857            id: Some(&id),
858            path: Some(path),
859            node_data_type,
860            node_data,
861            user_data,
862            ..Default::default()
863        },
864    ))
865}
866
867type ShapeV1<'a> = WIPOffset<Vector<'a, generated::DimensionShape>>;
868type ShapeV2<'a> =
869    WIPOffset<Vector<'a, ForwardsUOffset<generated::DimensionShapeV2<'a>>>>;
870
871fn mk_array_shapes<'a>(
872    builder: &mut FlatBufferBuilder<'a>,
873    spec_version: SpecVersionBin,
874    shape: &ArrayShape,
875) -> (Option<ShapeV1<'a>>, Option<ShapeV2<'a>>) {
876    use SpecVersionBin::*;
877    match spec_version {
878        V1 => {
879            let shape = shape
880                .0
881                .iter()
882                .map(|ds| {
883                    let chunk_length = if ds.num_chunks == 0 {
884                        debug_assert_eq!(ds.dim_length, 0);
885                        0
886                    } else {
887                        // FIXME: this can be *very* wrong, but I'm not sure it really matters
888                        //        we still preserve num_chunks, and return the proper Zarr JSON metadata
889                        //        which is stored in NodeData::user_data which stores the zarr.json exactly
890                        //        as provided by Zarr
891                        ds.dim_length.div_ceil(ds.num_chunks as u64)
892                    };
893
894                    generated::DimensionShape::new(ds.dim_length, chunk_length)
895                })
896                .collect::<Vec<_>>();
897            (Some(builder.create_vector(shape.as_slice())), None)
898        }
899        V2 => {
900            let shape = shape
901                .0
902                .iter()
903                .map(|ds| {
904                    generated::DimensionShapeV2::create(
905                        builder,
906                        &generated::DimensionShapeV2Args {
907                            array_length: ds.dim_length,
908                            num_chunks: ds.num_chunks,
909                        },
910                    )
911                })
912                .collect::<Vec<_>>();
913            let empty_shape_for_v1_compat =
914                builder.create_vector(&[] as &[generated::DimensionShape]);
915            (
916                Some(empty_shape_for_v1_compat),
917                Some(builder.create_vector(shape.as_slice())),
918            )
919        }
920    }
921}
922
923fn mk_node_data(
924    builder: &mut FlatBufferBuilder<'_>,
925    node_data: &NodeData,
926    spec_version: SpecVersionBin,
927) -> IcechunkResult<(generated::NodeData, Option<WIPOffset<UnionWIPOffset>>)> {
928    match node_data {
929        NodeData::Array { manifests, dimension_names, shape } => {
930            let manifests = manifests
931                .iter()
932                .map(|manref| {
933                    let object_id = generated::ObjectId12::new(&manref.object_id.0);
934                    let extents = manref
935                        .extents
936                        .iter()
937                        .map(|range| {
938                            generated::ChunkIndexRange::new(range.start, range.end)
939                        })
940                        .collect::<Vec<_>>();
941                    let extents = builder.create_vector(&extents);
942                    generated::ManifestRef::create(
943                        builder,
944                        &generated::ManifestRefArgs {
945                            object_id: Some(&object_id),
946                            extents: Some(extents),
947                        },
948                    )
949                })
950                .collect::<Vec<_>>();
951            let manifests = builder.create_vector(manifests.as_slice());
952            let dimensions = dimension_names.as_ref().map(|dn| {
953                let names = dn
954                    .iter()
955                    .map(|n| match n {
956                        DimensionName::Name(s) => {
957                            let n = builder.create_shared_string(s.as_str());
958                            generated::DimensionName::create(
959                                builder,
960                                &generated::DimensionNameArgs { name: Some(n) },
961                            )
962                        }
963                        DimensionName::NotSpecified => generated::DimensionName::create(
964                            builder,
965                            &generated::DimensionNameArgs { name: None },
966                        ),
967                    })
968                    .collect::<Vec<_>>();
969                builder.create_vector(names.as_slice())
970            });
971            let (shape_v1, shape_v2) = mk_array_shapes(builder, spec_version, shape);
972            let node_data = generated::ArrayNodeData::create(
973                builder,
974                &generated::ArrayNodeDataArgs {
975                    manifests: Some(manifests),
976                    shape: shape_v1,
977                    shape_v2,
978                    dimension_names: dimensions,
979                },
980            );
981            Ok((generated::NodeData::Array, Some(node_data.as_union_value())))
982        }
983        NodeData::Group => Ok((
984            generated::NodeData::Group,
985            Some(
986                generated::GroupNodeData::create(
987                    builder,
988                    &generated::GroupNodeDataArgs {},
989                )
990                .as_union_value(),
991            ),
992        )),
993    }
994}
995
996#[cfg(test)]
997#[expect(unused_qualifications)] // proptest macros generate fully qualified paths
998mod tests {
999    use crate::{IcechunkFormatError, ObjectId};
1000
1001    use super::*;
1002    use crate::{
1003        roundtrip_serialization_tests,
1004        strategies::{ShapeDim, manifest_file_info, node_snapshot, shapes_and_dims},
1005    };
1006    use pretty_assertions::assert_eq;
1007    use proptest::prelude::*;
1008    use std::iter::{self};
1009
1010    roundtrip_serialization_tests!(
1011        serialize_and_deserialize_node_snapshot - node_snapshot,
1012        serialize_and_deserialize_manifest_file_info - manifest_file_info
1013    );
1014
1015    #[icechunk_macros::test]
1016    fn test_manifest_info() -> IcechunkResult<()> {
1017        let manifest_id = |value: u32| {
1018            let mut bytes = [0; 12];
1019            bytes[8..].copy_from_slice(&value.to_be_bytes());
1020            ManifestId::new(bytes)
1021        };
1022
1023        for spec_version in [SpecVersionBin::V1, SpecVersionBin::V2] {
1024            for count in [0, 1, 2, 3, 5, 4096] {
1025                // Reverse the input to exercise snapshot construction's ID sorting.
1026                let manifests: Vec<_> = (1..=count)
1027                    .rev()
1028                    .map(|i| ManifestFileInfo {
1029                        id: manifest_id(2 * i),
1030                        size_bytes: u64::from(i) * 100,
1031                        num_chunk_refs: i,
1032                    })
1033                    .collect();
1034                let snapshot = Snapshot::from_iter(
1035                    None,
1036                    None,
1037                    spec_version,
1038                    "",
1039                    None,
1040                    manifests.clone(),
1041                    None,
1042                    iter::empty(),
1043                )?;
1044
1045                for manifest in manifests {
1046                    assert_eq!(snapshot.manifest_info(&manifest.id)?, Some(manifest));
1047                }
1048                // Misses before, between, and after the stored IDs.
1049                assert_eq!(snapshot.manifest_info(&manifest_id(0))?, None);
1050                for i in 0..=count {
1051                    assert_eq!(snapshot.manifest_info(&manifest_id(2 * i + 1))?, None);
1052                }
1053            }
1054        }
1055        Ok(())
1056    }
1057
1058    #[icechunk_macros::test]
1059    fn test_manifest_info_returns_first_duplicate() -> IcechunkResult<()> {
1060        for spec_version in [SpecVersionBin::V1, SpecVersionBin::V2] {
1061            for ids in
1062                [vec![2, 2, 2], vec![2, 2, 4], vec![2, 4, 4, 4, 6], vec![2, 4, 4, 4]]
1063            {
1064                // Distinct metadata makes selection among equal IDs observable.
1065                // Reverse the IDs to also exercise the constructor's stable sort.
1066                let manifests: Vec<_> = ids
1067                    .into_iter()
1068                    .rev()
1069                    .zip(1u32..)
1070                    .map(|(id, i)| ManifestFileInfo {
1071                        id: ManifestId::new([id; 12]),
1072                        size_bytes: u64::from(i) * 100,
1073                        num_chunk_refs: i,
1074                    })
1075                    .collect();
1076                let snapshot = Snapshot::from_iter(
1077                    None,
1078                    None,
1079                    spec_version,
1080                    "",
1081                    None,
1082                    manifests.clone(),
1083                    None,
1084                    iter::empty(),
1085                )?;
1086                let loaded =
1087                    Snapshot::from_buffer(spec_version, snapshot.bytes().to_vec())?;
1088
1089                for snapshot in [&snapshot, &loaded] {
1090                    for id in 0..=7 {
1091                        let id = ManifestId::new([id; 12]);
1092                        let expected = manifests.iter().find(|mf| mf.id == id).cloned();
1093                        assert_eq!(snapshot.manifest_info(&id)?, expected);
1094                    }
1095                }
1096            }
1097        }
1098        Ok(())
1099    }
1100
1101    #[icechunk_macros::test]
1102    fn test_get_node() -> Result<(), Box<dyn std::error::Error>> {
1103        let shape1 = ArrayShape::new(vec![(10u64, 3), (20, 2), (30, 1)]).unwrap();
1104        let dim_names1 = Some(vec!["x".into(), "y".into(), "t".into()]);
1105
1106        let shape2 = shape1.clone();
1107        let dim_names2 = Some(vec![
1108            DimensionName::NotSpecified,
1109            DimensionName::NotSpecified,
1110            "t".into(),
1111        ]);
1112
1113        let shape3 = shape1.clone();
1114        let dim_names3 = None;
1115
1116        let man_ref1 = ManifestRef {
1117            object_id: ObjectId::random(),
1118            extents: ManifestExtents::new(&[0, 0, 0], &[100, 100, 100]),
1119        };
1120        let man_ref2 = ManifestRef {
1121            object_id: ObjectId::random(),
1122            extents: ManifestExtents::new(&[0, 0, 0], &[100, 100, 100]),
1123        };
1124
1125        let node_ids = iter::repeat_with(NodeId::random).take(7).collect::<Vec<_>>();
1126        // nodes must be sorted by path
1127        let nodes = vec![
1128            NodeSnapshot {
1129                path: Path::root(),
1130                id: node_ids[0].clone(),
1131                user_data: Bytes::new(),
1132                node_data: NodeData::Group,
1133            },
1134            NodeSnapshot {
1135                path: "/a".try_into().unwrap(),
1136                id: node_ids[1].clone(),
1137                user_data: Bytes::new(),
1138                node_data: NodeData::Group,
1139            },
1140            NodeSnapshot {
1141                path: "/array2".try_into().unwrap(),
1142                id: node_ids[5].clone(),
1143                user_data: Bytes::new(),
1144                node_data: NodeData::Array {
1145                    shape: shape2.clone(),
1146                    dimension_names: dim_names2.clone(),
1147                    manifests: vec![],
1148                },
1149            },
1150            NodeSnapshot {
1151                path: "/b".try_into().unwrap(),
1152                id: node_ids[2].clone(),
1153                user_data: Bytes::new(),
1154                node_data: NodeData::Group,
1155            },
1156            NodeSnapshot {
1157                path: "/b/array1".try_into().unwrap(),
1158                id: node_ids[4].clone(),
1159                user_data: Bytes::copy_from_slice(b"hello"),
1160                node_data: NodeData::Array {
1161                    shape: shape1.clone(),
1162                    dimension_names: dim_names1.clone(),
1163                    manifests: vec![man_ref1.clone(), man_ref2.clone()],
1164                },
1165            },
1166            NodeSnapshot {
1167                path: "/b/array3".try_into().unwrap(),
1168                id: node_ids[6].clone(),
1169                user_data: Bytes::new(),
1170                node_data: NodeData::Array {
1171                    shape: shape3.clone(),
1172                    dimension_names: dim_names3.clone(),
1173                    manifests: vec![],
1174                },
1175            },
1176            NodeSnapshot {
1177                path: "/b/c".try_into().unwrap(),
1178                id: node_ids[3].clone(),
1179                user_data: Bytes::copy_from_slice(b"bye"),
1180                node_data: NodeData::Group,
1181            },
1182        ];
1183        let manifests = vec![
1184            ManifestFileInfo {
1185                id: man_ref1.object_id.clone(),
1186                size_bytes: 1_000_000,
1187                num_chunk_refs: 100_000,
1188            },
1189            ManifestFileInfo {
1190                id: man_ref2.object_id.clone(),
1191                size_bytes: 1_000_000,
1192                num_chunk_refs: 100_000,
1193            },
1194        ];
1195        let st = Snapshot::from_iter(
1196            None,
1197            None,
1198            SpecVersionBin::current(),
1199            "",
1200            Default::default(),
1201            manifests,
1202            None,
1203            nodes.into_iter().map(Ok::<NodeSnapshot, IcechunkFormatError>),
1204        )
1205        .unwrap();
1206
1207        assert!(matches!(
1208            st.get_node(&"/nonexistent".try_into().unwrap()),
1209            Err(IcechunkFormatError {
1210                kind: IcechunkFormatErrorKind::NodeNotFound {
1211                    path
1212                },
1213                ..
1214            }) if path == "/nonexistent".try_into().unwrap()
1215        ));
1216
1217        let node = st.get_node(&"/b/c".try_into().unwrap()).unwrap();
1218        assert_eq!(
1219            node,
1220            Arc::new(NodeSnapshot {
1221                path: "/b/c".try_into().unwrap(),
1222                id: node_ids[3].clone(),
1223                user_data: Bytes::copy_from_slice(b"bye"),
1224                node_data: NodeData::Group,
1225            }),
1226        );
1227        let node = st.get_node(&Path::root()).unwrap();
1228        assert_eq!(
1229            node,
1230            Arc::new(NodeSnapshot {
1231                path: Path::root(),
1232                id: node_ids[0].clone(),
1233                user_data: Bytes::new(),
1234                node_data: NodeData::Group,
1235            }),
1236        );
1237        let node = st.get_node(&"/b/array1".try_into().unwrap()).unwrap();
1238        assert_eq!(
1239            node,
1240            Arc::new(NodeSnapshot {
1241                path: "/b/array1".try_into().unwrap(),
1242                id: node_ids[4].clone(),
1243                user_data: Bytes::copy_from_slice(b"hello"),
1244                node_data: NodeData::Array {
1245                    shape: shape1.clone(),
1246                    dimension_names: dim_names1.clone(),
1247                    manifests: vec![man_ref1, man_ref2]
1248                },
1249            }),
1250        );
1251        let node = st.get_node(&"/array2".try_into().unwrap()).unwrap();
1252        assert_eq!(
1253            node,
1254            Arc::new(NodeSnapshot {
1255                path: "/array2".try_into().unwrap(),
1256                id: node_ids[5].clone(),
1257                user_data: Bytes::new(),
1258                node_data: NodeData::Array {
1259                    shape: shape2.clone(),
1260                    dimension_names: dim_names2.clone(),
1261                    manifests: vec![]
1262                },
1263            }),
1264        );
1265        let node = st.get_node(&"/b/array3".try_into().unwrap()).unwrap();
1266        assert_eq!(
1267            node,
1268            Arc::new(NodeSnapshot {
1269                path: "/b/array3".try_into().unwrap(),
1270                id: node_ids[6].clone(),
1271                user_data: Bytes::new(),
1272                node_data: NodeData::Array {
1273                    shape: shape3.clone(),
1274                    dimension_names: dim_names3.clone(),
1275                    manifests: vec![]
1276                },
1277            }),
1278        );
1279        Ok(())
1280    }
1281
1282    #[icechunk_macros::test]
1283    fn test_valid_chunk_coord() {
1284        let shape1 =
1285            ArrayShape::new(vec![(10_000, 10), (10_001, 11), (9_999, 10)]).unwrap();
1286        let shape2 = ArrayShape::new(vec![(0, 1_000), (0, 1_000), (0, 1_000)]).unwrap();
1287        let coord1 = ChunkIndices(vec![9, 10, 9]);
1288        let coord2 = ChunkIndices(vec![10, 11, 10]);
1289        let coord3 = ChunkIndices(vec![0, 0, 0]);
1290
1291        assert!(shape1.valid_chunk_coord(&coord1));
1292        assert!(!shape1.valid_chunk_coord(&coord2));
1293        assert!(shape2.valid_chunk_coord(&coord3));
1294    }
1295
1296    #[icechunk_macros::test]
1297    fn test_valid_chunk_coord_zero_length_dim() {
1298        let shape = ArrayShape::new(vec![(0, 0)]).unwrap();
1299        assert!(shape.valid_chunk_coord(&ChunkIndices(vec![0])));
1300        assert!(!shape.valid_chunk_coord(&ChunkIndices(vec![1])));
1301    }
1302
1303    #[test_strategy::proptest]
1304    fn test_prop_valid_chunk_coord(
1305        #[strategy(shapes_and_dims(None, Some(1)))] shape_dim: ShapeDim,
1306        axis_offset: usize,
1307    ) {
1308        let shape = &shape_dim.shape;
1309        let ndim = shape.len();
1310        let axis = axis_offset % ndim;
1311
1312        // origin is always valid
1313        let origin = ChunkIndices(vec![0; ndim]);
1314        prop_assert!(shape.valid_chunk_coord(&origin));
1315
1316        // max valid coord is in bounds
1317        let max_valid =
1318            ChunkIndices(shape.num_chunks().map(|nc| nc.saturating_sub(1)).collect());
1319        prop_assert!(shape.valid_chunk_coord(&max_valid));
1320
1321        // bumping one axis out of bounds is rejected
1322        let mut oob = max_valid.0.clone();
1323        oob[axis] = shape.iter().nth(axis).unwrap().num_chunks().max(1);
1324        prop_assert!(!shape.valid_chunk_coord(&ChunkIndices(oob)));
1325    }
1326}