1use 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 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 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
306pub 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
341const 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 pub pruned_ancestor_tx_logs: Vec<SnapshotId>,
390}
391
392impl SnapshotInfo {
393 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 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 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#[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, ]);
446
447 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 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 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 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 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 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 #[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 Snapshot::from_iter(
679 Some(new_child.id()),
680 Some(self.id()),
681 SpecVersionBin::V1, &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 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
767fn 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 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 return None;
833 } else {
834 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 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)] mod 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 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 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 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 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 let origin = ChunkIndices(vec![0; ndim]);
1314 prop_assert!(shape.valid_chunk_coord(&origin));
1315
1316 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 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}