1use std::{borrow::Cow, 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
16use crate::{
17 AttributesId, ChunkIndices, IcechunkFormatError, IcechunkFormatErrorKind,
18 IcechunkResult, ManifestId, NodeId, Path, SnapshotId,
19 flatbuffers::generated,
20 format_constants::SpecVersionBin,
21 lookup_index_by_key,
22 manifest::{Manifest, ManifestExtents, ManifestRef},
23};
24use icechunk_types::{ICResultExt as _, error::ICResultCtxExt as _};
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct DimensionShape {
28 dim_length: u64,
29 num_chunks: u32,
30}
31
32impl DimensionShape {
33 pub fn new(array_length: u64, num_chunks: u32) -> Self {
34 Self { dim_length: array_length, num_chunks }
35 }
36 pub fn array_length(&self) -> u64 {
37 self.dim_length
38 }
39 pub fn num_chunks(&self) -> u32 {
40 self.num_chunks
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct ArrayShape(Vec<DimensionShape>);
46
47impl ArrayShape {
48 pub fn len(&self) -> usize {
49 self.0.len()
50 }
51 pub fn is_empty(&self) -> bool {
52 self.0.is_empty()
53 }
54
55 pub fn get(&self, ax: usize) -> Option<DimensionShape> {
56 if ax > self.len() - 1 { None } else { Some(self.0[ax].clone()) }
57 }
58
59 pub fn iter(&self) -> impl Iterator<Item = &DimensionShape> {
60 self.0.iter()
61 }
62
63 pub fn num_chunks(&self) -> impl Iterator<Item = u32> {
64 self.0.iter().map(|x| x.num_chunks())
65 }
66
67 pub fn new<I>(it: I) -> Option<Self>
68 where
69 I: IntoIterator<Item = (u64, u32)>,
70 {
71 let v = it.into_iter().map(|(al, nc)| Some(DimensionShape::new(al, nc)));
72 v.collect::<Option<Vec<_>>>().map(Self)
73 }
74
75 pub fn valid_chunk_coord(&self, coord: &ChunkIndices) -> bool {
91 coord
92 .0
93 .iter()
94 .zip(self.num_chunks())
95 .all(|(index, index_permitted)| *index <= (index_permitted.max(1) - 1))
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub enum DimensionName {
101 NotSpecified,
102 Name(String),
103}
104
105impl From<Option<&str>> for DimensionName {
106 fn from(value: Option<&str>) -> Self {
107 match value {
108 Some(s) => s.into(),
109 None => DimensionName::NotSpecified,
110 }
111 }
112}
113
114impl From<DimensionName> for Option<String> {
115 fn from(value: DimensionName) -> Option<String> {
116 match value {
117 DimensionName::NotSpecified => None,
118 DimensionName::Name(name) => Some(name),
119 }
120 }
121}
122
123impl From<&str> for DimensionName {
124 fn from(value: &str) -> Self {
125 if value.is_empty() {
126 DimensionName::NotSpecified
127 } else {
128 DimensionName::Name(value.to_string())
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub enum NodeData {
135 Array {
136 shape: ArrayShape,
137 dimension_names: Option<Vec<DimensionName>>,
138 manifests: Vec<ManifestRef>,
139 },
140 Group,
141}
142
143#[derive(Clone, Debug, Hash, PartialEq, Eq)]
144pub enum NodeType {
145 Group,
146 Array,
147}
148
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct NodeSnapshot {
151 pub id: NodeId,
152 pub path: Path,
153 pub user_data: Bytes,
154 pub node_data: NodeData,
155}
156
157impl NodeSnapshot {
158 pub fn node_type(&self) -> NodeType {
159 match &self.node_data {
160 NodeData::Group => NodeType::Group,
161 NodeData::Array { .. } => NodeType::Array,
162 }
163 }
164}
165
166impl From<&generated::ObjectId8> for NodeId {
167 fn from(value: &generated::ObjectId8) -> Self {
168 NodeId::new(value.0)
169 }
170}
171
172impl From<&generated::ObjectId12> for ManifestId {
173 fn from(value: &generated::ObjectId12) -> Self {
174 ManifestId::new(value.0)
175 }
176}
177
178impl From<&generated::ObjectId12> for AttributesId {
179 fn from(value: &generated::ObjectId12) -> Self {
180 AttributesId::new(value.0)
181 }
182}
183
184impl<'a> From<generated::ManifestRef<'a>> for ManifestRef {
185 fn from(value: generated::ManifestRef<'a>) -> Self {
186 let extents = ManifestExtents::from_ranges_iter(
187 value
188 .extents()
189 .iter()
190 .map(|range| Range { start: range.from(), end: range.to() }),
191 );
192 ManifestRef { object_id: value.object_id().into(), extents }
193 }
194}
195
196impl TryFrom<&generated::DimensionShape> for DimensionShape {
197 type Error = IcechunkFormatError;
198
199 fn try_from(value: &generated::DimensionShape) -> Result<Self, Self::Error> {
200 if value.chunk_length() == 0 && value.array_length() != 0 {
201 return Err(IcechunkFormatErrorKind::InvalidArrayMetadata(format!(
202 "Array metadata has chunk_length = 0 while array_length={:?}",
203 value.array_length()
204 )))
205 .capture();
206 }
207 let num_chunks = if value.chunk_length() == 0 {
208 0
209 } else {
210 value.array_length().div_ceil(value.chunk_length()) as u32
211 };
212 Ok(DimensionShape { dim_length: value.array_length(), num_chunks })
213 }
214}
215
216impl<'a> From<&generated::DimensionShapeV2<'a>> for DimensionShape {
217 fn from(value: &generated::DimensionShapeV2<'a>) -> Self {
218 DimensionShape {
219 dim_length: value.array_length(),
220 num_chunks: value.num_chunks(),
221 }
222 }
223}
224
225impl<'a> TryFrom<generated::ArrayNodeData<'a>> for NodeData {
226 type Error = IcechunkFormatError;
227
228 fn try_from(value: generated::ArrayNodeData<'a>) -> Result<Self, Self::Error> {
229 let dimension_names = value
230 .dimension_names()
231 .map(|dn| dn.iter().map(|name| name.name().into()).collect());
232 let shape = ArrayShape(match value.shape_v2() {
236 None => value
237 .shape()
238 .iter()
239 .map(|dim| dim.try_into())
240 .collect::<Result<Vec<_>, IcechunkFormatError>>()?,
241 Some(x) => x.iter().map(|dim| (&dim).into()).collect(),
242 });
243 let manifests = value.manifests().iter().map(|m| m.into()).collect();
244 Ok(Self::Array { shape, dimension_names, manifests })
245 }
246}
247
248impl<'a> From<generated::GroupNodeData<'a>> for NodeData {
249 fn from(_: generated::GroupNodeData<'a>) -> Self {
250 Self::Group
251 }
252}
253
254impl<'a> TryFrom<generated::NodeSnapshot<'a>> for NodeSnapshot {
255 type Error = IcechunkFormatError;
256
257 fn try_from(value: generated::NodeSnapshot<'a>) -> Result<Self, Self::Error> {
258 #[expect(clippy::expect_used, clippy::panic)]
259 let node_data: NodeData = match value.node_data_type() {
260 generated::NodeData::Array => value
261 .node_data_as_array()
262 .expect("Bug in flatbuffers library")
263 .try_into()?,
264 generated::NodeData::Group => {
265 value.node_data_as_group().expect("Bug in flatbuffers library").into()
266 }
267 x => panic!("Invalid node data type in flatbuffers file {x:?}"),
268 };
269 let res = NodeSnapshot {
270 id: value.id().into(),
271 path: Path::from_trusted(value.path()),
272 node_data,
273 user_data: Bytes::copy_from_slice(value.user_data().bytes()),
274 };
275 Ok(res)
276 }
277}
278
279impl From<&generated::ManifestFileInfo> for ManifestFileInfo {
280 fn from(value: &generated::ManifestFileInfo) -> Self {
281 Self {
282 id: value.id().into(),
283 size_bytes: value.size_bytes(),
284 num_chunk_refs: value.num_chunk_refs(),
285 }
286 }
287}
288
289impl TryFrom<&generated::ManifestFileInfoV2<'_>> for ManifestFileInfo {
290 type Error = IcechunkFormatError;
291
292 fn try_from(value: &generated::ManifestFileInfoV2<'_>) -> Result<Self, Self::Error> {
293 let id = value.id().map(|id| id.into()).ok_or_else(|| {
294 IcechunkFormatError::capture(IcechunkFormatErrorKind::InvalidFlatBuffer(
295 flatbuffers::InvalidFlatbuffer::MissingRequiredField {
296 required: Cow::Borrowed("id"),
297 error_trace: Default::default(),
298 },
299 ))
300 })?;
301 Ok(Self {
302 id,
303 size_bytes: value.size_bytes(),
304 num_chunk_refs: value.num_chunk_refs(),
305 })
306 }
307}
308
309pub type SnapshotProperties = BTreeMap<String, Value>;
310
311pub fn inject_icechunk_metadata(
314 properties: &mut SnapshotProperties,
315 key: &str,
316 value: Value,
317) {
318 match properties.get_mut("__icechunk") {
319 Some(Value::Object(map)) => {
320 map.insert(key.to_string(), value);
321 }
322 _ => {
323 properties
324 .insert("__icechunk".to_string(), serde_json::json!({ key: value }));
325 }
326 }
327}
328
329#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Eq, Hash)]
330pub struct ManifestFileInfo {
331 pub id: ManifestId,
332 pub size_bytes: u64,
333 pub num_chunk_refs: u32,
334}
335
336impl ManifestFileInfo {
337 pub fn new(manifest: &Manifest, size_bytes: u64) -> Self {
338 Self {
339 id: manifest.id().clone(),
340 num_chunk_refs: manifest.len() as u32,
341 size_bytes,
342 }
343 }
344}
345
346const SNAPSHOT_NODE_CACHE_SIZE: usize = 2;
355
356pub struct Snapshot {
357 buffer: Vec<u8>,
358 spec_version: SpecVersionBin,
359 node_cache: Cache<Path, Arc<NodeSnapshot>>,
360}
361
362impl std::fmt::Debug for Snapshot {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 let nodes =
365 self.iter().map(|n| n.map(|n| n.path.to_string())).collect::<Vec<_>>();
366 #[expect(deprecated)]
367 f.debug_struct("Snapshot")
368 .field("id", &self.id())
369 .field("parent_id", &self.parent_id())
370 .field("flushed_at", &self.flushed_at())
371 .field("nodes", &nodes)
372 .field(
373 "manifests",
374 &self.manifest_files().collect::<IcechunkResult<Vec<_>>>(),
375 )
376 .field("message", &self.message())
377 .field("metadata", &self.metadata())
378 .finish_non_exhaustive()
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Hash)]
383pub struct SnapshotInfo {
384 pub id: SnapshotId,
385 pub parent_id: Option<SnapshotId>,
386 pub flushed_at: DateTime<Utc>,
387 pub message: String,
388 pub metadata: SnapshotProperties,
389}
390
391impl TryFrom<&Snapshot> for SnapshotInfo {
392 type Error = IcechunkFormatError;
393
394 fn try_from(value: &Snapshot) -> Result<Self, Self::Error> {
395 #[expect(deprecated)]
396 Ok(Self {
397 id: value.id().clone(),
398 parent_id: value.parent_id().clone(),
399 flushed_at: value.flushed_at()?,
400 message: value.message().clone(),
401 metadata: value.metadata()?.clone(),
402 })
403 }
404}
405
406impl SnapshotInfo {
407 pub fn is_initial(&self) -> bool {
408 self.id == Snapshot::INITIAL_SNAPSHOT_ID
409 }
410}
411
412impl SnapshotId {
413 pub fn is_initial(&self) -> bool {
414 *self == Snapshot::INITIAL_SNAPSHOT_ID
415 }
416}
417
418static ROOT_OPTIONS: VerifierOptions = VerifierOptions {
419 max_depth: 64,
420 max_tables: 50_000_000,
421 max_apparent_size: 1 << 31, ignore_missing_null_terminator: true,
423};
424
425impl Snapshot {
426 pub const INITIAL_COMMIT_MESSAGE: &'static str = "Repository initialized";
427 pub const INITIAL_SNAPSHOT_ID: SnapshotId = SnapshotId::new([
428 0x0b, 0x1c, 0xc8, 0xd6, 0x78, 0x75, 0x80, 0xf0, 0xe3, 0x3a, 0x65,
429 0x34, ]);
431
432 pub fn from_buffer(
433 spec_version: SpecVersionBin,
434 buffer: Vec<u8>,
435 ) -> IcechunkResult<Snapshot> {
436 let _ = flatbuffers::root_with_opts::<generated::Snapshot<'_>>(
437 &ROOT_OPTIONS,
438 buffer.as_slice(),
439 )
440 .capture()?;
441 Ok(Snapshot {
442 buffer,
443 spec_version,
444 node_cache: Cache::new(SNAPSHOT_NODE_CACHE_SIZE),
446 })
447 }
448
449 pub fn bytes(&self) -> &[u8] {
450 self.buffer.as_slice()
451 }
452
453 #[expect(clippy::too_many_arguments)]
454 pub fn from_iter<I>(
455 id: Option<SnapshotId>,
456 parent_id: Option<SnapshotId>,
457 spec_version: SpecVersionBin,
458 message: &str,
459 properties: Option<SnapshotProperties>,
460 mut manifest_files: Vec<ManifestFileInfo>,
461 flushed_at: Option<DateTime<Utc>>,
462 sorted_iter: I,
463 ) -> IcechunkResult<Self>
464 where
465 I: IntoIterator<Item = IcechunkResult<NodeSnapshot>>,
466 {
467 let mut builder = FlatBufferBuilder::with_capacity(4_096);
469
470 manifest_files.sort_by(|a, b| a.id.cmp(&b.id));
471 let (manifest_files_v1, manifest_files_v2) = match spec_version {
472 SpecVersionBin::V1 => {
473 let ms1 = manifest_files
474 .iter()
475 .map(|mfi| {
476 let id = generated::ObjectId12::new(&mfi.id.0);
477 generated::ManifestFileInfo::new(
478 &id,
479 mfi.size_bytes,
480 mfi.num_chunk_refs,
481 )
482 })
483 .collect::<Vec<_>>();
484 let ms1 = builder.create_vector(&ms1);
485 let ms2 = None;
486 (ms1, ms2)
487 }
488 SpecVersionBin::V2 => {
489 let ms2 = manifest_files
490 .iter()
491 .map(|mfi| {
492 let id = generated::ObjectId12::new(&mfi.id.0);
493
494 let args = generated::ManifestFileInfoV2Args {
495 id: Some(&id),
496 size_bytes: mfi.size_bytes,
497 num_chunk_refs: mfi.num_chunk_refs,
498 extra: None,
499 };
500 generated::ManifestFileInfoV2::create(&mut builder, &args)
501 })
502 .collect::<Vec<_>>();
503 let ms2 = builder.create_vector(&ms2);
504 let ms1 = builder.create_vector::<generated::ManifestFileInfo>(&[]);
505 (ms1, Some(ms2))
506 }
507 };
508
509 let metadata_items: Vec<_> = properties
510 .unwrap_or_default()
511 .iter()
512 .map(|(k, v)| {
513 let name = builder.create_shared_string(k.as_str());
514 let serialized = if spec_version == SpecVersionBin::V1 {
515 rmp_serde::to_vec(v).map_err(Box::new).capture()?
516 } else {
517 flexbuffers::to_vec(v).map_err(Box::new).capture()?
518 };
519
520 let value = builder.create_vector(serialized.as_slice());
521 Ok::<_, IcechunkFormatError>(generated::MetadataItem::create(
522 &mut builder,
523 &generated::MetadataItemArgs { name: Some(name), value: Some(value) },
524 ))
525 })
526 .try_collect()?;
527 let metadata_items = builder.create_vector(metadata_items.as_slice());
528
529 let message = builder.create_string(message);
530 let parent_id = parent_id.map(|oid| generated::ObjectId12::new(&oid.0));
532 let flushed_at = flushed_at.unwrap_or_else(Utc::now).timestamp_micros() as u64;
533 let id = generated::ObjectId12::new(&id.unwrap_or_else(SnapshotId::random).0);
534
535 let nodes: Vec<_> = sorted_iter
536 .into_iter()
537 .map(|node| node.and_then(|node| mk_node(&mut builder, &node, spec_version)))
538 .try_collect()?;
539 let nodes = builder.create_vector(&nodes);
540
541 let snap = generated::Snapshot::create(
542 &mut builder,
543 &generated::SnapshotArgs {
544 id: Some(&id),
545 parent_id: parent_id.as_ref(),
546 nodes: Some(nodes),
547 flushed_at,
548 message: Some(message),
549 metadata: Some(metadata_items),
550 manifest_files: Some(manifest_files_v1),
551 manifest_files_v2,
552 ..Default::default()
553 },
554 );
555
556 builder.finish(snap, Some("Ichk"));
557 let (mut buffer, offset) = builder.collapse();
558 buffer.drain(0..offset);
559 buffer.shrink_to_fit();
560 Ok(Snapshot {
561 buffer,
562 spec_version,
563 node_cache: Cache::new(SNAPSHOT_NODE_CACHE_SIZE),
565 })
566 }
567
568 pub fn initial(spec_version: SpecVersionBin) -> IcechunkResult<Self> {
569 let mut properties = SnapshotProperties::default();
570 inject_icechunk_metadata(&mut properties, "is_root", Value::from(true));
571 let nodes = Vec::<IcechunkResult<NodeSnapshot>>::new();
572 Self::from_iter(
573 Some(Self::INITIAL_SNAPSHOT_ID),
574 None,
575 spec_version,
576 Self::INITIAL_COMMIT_MESSAGE,
577 Some(properties),
578 Default::default(),
579 None,
580 nodes,
581 )
582 }
583
584 #[expect(unsafe_code)]
585 fn root(&self) -> generated::Snapshot<'_> {
586 unsafe { flatbuffers::root_unchecked::<generated::Snapshot<'_>>(&self.buffer) }
590 }
591
592 pub fn id(&self) -> SnapshotId {
593 SnapshotId::new(self.root().id().0)
594 }
595
596 #[deprecated(
597 since = "2.0.0",
598 note = "New versions of icechunk don't use this field and initialize it to None"
599 )]
600 pub fn parent_id(&self) -> Option<SnapshotId> {
601 self.root().parent_id().map(|pid| SnapshotId::new(pid.0))
602 }
603
604 pub fn metadata(&self) -> IcechunkResult<SnapshotProperties> {
605 self.root()
606 .metadata()
607 .iter()
608 .map(|item| {
609 let key = item.name().to_string();
610 let value = if self.spec_version == SpecVersionBin::V1 {
611 rmp_serde::from_slice(item.value().bytes())
612 .map_err(Box::new)
613 .capture()?
614 } else {
615 flexbuffers::from_slice(item.value().bytes())
616 .map_err(Box::new)
617 .capture()?
618 };
619 Ok((key, value))
620 })
621 .try_collect()
622 }
623
624 pub fn flushed_at(&self) -> IcechunkResult<DateTime<Utc>> {
625 let ts = self.root().flushed_at();
626 let ts: i64 = ts
627 .try_into()
628 .map_err(|_| IcechunkFormatErrorKind::InvalidTimestamp)
629 .capture()?;
630 DateTime::from_timestamp_micros(ts)
631 .ok_or(IcechunkFormatErrorKind::InvalidTimestamp)
632 .capture()
633 }
634
635 pub fn message(&self) -> String {
636 self.root().message().to_string()
637 }
638
639 pub fn manifest_files(
640 &self,
641 ) -> impl Iterator<Item = IcechunkResult<ManifestFileInfo>> + '_ {
642 let root = self.root();
643 if let Some(mf2) = root.manifest_files_v2() {
644 Either::Left(mf2.iter().map(|mf| (&mf).try_into()))
645 } else {
646 Either::Right(root.manifest_files().iter().map(|mf| Ok(mf.into())))
647 }
648 }
649
650 #[deprecated(
652 since = "2.0.0",
653 note = "Shouldn't be necessary after 2.0, only to support Icechunk 1 repos"
654 )]
655 pub fn adopt(&self, new_child: &Snapshot) -> IcechunkResult<Self> {
656 Snapshot::from_iter(
660 Some(new_child.id()),
661 Some(self.id()),
662 SpecVersionBin::V1, &new_child.message(),
664 Some(new_child.metadata()?.clone()),
665 new_child.manifest_files().try_collect()?,
666 Some(new_child.flushed_at()?),
667 new_child.iter(),
668 )
669 }
670
671 fn _get_node(&self, path: &Path) -> IcechunkResult<NodeSnapshot> {
672 let res = self
673 .root()
674 .nodes()
675 .lookup_by_key(path.to_string().as_str(), |node, path| node.path().cmp(path))
676 .ok_or_else(|| IcechunkFormatErrorKind::NodeNotFound { path: path.clone() })
677 .capture()?;
678 res.try_into()
679 }
680
681 pub fn get_node(&self, path: &Path) -> IcechunkResult<Arc<NodeSnapshot>> {
682 use GuardResult::*;
683 match self.node_cache.get_value_or_guard(path, None) {
684 Value(node) => Ok(node),
685 Timeout => Ok(Arc::new(self._get_node(path)?)),
686 Guard(guard) => {
687 let node = self._get_node(path)?;
688 let node = Arc::new(node);
689 let _ = guard.insert(Arc::clone(&node));
690 Ok(node)
691 }
692 }
693 }
694
695 pub fn get_node_index(&self, path: &Path) -> IcechunkResult<usize> {
696 let path_str = path.to_string();
697 let res =
698 lookup_index_by_key(self.root().nodes(), path_str.as_str(), |node, path| {
699 node.path().cmp(path)
700 })
701 .ok_or_else(|| IcechunkFormatErrorKind::NodeNotFound { path: path.clone() })
702 .capture()?;
703 Ok(res)
704 }
705
706 pub fn iter(&self) -> impl Iterator<Item = IcechunkResult<NodeSnapshot>> + '_ {
707 self.root().nodes().iter().map(|node| node.try_into().inject())
708 }
709
710 pub fn iter_arc(
711 self: Arc<Self>,
712 parent_group: &Path,
713 ) -> impl Iterator<Item = IcechunkResult<NodeSnapshot>> + use<> {
714 NodeIterator::new(self, parent_group)
715 }
716
717 pub fn len(&self) -> usize {
718 self.root().nodes().len()
719 }
720
721 #[must_use]
722 pub fn is_empty(&self) -> bool {
723 self.len() == 0
724 }
725
726 pub fn manifest_info(
727 &self,
728 id: &ManifestId,
729 ) -> IcechunkResult<Option<ManifestFileInfo>> {
730 let root = self.root();
731 if let Some(mf2) = root.manifest_files_v2() {
732 mf2.iter()
733 .find(|mf| mf.id().is_some_and(|mid| mid.0 == id.0))
734 .map(|mf| (&mf).try_into())
735 .transpose()
736 } else {
737 Ok(root
738 .manifest_files()
739 .iter()
740 .find(|mi| mi.id().0 == id.0)
741 .map(|man| man.into()))
742 }
743 }
744}
745
746struct NodeIterator {
747 snapshot: Arc<Snapshot>,
748 next_index: usize,
749 prefix: String,
750}
751
752impl NodeIterator {
753 fn new(snapshot: Arc<Snapshot>, parent_group: &Path) -> Self {
754 let next_index = snapshot.get_node_index(parent_group).unwrap_or_default();
755 let prefix = parent_group.to_string();
756 let prefix = if prefix == "/" { String::new() } else { prefix };
757 NodeIterator { snapshot, next_index, prefix }
758 }
759}
760
761impl Iterator for NodeIterator {
762 type Item = IcechunkResult<NodeSnapshot>;
763
764 fn next(&mut self) -> Option<Self::Item> {
765 let nodes = self.snapshot.root().nodes();
766 loop {
767 if self.next_index >= nodes.len() {
770 return None;
771 }
772
773 let node: IcechunkResult<NodeSnapshot> =
774 nodes.get(self.next_index).try_into();
775
776 match node {
777 Ok(res) => {
778 let node_path = res.path.to_string();
779 if let Some(after_prefix) =
780 node_path.strip_prefix(self.prefix.as_str())
781 && (after_prefix.is_empty() || after_prefix.starts_with('/'))
782 {
783 self.next_index += 1;
784 return Some(Ok(res));
785 } else if node_path.as_str() > self.prefix.as_str()
786 && !node_path.starts_with(self.prefix.as_str())
787 {
788 return None;
790 } else {
791 self.next_index += 1;
793 }
794 }
795 Err(err) => return Some(Err(err)),
796 }
797 }
798 }
799}
800
801fn mk_node<'bldr>(
802 builder: &mut FlatBufferBuilder<'bldr>,
803 node: &NodeSnapshot,
804 spec_version: SpecVersionBin,
805) -> IcechunkResult<WIPOffset<generated::NodeSnapshot<'bldr>>> {
806 let id = generated::ObjectId8::new(&node.id.0);
807 let path = builder.create_string(node.path.to_string().as_str());
808 let (node_data_type, node_data) =
809 mk_node_data(builder, &node.node_data, spec_version)?;
810 let user_data = Some(builder.create_vector(&node.user_data));
811 Ok(generated::NodeSnapshot::create(
812 builder,
813 &generated::NodeSnapshotArgs {
814 id: Some(&id),
815 path: Some(path),
816 node_data_type,
817 node_data,
818 user_data,
819 ..Default::default()
820 },
821 ))
822}
823
824type ShapeV1<'a> = WIPOffset<Vector<'a, generated::DimensionShape>>;
825type ShapeV2<'a> =
826 WIPOffset<Vector<'a, ForwardsUOffset<generated::DimensionShapeV2<'a>>>>;
827
828fn mk_array_shapes<'a>(
829 builder: &mut FlatBufferBuilder<'a>,
830 spec_version: SpecVersionBin,
831 shape: &ArrayShape,
832) -> (Option<ShapeV1<'a>>, Option<ShapeV2<'a>>) {
833 use SpecVersionBin::*;
834 match spec_version {
835 V1 => {
836 let shape = shape
837 .0
838 .iter()
839 .map(|ds| {
840 let chunk_length = if ds.num_chunks == 0 {
841 debug_assert_eq!(ds.dim_length, 0);
842 0
843 } else {
844 ds.dim_length.div_ceil(ds.num_chunks as u64)
849 };
850
851 generated::DimensionShape::new(ds.dim_length, chunk_length)
852 })
853 .collect::<Vec<_>>();
854 (Some(builder.create_vector(shape.as_slice())), None)
855 }
856 V2 => {
857 let shape = shape
858 .0
859 .iter()
860 .map(|ds| {
861 generated::DimensionShapeV2::create(
862 builder,
863 &generated::DimensionShapeV2Args {
864 array_length: ds.dim_length,
865 num_chunks: ds.num_chunks,
866 },
867 )
868 })
869 .collect::<Vec<_>>();
870 let empty_shape_for_v1_compat =
871 builder.create_vector(&[] as &[generated::DimensionShape]);
872 (
873 Some(empty_shape_for_v1_compat),
874 Some(builder.create_vector(shape.as_slice())),
875 )
876 }
877 }
878}
879
880fn mk_node_data(
881 builder: &mut FlatBufferBuilder<'_>,
882 node_data: &NodeData,
883 spec_version: SpecVersionBin,
884) -> IcechunkResult<(generated::NodeData, Option<WIPOffset<UnionWIPOffset>>)> {
885 match node_data {
886 NodeData::Array { manifests, dimension_names, shape } => {
887 let manifests = manifests
888 .iter()
889 .map(|manref| {
890 let object_id = generated::ObjectId12::new(&manref.object_id.0);
891 let extents = manref
892 .extents
893 .iter()
894 .map(|range| {
895 generated::ChunkIndexRange::new(range.start, range.end)
896 })
897 .collect::<Vec<_>>();
898 let extents = builder.create_vector(&extents);
899 generated::ManifestRef::create(
900 builder,
901 &generated::ManifestRefArgs {
902 object_id: Some(&object_id),
903 extents: Some(extents),
904 },
905 )
906 })
907 .collect::<Vec<_>>();
908 let manifests = builder.create_vector(manifests.as_slice());
909 let dimensions = dimension_names.as_ref().map(|dn| {
910 let names = dn
911 .iter()
912 .map(|n| match n {
913 DimensionName::Name(s) => {
914 let n = builder.create_shared_string(s.as_str());
915 generated::DimensionName::create(
916 builder,
917 &generated::DimensionNameArgs { name: Some(n) },
918 )
919 }
920 DimensionName::NotSpecified => generated::DimensionName::create(
921 builder,
922 &generated::DimensionNameArgs { name: None },
923 ),
924 })
925 .collect::<Vec<_>>();
926 builder.create_vector(names.as_slice())
927 });
928 let (shape_v1, shape_v2) = mk_array_shapes(builder, spec_version, shape);
929 let node_data = generated::ArrayNodeData::create(
930 builder,
931 &generated::ArrayNodeDataArgs {
932 manifests: Some(manifests),
933 shape: shape_v1,
934 shape_v2,
935 dimension_names: dimensions,
936 },
937 );
938 Ok((generated::NodeData::Array, Some(node_data.as_union_value())))
939 }
940 NodeData::Group => Ok((
941 generated::NodeData::Group,
942 Some(
943 generated::GroupNodeData::create(
944 builder,
945 &generated::GroupNodeDataArgs {},
946 )
947 .as_union_value(),
948 ),
949 )),
950 }
951}
952
953#[cfg(test)]
954#[expect(unused_qualifications)] mod tests {
956 use crate::{IcechunkFormatError, ObjectId};
957
958 use super::*;
959 use crate::{
960 roundtrip_serialization_tests,
961 strategies::{ShapeDim, manifest_file_info, node_snapshot, shapes_and_dims},
962 };
963 use pretty_assertions::assert_eq;
964 use proptest::prelude::*;
965 use std::iter::{self};
966
967 roundtrip_serialization_tests!(
968 serialize_and_deserialize_node_snapshot - node_snapshot,
969 serialize_and_deserialize_manifest_file_info - manifest_file_info
970 );
971
972 #[icechunk_macros::test]
973 fn test_get_node() -> Result<(), Box<dyn std::error::Error>> {
974 let shape1 = ArrayShape::new(vec![(10u64, 3), (20, 2), (30, 1)]).unwrap();
975 let dim_names1 = Some(vec!["x".into(), "y".into(), "t".into()]);
976
977 let shape2 = shape1.clone();
978 let dim_names2 = Some(vec![
979 DimensionName::NotSpecified,
980 DimensionName::NotSpecified,
981 "t".into(),
982 ]);
983
984 let shape3 = shape1.clone();
985 let dim_names3 = None;
986
987 let man_ref1 = ManifestRef {
988 object_id: ObjectId::random(),
989 extents: ManifestExtents::new(&[0, 0, 0], &[100, 100, 100]),
990 };
991 let man_ref2 = ManifestRef {
992 object_id: ObjectId::random(),
993 extents: ManifestExtents::new(&[0, 0, 0], &[100, 100, 100]),
994 };
995
996 let node_ids = iter::repeat_with(NodeId::random).take(7).collect::<Vec<_>>();
997 let nodes = vec![
999 NodeSnapshot {
1000 path: Path::root(),
1001 id: node_ids[0].clone(),
1002 user_data: Bytes::new(),
1003 node_data: NodeData::Group,
1004 },
1005 NodeSnapshot {
1006 path: "/a".try_into().unwrap(),
1007 id: node_ids[1].clone(),
1008 user_data: Bytes::new(),
1009 node_data: NodeData::Group,
1010 },
1011 NodeSnapshot {
1012 path: "/array2".try_into().unwrap(),
1013 id: node_ids[5].clone(),
1014 user_data: Bytes::new(),
1015 node_data: NodeData::Array {
1016 shape: shape2.clone(),
1017 dimension_names: dim_names2.clone(),
1018 manifests: vec![],
1019 },
1020 },
1021 NodeSnapshot {
1022 path: "/b".try_into().unwrap(),
1023 id: node_ids[2].clone(),
1024 user_data: Bytes::new(),
1025 node_data: NodeData::Group,
1026 },
1027 NodeSnapshot {
1028 path: "/b/array1".try_into().unwrap(),
1029 id: node_ids[4].clone(),
1030 user_data: Bytes::copy_from_slice(b"hello"),
1031 node_data: NodeData::Array {
1032 shape: shape1.clone(),
1033 dimension_names: dim_names1.clone(),
1034 manifests: vec![man_ref1.clone(), man_ref2.clone()],
1035 },
1036 },
1037 NodeSnapshot {
1038 path: "/b/array3".try_into().unwrap(),
1039 id: node_ids[6].clone(),
1040 user_data: Bytes::new(),
1041 node_data: NodeData::Array {
1042 shape: shape3.clone(),
1043 dimension_names: dim_names3.clone(),
1044 manifests: vec![],
1045 },
1046 },
1047 NodeSnapshot {
1048 path: "/b/c".try_into().unwrap(),
1049 id: node_ids[3].clone(),
1050 user_data: Bytes::copy_from_slice(b"bye"),
1051 node_data: NodeData::Group,
1052 },
1053 ];
1054 let manifests = vec![
1055 ManifestFileInfo {
1056 id: man_ref1.object_id.clone(),
1057 size_bytes: 1_000_000,
1058 num_chunk_refs: 100_000,
1059 },
1060 ManifestFileInfo {
1061 id: man_ref2.object_id.clone(),
1062 size_bytes: 1_000_000,
1063 num_chunk_refs: 100_000,
1064 },
1065 ];
1066 let st = Snapshot::from_iter(
1067 None,
1068 None,
1069 SpecVersionBin::current(),
1070 "",
1071 Default::default(),
1072 manifests,
1073 None,
1074 nodes.into_iter().map(Ok::<NodeSnapshot, IcechunkFormatError>),
1075 )
1076 .unwrap();
1077
1078 assert!(matches!(
1079 st.get_node(&"/nonexistent".try_into().unwrap()),
1080 Err(IcechunkFormatError {
1081 kind: IcechunkFormatErrorKind::NodeNotFound {
1082 path
1083 },
1084 ..
1085 }) if path == "/nonexistent".try_into().unwrap()
1086 ));
1087
1088 let node = st.get_node(&"/b/c".try_into().unwrap()).unwrap();
1089 assert_eq!(
1090 node,
1091 Arc::new(NodeSnapshot {
1092 path: "/b/c".try_into().unwrap(),
1093 id: node_ids[3].clone(),
1094 user_data: Bytes::copy_from_slice(b"bye"),
1095 node_data: NodeData::Group,
1096 }),
1097 );
1098 let node = st.get_node(&Path::root()).unwrap();
1099 assert_eq!(
1100 node,
1101 Arc::new(NodeSnapshot {
1102 path: Path::root(),
1103 id: node_ids[0].clone(),
1104 user_data: Bytes::new(),
1105 node_data: NodeData::Group,
1106 }),
1107 );
1108 let node = st.get_node(&"/b/array1".try_into().unwrap()).unwrap();
1109 assert_eq!(
1110 node,
1111 Arc::new(NodeSnapshot {
1112 path: "/b/array1".try_into().unwrap(),
1113 id: node_ids[4].clone(),
1114 user_data: Bytes::copy_from_slice(b"hello"),
1115 node_data: NodeData::Array {
1116 shape: shape1.clone(),
1117 dimension_names: dim_names1.clone(),
1118 manifests: vec![man_ref1, man_ref2]
1119 },
1120 }),
1121 );
1122 let node = st.get_node(&"/array2".try_into().unwrap()).unwrap();
1123 assert_eq!(
1124 node,
1125 Arc::new(NodeSnapshot {
1126 path: "/array2".try_into().unwrap(),
1127 id: node_ids[5].clone(),
1128 user_data: Bytes::new(),
1129 node_data: NodeData::Array {
1130 shape: shape2.clone(),
1131 dimension_names: dim_names2.clone(),
1132 manifests: vec![]
1133 },
1134 }),
1135 );
1136 let node = st.get_node(&"/b/array3".try_into().unwrap()).unwrap();
1137 assert_eq!(
1138 node,
1139 Arc::new(NodeSnapshot {
1140 path: "/b/array3".try_into().unwrap(),
1141 id: node_ids[6].clone(),
1142 user_data: Bytes::new(),
1143 node_data: NodeData::Array {
1144 shape: shape3.clone(),
1145 dimension_names: dim_names3.clone(),
1146 manifests: vec![]
1147 },
1148 }),
1149 );
1150 Ok(())
1151 }
1152
1153 #[icechunk_macros::test]
1154 fn test_valid_chunk_coord() {
1155 let shape1 =
1156 ArrayShape::new(vec![(10_000, 10), (10_001, 11), (9_999, 10)]).unwrap();
1157 let shape2 = ArrayShape::new(vec![(0, 1_000), (0, 1_000), (0, 1_000)]).unwrap();
1158 let coord1 = ChunkIndices(vec![9, 10, 9]);
1159 let coord2 = ChunkIndices(vec![10, 11, 10]);
1160 let coord3 = ChunkIndices(vec![0, 0, 0]);
1161
1162 assert!(shape1.valid_chunk_coord(&coord1));
1163 assert!(!shape1.valid_chunk_coord(&coord2));
1164 assert!(shape2.valid_chunk_coord(&coord3));
1165 }
1166
1167 #[icechunk_macros::test]
1168 fn test_valid_chunk_coord_zero_length_dim() {
1169 let shape = ArrayShape::new(vec![(0, 0)]).unwrap();
1170 assert!(shape.valid_chunk_coord(&ChunkIndices(vec![0])));
1171 assert!(!shape.valid_chunk_coord(&ChunkIndices(vec![1])));
1172 }
1173
1174 #[test_strategy::proptest]
1175 fn test_prop_valid_chunk_coord(
1176 #[strategy(shapes_and_dims(None, Some(1)))] shape_dim: ShapeDim,
1177 axis_offset: usize,
1178 ) {
1179 let shape = &shape_dim.shape;
1180 let ndim = shape.len();
1181 let axis = axis_offset % ndim;
1182
1183 let origin = ChunkIndices(vec![0; ndim]);
1185 prop_assert!(shape.valid_chunk_coord(&origin));
1186
1187 let max_valid =
1189 ChunkIndices(shape.num_chunks().map(|nc| nc.saturating_sub(1)).collect());
1190 prop_assert!(shape.valid_chunk_coord(&max_valid));
1191
1192 let mut oob = max_valid.0.clone();
1194 oob[axis] = shape.iter().nth(axis).unwrap().num_chunks().max(1);
1195 prop_assert!(!shape.valid_chunk_coord(&ChunkIndices(oob)));
1196 }
1197}