1use std::sync::atomic::{AtomicU64, Ordering};
2
3use ahash::HashMap;
4use anyhow::Context as _;
5use arrow::array::{
6 Array as ArrowArray, ArrayRef as ArrowArrayRef, FixedSizeBinaryArray,
7 ListArray as ArrowListArray,
8};
9use arrow::buffer::{NullBuffer as ArrowNullBuffer, ScalarBuffer as ArrowScalarBuffer};
10use itertools::{Either, Itertools as _, izip};
11use nohash_hasher::IntMap;
12use re_arrow_util::{ArrowArrayDowncastRef as _, widen_binary_arrays};
13use re_byte_size::SizeBytes as _;
14use re_log::debug_assert;
15use re_log_types::{
16 AbsoluteTimeRange, EntityPath, NonMinI64, TimeInt, TimeType, Timeline, TimelineName,
17};
18use re_types_core::{
19 ArrowDataType as _, ComponentDescriptor, ComponentIdentifier, ComponentType,
20 DeserializationError, SerializationError, SerializedComponentColumn,
21};
22
23use crate::{ChunkId, RowId};
24
25#[derive(thiserror::Error, Debug)]
30pub enum ChunkError {
31 #[error("Detected malformed chunk: {reason}")]
32 Malformed { reason: String },
33
34 #[error("Arrow: {0}")]
35 Arrow(#[from] arrow::error::ArrowError),
36
37 #[error("{kind} index out of bounds: {index} (len={len})")]
38 IndexOutOfBounds {
39 kind: String,
40 len: usize,
41 index: usize,
42 },
43
44 #[error("Serialization: {0}")]
45 Serialization(#[from] SerializationError),
46
47 #[error("Deserialization: {0}")]
48 Deserialization(#[from] DeserializationError),
49
50 #[error(transparent)]
51 IndexColumn(#[from] re_sorbet::IndexColumnError),
52
53 #[error(transparent)]
54 WrongDatatypeError(#[from] re_arrow_util::WrongDatatypeError),
55
56 #[error(transparent)]
57 MismatchedChunkSchemaError(#[from] re_sorbet::MismatchedChunkSchemaError),
58
59 #[error(transparent)]
60 InvalidSorbetSchema(#[from] re_sorbet::SorbetError),
61
62 #[error(transparent)]
64 DataframeToChunks(#[from] Box<re_sorbet::DataframeToChunksError>),
65}
66
67const _: () = assert!(
68 std::mem::size_of::<ChunkError>() <= 72,
69 "Error type is too large. Try to reduce its size by boxing some of its variants.",
70);
71
72pub type ChunkResult<T> = Result<T, ChunkError>;
73
74#[derive(Debug, Clone, Default, PartialEq)]
77pub struct ChunkComponents(pub IntMap<ComponentIdentifier, SerializedComponentColumn>);
78
79impl ChunkComponents {
80 #[inline]
84 pub fn get_by_component_type(
85 &self,
86 component_type: ComponentType,
87 ) -> impl Iterator<Item = &ArrowListArray> {
88 self.0.values().filter_map(move |column| {
89 (column.descriptor.component_type == Some(component_type)).then_some(&column.list_array)
90 })
91 }
92
93 pub fn ensure_similar(left: &Self, right: &Self) -> anyhow::Result<()> {
101 anyhow::ensure!(left.len() == right.len());
102 for (component, left_column) in left.iter() {
103 let Some(right_column) = right.get(*component) else {
104 anyhow::bail!("rhs is missing {component:?}");
105 };
106 anyhow::ensure!(left_column.descriptor == right_column.descriptor);
107
108 let left_array = widen_binary_arrays(&left_column.list_array);
109 let right_array = widen_binary_arrays(&right_column.list_array);
110 re_arrow_util::ensure_similar(&left_array.to_data(), &right_array.to_data())
111 .with_context(|| format!("Component {component:?}"))?;
112 }
113 Ok(())
114 }
115
116 #[inline]
118 pub fn contains_component(&self, component: ComponentIdentifier) -> bool {
119 self.0.contains_key(&component)
120 }
121
122 #[inline]
124 pub fn component_descriptors(&self) -> impl Iterator<Item = &ComponentDescriptor> + '_ {
125 self.0.values().map(|column| &column.descriptor)
126 }
127
128 #[inline]
130 pub fn list_arrays(&self) -> impl Iterator<Item = &ArrowListArray> + '_ {
131 self.0.values().map(|column| &column.list_array)
132 }
133
134 #[inline]
136 pub fn list_arrays_mut(&mut self) -> impl Iterator<Item = &mut ArrowListArray> + '_ {
137 self.0.values_mut().map(|column| &mut column.list_array)
138 }
139
140 #[inline]
142 pub fn get_array(&self, component: ComponentIdentifier) -> Option<&ArrowListArray> {
143 self.0.get(&component).map(|column| &column.list_array)
144 }
145
146 #[inline]
148 pub fn get_descriptor(&self, component: ComponentIdentifier) -> Option<&ComponentDescriptor> {
149 self.0.get(&component).map(|column| &column.descriptor)
150 }
151
152 #[inline]
154 pub fn get(&self, component: ComponentIdentifier) -> Option<&SerializedComponentColumn> {
155 self.0.get(&component)
156 }
157
158 #[inline]
162 pub fn insert(
163 &mut self,
164 column: SerializedComponentColumn,
165 ) -> Option<SerializedComponentColumn> {
166 self.0.insert(column.descriptor.component, column)
167 }
168
169 pub(crate) fn canonicalize_list_fields(&mut self) {
177 for column in self.0.values_mut() {
178 if let Some(canonical) =
179 re_arrow_util::canonicalized_component_list_array(&column.list_array)
180 {
181 column.list_array = canonical;
182 }
183 }
184 }
185}
186
187impl std::ops::Deref for ChunkComponents {
188 type Target = IntMap<ComponentIdentifier, SerializedComponentColumn>;
189
190 #[inline]
191 fn deref(&self) -> &Self::Target {
192 &self.0
193 }
194}
195
196impl std::ops::DerefMut for ChunkComponents {
197 #[inline]
198 fn deref_mut(&mut self) -> &mut Self::Target {
199 &mut self.0
200 }
201}
202
203impl FromIterator<(ComponentDescriptor, ArrowListArray)> for ChunkComponents {
205 #[inline]
206 fn from_iter<T: IntoIterator<Item = (ComponentDescriptor, ArrowListArray)>>(iter: T) -> Self {
207 let mut this = Self::default();
208 {
209 for (component_desc, list_array) in iter {
210 this.insert(SerializedComponentColumn::new(list_array, component_desc));
211 }
212 }
213 this
214 }
215}
216
217impl FromIterator<SerializedComponentColumn> for ChunkComponents {
218 #[inline]
219 fn from_iter<T: IntoIterator<Item = SerializedComponentColumn>>(iter: T) -> Self {
220 let mut this = Self::default();
221 {
222 for serialized in iter {
223 this.insert(serialized);
224 }
225 }
226 this
227 }
228}
229
230pub struct Chunk {
243 pub(crate) id: ChunkId,
244
245 pub(crate) entity_path: EntityPath,
246
247 pub(crate) heap_size_bytes: AtomicU64,
252
253 pub(crate) is_sorted: bool,
255
256 pub(crate) row_ids: FixedSizeBinaryArray,
258
259 pub(crate) timelines: IntMap<TimelineName, TimeColumn>,
265
266 pub(crate) components: ChunkComponents,
272}
273
274fn auto_row_ids(id: ChunkId, count: usize) -> FixedSizeBinaryArray {
281 let row_ids: Vec<RowId> =
282 std::iter::successors(Some(RowId::from_tuid((*id).next())), |row_id| {
283 Some(row_id.next())
284 })
285 .take(count)
286 .collect();
287 RowId::arrow_from_slice(&row_ids)
288}
289
290impl std::fmt::Debug for Chunk {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 let Self {
293 id,
294 entity_path,
295 heap_size_bytes,
296 is_sorted,
297 row_ids: _, timelines,
299 components,
300 } = self;
301
302 if f.alternate() {
303 f.debug_struct("Chunk")
304 .field("id", id)
305 .field("entity_path", entity_path)
306 .field("heap_size_bytes", &heap_size_bytes.load(Ordering::Relaxed))
307 .field("is_sorted", is_sorted)
308 .field("row_ids", &self.row_ids_slice())
309 .field("timelines", timelines)
310 .field("components", components)
311 .finish()
312 } else {
313 f.debug_struct("Chunk")
314 .field("id", id)
315 .field("entity_path", entity_path)
316 .finish_non_exhaustive()
317 }
318 }
319}
320
321impl PartialEq for Chunk {
322 #[inline]
324 fn eq(&self, other: &Self) -> bool {
325 let Self {
326 id: _,
327 entity_path,
328 heap_size_bytes: _,
329 is_sorted,
330 row_ids,
331 timelines,
332 components,
333 } = self;
334
335 *entity_path == other.entity_path
336 && *is_sorted == other.is_sorted
337 && *row_ids == other.row_ids
338 && *timelines == other.timelines
339 && *components == other.components
340 }
341}
342
343impl Chunk {
344 #[must_use]
348 #[inline]
349 pub fn with_id(mut self, id: ChunkId) -> Self {
350 self.id = id;
351 self
352 }
353
354 pub fn ensure_similar(lhs: &Self, rhs: &Self) -> anyhow::Result<()> {
364 Self::ensure_similar_ignoring_timelines(lhs, rhs, &[])
365 }
366
367 pub fn ensure_similar_ignoring_timelines(
373 lhs: &Self,
374 rhs: &Self,
375 ignored_timelines: &[TimelineName],
376 ) -> anyhow::Result<()> {
377 anyhow::ensure!(lhs.num_rows() == rhs.num_rows());
378
379 let Self {
380 id: _,
381 entity_path,
382 heap_size_bytes: _,
383 is_sorted: _,
384 row_ids: _,
385 timelines,
386 components,
387 } = lhs;
388
389 let is_ignored = |timeline: &TimelineName| ignored_timelines.contains(timeline);
390
391 anyhow::ensure!(*entity_path == rhs.entity_path);
392
393 let lhs_timelines = timelines
396 .keys()
397 .filter(|t| !is_ignored(t))
398 .sorted()
399 .collect_vec();
400 let rhs_timelines = rhs
401 .timelines
402 .keys()
403 .filter(|t| !is_ignored(t))
404 .sorted()
405 .collect_vec();
406 anyhow::ensure!(
407 lhs_timelines == rhs_timelines,
408 "Timelines differ: {lhs_timelines:?} vs {rhs_timelines:?}"
409 );
410
411 anyhow::ensure!(
413 components.len() == rhs.components.len(),
414 "Number of components differs: {} vs {}",
415 components.len(),
416 rhs.components.len()
417 );
418
419 for (timeline, left_time_col) in timelines {
420 if is_ignored(timeline) {
421 continue;
422 }
423 let right_time_col = rhs
424 .timelines
425 .get(timeline)
426 .ok_or_else(|| anyhow::format_err!("right is missing timeline {timeline:?}"))?;
427 if timeline == &TimelineName::log_time() {
428 continue; }
430 if timeline == "sim_time" {
431 continue; }
433 anyhow::ensure!(
434 left_time_col == right_time_col,
435 "Timeline differs: {timeline:?}"
436 );
437 }
438
439 if entity_path == &EntityPath::properties() {
441 anyhow::ensure!(components.len() == rhs.components.len());
444
445 let recording_time_component: ComponentIdentifier = "RecordingInfo:start_time".into();
447
448 let lhs_components = components
450 .iter()
451 .filter(|&(component, _list_array)| component != &recording_time_component)
452 .map(|(component, list_array)| (*component, list_array.clone()))
453 .collect::<IntMap<_, _>>();
454 let rhs_components = rhs
455 .components
456 .iter()
457 .filter(|&(component, _list_array)| component != &recording_time_component)
458 .map(|(component, list_array)| (*component, list_array.clone()))
459 .collect::<IntMap<_, _>>();
460
461 anyhow::ensure!(lhs_components == rhs_components);
462 Ok(())
463 } else {
464 ChunkComponents::ensure_similar(components, &rhs.components)
465 }
466 }
467
468 pub fn are_equal(&self, other: &Self) -> bool {
470 let Self {
471 id,
472 entity_path,
473 heap_size_bytes: _,
474 is_sorted,
475 row_ids,
476 timelines,
477 components,
478 } = self;
479
480 *id == other.id
481 && *entity_path == other.entity_path
482 && *is_sorted == other.is_sorted
483 && row_ids == &other.row_ids
484 && *timelines == other.timelines
485 && components.0 == other.components.0
486 }
487
488 pub fn with_mapped_component<E>(
496 &self,
497 source: ComponentIdentifier,
498 target: Option<ComponentDescriptor>,
499 f: impl FnOnce(ArrowListArray) -> Result<ArrowListArray, E>,
500 ) -> Result<Self, E> {
501 let mut new_components = self.components.clone();
502 if let Some(old_entry) = new_components.remove(&source) {
503 new_components.insert(SerializedComponentColumn {
504 descriptor: target.unwrap_or(old_entry.descriptor),
505 list_array: f(old_entry.list_array)?,
506 });
507 }
508
509 let id = ChunkId::new();
510 let row_ids = auto_row_ids(id, self.num_rows());
511
512 Ok(Self {
513 id,
514 entity_path: self.entity_path.clone(),
515 heap_size_bytes: AtomicU64::new(0),
516 is_sorted: true,
517 row_ids,
518 timelines: self.timelines.clone(),
519 components: new_components,
520 })
521 }
522
523 pub fn with_shadowed_component<E>(
541 &self,
542 source: ComponentIdentifier,
543 target: ComponentIdentifier,
544 f: impl FnOnce(ArrowListArray) -> Result<ArrowListArray, E>,
545 ) -> Result<Self, E> {
546 let mut new_chunk = self.clone_with_new_id();
547 if let Some(old_entry) = new_chunk.components.remove(&source) {
548 new_chunk.components.insert(SerializedComponentColumn {
549 descriptor: ComponentDescriptor {
550 component: target,
551 archetype: None,
552 component_type: None,
553 },
554 list_array: f(old_entry.list_array)?,
555 });
556 }
557
558 Ok(new_chunk)
559 }
560}
561
562impl Clone for Chunk {
563 #[inline]
564 fn clone(&self) -> Self {
565 self.clone_with_same_id()
566 }
567}
568
569impl Chunk {
570 #[inline]
571 pub fn clone_with_id(&self, id: ChunkId) -> Self {
572 Self {
573 id,
574 entity_path: self.entity_path.clone(),
575 heap_size_bytes: AtomicU64::new(self.heap_size_bytes.load(Ordering::Relaxed)),
576 is_sorted: self.is_sorted,
577 row_ids: self.row_ids.clone(),
578 timelines: self.timelines.clone(),
579 components: self.components.clone(),
580 }
581 }
582
583 #[inline]
584 pub fn clone_with_same_id(&self) -> Self {
585 self.clone_with_id(self.id)
586 }
587
588 #[inline]
589 pub fn clone_with_new_id(&self) -> Self {
590 self.clone_with_id(ChunkId::new())
591 }
592
593 #[inline]
594 pub fn clone_with_new_entity_path(&self, entity_path: EntityPath) -> Self {
595 let mut chunk = self.clone_with_id(ChunkId::new());
596 chunk.entity_path = entity_path;
597 chunk
598 }
599}
600
601impl Chunk {
602 fn reset_cached_heap_size_bytes(&self) {
603 self.heap_size_bytes.store(0, Ordering::Relaxed);
604 }
605
606 #[inline]
611 pub fn clone_as(&self, id: ChunkId, first_row_id: RowId) -> Self {
612 let row_ids = std::iter::from_fn({
613 let mut row_id = first_row_id;
614 move || {
615 let yielded = row_id;
616 row_id = row_id.next();
617 Some(yielded)
618 }
619 })
620 .take(self.row_ids.len())
621 .collect_vec();
622
623 let new_chunk = Self {
624 row_ids: RowId::arrow_from_slice(&row_ids),
625 ..self.clone_with_id(id)
626 };
627
628 new_chunk.reset_cached_heap_size_bytes();
631
632 new_chunk
633 }
634
635 #[inline]
637 pub fn into_static(mut self) -> Self {
638 self.timelines.clear();
639 self.reset_cached_heap_size_bytes();
640 self
641 }
642
643 pub fn zeroed(self) -> Self {
647 let row_ids = vec![RowId::ZERO; self.row_ids.len()];
648
649 let row_ids = RowId::arrow_from_slice(&row_ids);
650
651 let new_chunk = Self {
652 id: ChunkId::new(),
653 row_ids,
654 ..self
655 };
656
657 new_chunk.reset_cached_heap_size_bytes();
660
661 new_chunk
662 }
663
664 #[inline]
673 pub fn time_range_per_component(
674 &self,
675 ) -> IntMap<TimelineName, IntMap<ComponentIdentifier, AbsoluteTimeRange>> {
676 re_tracing::profile_function!();
677
678 self.timelines
679 .iter()
680 .map(|(timeline_name, time_column)| {
681 (
682 *timeline_name,
683 time_column.time_range_per_component(&self.components),
684 )
685 })
686 .collect()
687 }
688
689 #[inline]
690 pub fn component_descriptors(&self) -> impl Iterator<Item = &ComponentDescriptor> + '_ {
691 self.components.component_descriptors()
692 }
693
694 #[inline]
700 pub fn num_events_cumulative(&self) -> u64 {
701 self.components
703 .list_arrays()
704 .map(|list_array| {
705 list_array.nulls().map_or_else(
706 || list_array.len() as u64,
707 |validity| validity.len() as u64 - validity.null_count() as u64,
708 )
709 })
710 .sum()
711 }
712
713 pub fn num_events_cumulative_per_unique_time(
721 &self,
722 timeline: &TimelineName,
723 ) -> Vec<(TimeInt, u64)> {
724 if self.is_static() {
725 return vec![(TimeInt::STATIC, self.num_events_cumulative())];
726 }
727
728 let Some(time_column) = self.timelines().get(timeline) else {
729 return Vec::new();
730 };
731
732 let time_range = time_column.time_range();
733 if time_range.min() == time_range.max() {
734 return vec![(time_range.min(), self.num_events_cumulative())];
735 }
736
737 let counts = if time_column.is_sorted() {
738 self.num_events_cumulative_per_unique_time_sorted(time_column)
739 } else {
740 self.num_events_cumulative_per_unique_time_unsorted(time_column)
741 };
742
743 debug_assert!(
744 counts
745 .iter()
746 .tuple_windows::<(_, _)>()
747 .all(|((time1, _), (time2, _))| time1 < time2)
748 );
749
750 counts
751 }
752
753 fn num_events_cumulative_per_unique_time_sorted(
754 &self,
755 time_column: &TimeColumn,
756 ) -> Vec<(TimeInt, u64)> {
757 debug_assert!(time_column.is_sorted());
758
759 let mut counts_raw = vec![0u64; self.num_rows()];
764 {
765 self.components.list_arrays().for_each(|list_array| {
766 if let Some(validity) = list_array.nulls() {
767 validity
768 .iter()
769 .enumerate()
770 .for_each(|(i, is_valid)| counts_raw[i] += is_valid as u64);
771 } else {
772 for count in &mut counts_raw {
773 *count += 1;
774 }
775 }
776 });
777 }
778
779 let mut counts = Vec::with_capacity(counts_raw.len());
780
781 let Some(mut cur_time) = time_column.times().next() else {
782 return Vec::new();
783 };
784 let mut cur_count = 0;
785 izip!(time_column.times(), counts_raw).for_each(|(time, count)| {
786 if time == cur_time {
787 cur_count += count;
788 } else {
789 counts.push((cur_time, cur_count));
790 cur_count = count;
791 cur_time = time;
792 }
793 });
794
795 if counts.last().map(|(time, _)| *time) != Some(cur_time) {
796 counts.push((cur_time, cur_count));
797 }
798
799 counts
800 }
801
802 fn num_events_cumulative_per_unique_time_unsorted(
803 &self,
804 time_column: &TimeColumn,
805 ) -> Vec<(TimeInt, u64)> {
806 debug_assert!(!time_column.is_sorted());
807
808 let result_unordered =
811 self.components
812 .list_arrays()
813 .fold(HashMap::default(), |acc, list_array| {
814 if let Some(validity) = list_array.nulls() {
815 std::iter::zip(time_column.times(), validity.iter()).fold(
816 acc,
817 |mut acc, (time, is_valid)| {
818 *acc.entry(time).or_default() += is_valid as u64;
819 acc
820 },
821 )
822 } else {
823 time_column.times().fold(acc, |mut acc, time| {
824 *acc.entry(time).or_default() += 1;
825 acc
826 })
827 }
828 });
829
830 let mut result = result_unordered.into_iter().collect_vec();
831 result.sort_by_key(|val| val.0);
832 result
833 }
834
835 #[inline]
841 pub fn num_events_for_component(&self, component: ComponentIdentifier) -> Option<u64> {
842 self.components.get_array(component).map(|list_array| {
844 list_array.nulls().map_or_else(
845 || list_array.len() as u64,
846 |validity| validity.len() as u64 - validity.null_count() as u64,
847 )
848 })
849 }
850
851 pub fn row_id_range_per_component(&self) -> IntMap<ComponentIdentifier, (RowId, RowId)> {
860 re_tracing::profile_function!();
861
862 let row_ids = self.row_ids().collect_vec();
863
864 if self.is_row_ids_sorted() {
865 self.components
866 .iter()
867 .filter_map(|(component, column)| {
868 let mut row_id_min = None;
869 let mut row_id_max = None;
870
871 for (i, &row_id) in row_ids.iter().enumerate() {
872 if column.list_array.is_valid(i) {
873 row_id_max = Some(row_id);
874 }
875 }
876 for (i, &row_id) in row_ids.iter().enumerate().rev() {
877 if column.list_array.is_valid(i) {
878 row_id_min = Some(row_id);
879 }
880 }
881
882 Some((*component, (row_id_min?, row_id_max?)))
883 })
884 .collect()
885 } else {
886 self.components
887 .iter()
888 .filter_map(|(component, column)| {
889 let mut row_id_min = Some(RowId::MAX);
890 let mut row_id_max = Some(RowId::ZERO);
891
892 for (i, &row_id) in row_ids.iter().enumerate() {
893 if column.list_array.is_valid(i) && Some(row_id) < row_id_min {
894 row_id_min = Some(row_id);
895 }
896 }
897 for (i, &row_id) in row_ids.iter().enumerate().rev() {
898 if column.list_array.is_valid(i) && Some(row_id) > row_id_max {
899 row_id_max = Some(row_id);
900 }
901 }
902
903 Some((*component, (row_id_min?, row_id_max?)))
904 })
905 .collect()
906 }
907 }
908}
909
910#[derive(Debug, Clone, PartialEq, Eq, re_byte_size::SizeBytes)]
913pub struct TimeColumn {
914 pub(crate) timeline: Timeline,
915
916 pub(crate) times: ArrowScalarBuffer<i64>,
927
928 pub(crate) is_sorted: bool,
933
934 pub(crate) time_range: AbsoluteTimeRange,
938}
939
940#[derive(Debug, thiserror::Error)]
942pub enum TimeColumnError {
943 #[error("Time columns had nulls, but should be dense")]
944 ContainsNulls,
945
946 #[error("Unsupported data type : {0}")]
947 UnsupportedDataType(arrow::datatypes::DataType),
948}
949
950impl Chunk {
951 pub fn new(
961 id: ChunkId,
962 entity_path: EntityPath,
963 is_sorted: Option<bool>,
964 row_ids: FixedSizeBinaryArray,
965 timelines: IntMap<TimelineName, TimeColumn>,
966 components: ChunkComponents,
967 ) -> ChunkResult<Self> {
968 let mut chunk = Self {
969 id,
970 entity_path,
971 heap_size_bytes: AtomicU64::new(0),
972 is_sorted: false,
973 row_ids,
974 timelines,
975 components,
976 };
977
978 chunk.components.canonicalize_list_fields();
981
982 chunk.is_sorted = is_sorted.unwrap_or_else(|| chunk.is_row_ids_sorted_uncached());
983
984 chunk.sanity_check()?;
985
986 Ok(chunk)
987 }
988
989 pub fn from_native_row_ids(
999 id: ChunkId,
1000 entity_path: EntityPath,
1001 is_sorted: Option<bool>,
1002 row_ids: &[RowId],
1003 timelines: IntMap<TimelineName, TimeColumn>,
1004 components: ChunkComponents,
1005 ) -> ChunkResult<Self> {
1006 re_tracing::profile_function!();
1007 let row_ids = RowId::arrow_from_slice(row_ids);
1008 Self::new(id, entity_path, is_sorted, row_ids, timelines, components)
1009 }
1010
1011 pub fn from_auto_row_ids(
1023 id: ChunkId,
1024 entity_path: EntityPath,
1025 timelines: IntMap<TimelineName, TimeColumn>,
1026 components: ChunkComponents,
1027 ) -> ChunkResult<Self> {
1028 re_tracing::profile_function!();
1029
1030 let count = components
1031 .list_arrays()
1032 .next()
1033 .map_or(0, |list_array| list_array.len());
1034
1035 let timeline_order: Vec<TimelineName> = timelines.keys().copied().sorted().collect();
1039
1040 let mut swaps: Vec<usize> = (0..count).collect();
1041 swaps.sort_by(|&a, &b| {
1042 for name in &timeline_order {
1043 let times = timelines[name].times_raw();
1044 let ord = times[a].cmp(×[b]);
1045 if ord != std::cmp::Ordering::Equal {
1046 return ord;
1047 }
1048 }
1049 std::cmp::Ordering::Equal
1050 });
1051
1052 let placeholder_row_ids = auto_row_ids(id, count);
1055 let mut chunk = Self::new(
1056 id,
1057 entity_path,
1058 Some(true),
1059 placeholder_row_ids,
1060 timelines,
1061 components,
1062 )?;
1063
1064 let already_sorted = swaps
1065 .iter()
1066 .copied()
1067 .enumerate()
1068 .all(|(to, from)| to == from);
1069 if !already_sorted {
1070 chunk.shuffle_with(&swaps);
1071 chunk.row_ids = auto_row_ids(chunk.id, count);
1072 chunk.is_sorted = true;
1073
1074 #[cfg(debug_assertions)]
1075 #[expect(clippy::unwrap_used)] chunk.sanity_check().unwrap();
1077 }
1078
1079 Ok(chunk)
1080 }
1081
1082 pub fn from_columns(
1093 entity_path: impl Into<EntityPath>,
1094 timelines: impl IntoIterator<Item = TimeColumn>,
1095 components: impl IntoIterator<Item = (ComponentDescriptor, ArrowListArray)>,
1096 ) -> ChunkResult<Self> {
1097 let timelines: IntMap<TimelineName, TimeColumn> = timelines
1098 .into_iter()
1099 .map(|tc| (*tc.timeline().name(), tc))
1100 .collect();
1101 let components: ChunkComponents = components.into_iter().collect();
1102 Self::from_auto_row_ids(ChunkId::new(), entity_path.into(), timelines, components)
1103 }
1104
1105 #[inline]
1109 pub fn new_static(
1110 id: ChunkId,
1111 entity_path: EntityPath,
1112 is_sorted: Option<bool>,
1113 row_ids: FixedSizeBinaryArray,
1114 components: ChunkComponents,
1115 ) -> ChunkResult<Self> {
1116 Self::new(
1117 id,
1118 entity_path,
1119 is_sorted,
1120 row_ids,
1121 Default::default(),
1122 components,
1123 )
1124 }
1125
1126 #[inline]
1127 pub fn empty(id: ChunkId, entity_path: EntityPath) -> Self {
1128 Self {
1129 id,
1130 entity_path,
1131 heap_size_bytes: Default::default(),
1132 is_sorted: true,
1133 row_ids: RowId::arrow_from_slice(&[]),
1134 timelines: Default::default(),
1135 components: Default::default(),
1136 }
1137 }
1138
1139 #[inline]
1145 pub fn add_component(
1146 &mut self,
1147 component_column: SerializedComponentColumn,
1148 ) -> ChunkResult<()> {
1149 self.id = ChunkId::new();
1150 self.components.insert(component_column);
1151 self.components.canonicalize_list_fields();
1152 self.reset_cached_heap_size_bytes();
1153 self.sanity_check()
1154 }
1155
1156 #[inline]
1162 pub fn add_timeline(&mut self, chunk_timeline: TimeColumn) -> ChunkResult<()> {
1163 self.id = ChunkId::new();
1164
1165 self.timelines
1166 .insert(*chunk_timeline.timeline.name(), chunk_timeline);
1167 self.reset_cached_heap_size_bytes();
1168
1169 self.sanity_check()
1170 }
1171}
1172
1173impl TimeColumn {
1174 pub fn new(is_sorted: Option<bool>, timeline: Timeline, times: ArrowScalarBuffer<i64>) -> Self {
1181 re_tracing::profile_function_if!(1000 < times.len(), format!("{} times", times.len()));
1182
1183 let time_slice = times.as_ref();
1184
1185 let is_sorted =
1186 is_sorted.unwrap_or_else(|| time_slice.array_windows().all(|[a, b]| a <= b));
1187
1188 let time_range = if is_sorted {
1189 let min_time = time_slice
1191 .first()
1192 .copied()
1193 .map_or(TimeInt::MIN, TimeInt::new_temporal);
1194 let max_time = time_slice
1195 .last()
1196 .copied()
1197 .map_or(TimeInt::MAX, TimeInt::new_temporal);
1198 AbsoluteTimeRange::new(min_time, max_time)
1199 } else {
1200 let min_time = time_slice
1203 .iter()
1204 .min()
1205 .copied()
1206 .map_or(TimeInt::MIN, TimeInt::new_temporal);
1207 let max_time = time_slice
1208 .iter()
1209 .max()
1210 .copied()
1211 .map_or(TimeInt::MAX, TimeInt::new_temporal);
1212 AbsoluteTimeRange::new(min_time, max_time)
1213 };
1214
1215 Self {
1216 timeline,
1217 times,
1218 is_sorted,
1219 time_range,
1220 }
1221 }
1222
1223 pub fn new_sequence(
1225 name: impl Into<re_log_types::TimelineName>,
1226 times: impl IntoIterator<Item = impl Into<i64>>,
1227 ) -> Self {
1228 let time_vec: Vec<_> = times.into_iter().map(|t| {
1229 let t = t.into();
1230 TimeInt::try_from(t)
1231 .unwrap_or_else(|_| {
1232 re_log::error!(
1233 illegal_value = t,
1234 new_value = TimeInt::MIN.as_i64(),
1235 "TimeColumn::new_sequence() called with illegal value - clamped to minimum legal value"
1236 );
1237 TimeInt::MIN
1238 })
1239 .as_i64()
1240 }).collect();
1241
1242 Self::new(
1243 None,
1244 Timeline::new_sequence(name.into()),
1245 ArrowScalarBuffer::from(time_vec),
1246 )
1247 }
1248
1249 pub fn new_duration_secs(
1251 name: impl Into<re_log_types::TimelineName>,
1252 seconds: impl IntoIterator<Item = impl Into<f64>>,
1253 ) -> Self {
1254 let time_vec = seconds.into_iter().map(|seconds| {
1255 let seconds = seconds.into();
1256 let nanos = (1e9 * seconds).round();
1257 let clamped = NonMinI64::saturating_from_i64(nanos as i64);
1258 if clamped.get() as f64 != nanos {
1259 re_log::warn!(
1260 illegal_value = nanos,
1261 new_value = clamped.get(),
1262 "TimeColumn::new_duration_secs() called with out-of-range value. Clamped to valid range."
1263 );
1264 }
1265 clamped.get()
1266 }).collect_vec();
1267
1268 Self::new(
1269 None,
1270 Timeline::new(name, TimeType::DurationNs),
1271 ArrowScalarBuffer::from(time_vec),
1272 )
1273 }
1274
1275 pub fn new_timestamp_secs_since_epoch(
1277 name: impl Into<re_log_types::TimelineName>,
1278 seconds: impl IntoIterator<Item = impl Into<f64>>,
1279 ) -> Self {
1280 let time_vec = seconds.into_iter().map(|seconds| {
1281 let seconds = seconds.into();
1282 let nanos = (1e9 * seconds).round();
1283 let clamped = NonMinI64::saturating_from_i64(nanos as i64);
1284 if clamped.get() as f64 != nanos {
1285 re_log::warn!(
1286 illegal_value = nanos,
1287 new_value = clamped.get(),
1288 "TimeColumn::new_timestamp_secs_since_epoch() called with out-of-range value. Clamped to valid range."
1289 );
1290 }
1291 clamped.get()
1292 }).collect_vec();
1293
1294 Self::new(
1295 None,
1296 Timeline::new(name, TimeType::TimestampNs),
1297 ArrowScalarBuffer::from(time_vec),
1298 )
1299 }
1300
1301 pub fn new_duration_nanos(
1303 name: impl Into<re_log_types::TimelineName>,
1304 nanos: impl IntoIterator<Item = impl Into<i64>>,
1305 ) -> Self {
1306 let time_vec = nanos
1307 .into_iter()
1308 .map(|nanos| {
1309 let nanos = nanos.into();
1310 NonMinI64::new(nanos)
1311 .unwrap_or_else(|| {
1312 re_log::error!(
1313 illegal_value = nanos,
1314 new_value = TimeInt::MIN.as_i64(),
1315 "TimeColumn::new_duration_nanos() called with illegal value - clamped to minimum legal value"
1316 );
1317 NonMinI64::MIN
1318 })
1319 .get()
1320 })
1321 .collect_vec();
1322
1323 Self::new(
1324 None,
1325 Timeline::new(name, TimeType::DurationNs),
1326 ArrowScalarBuffer::from(time_vec),
1327 )
1328 }
1329
1330 pub fn new_timestamp_nanos_since_epoch(
1332 name: impl Into<re_log_types::TimelineName>,
1333 nanos: impl IntoIterator<Item = impl Into<i64>>,
1334 ) -> Self {
1335 let time_vec = nanos
1336 .into_iter()
1337 .map(|nanos| {
1338 let nanos = nanos.into();
1339 NonMinI64::new(nanos)
1340 .unwrap_or_else(|| {
1341 re_log::error!(
1342 illegal_value = nanos,
1343 new_value = TimeInt::MIN.as_i64(),
1344 "TimeColumn::new_timestamp_nanos_since_epoch() called with illegal value - clamped to minimum legal value"
1345 );
1346 NonMinI64::MIN
1347 })
1348 .get()
1349 })
1350 .collect_vec();
1351
1352 Self::new(
1353 None,
1354 Timeline::new(name, TimeType::TimestampNs),
1355 ArrowScalarBuffer::from(time_vec),
1356 )
1357 }
1358
1359 pub fn read_array(array: &dyn ArrowArray) -> Result<ArrowScalarBuffer<i64>, TimeColumnError> {
1363 if array.null_count() > 0 {
1364 Err(TimeColumnError::ContainsNulls)
1365 } else {
1366 Self::read_nullable_array(array).map(|(times, _nulls)| times)
1367 }
1368 }
1369
1370 pub fn read_nullable_array(
1374 array: &dyn ArrowArray,
1375 ) -> Result<(ArrowScalarBuffer<i64>, Option<ArrowNullBuffer>), TimeColumnError> {
1376 if let Some(times) = array.downcast_array_ref::<arrow::array::Int64Array>() {
1378 Ok((times.values().clone(), times.nulls().cloned()))
1379 } else if let Some(times) =
1380 array.downcast_array_ref::<arrow::array::TimestampNanosecondArray>()
1381 {
1382 Ok((times.values().clone(), times.nulls().cloned()))
1383 } else if let Some(times) =
1384 array.downcast_array_ref::<arrow::array::Time64NanosecondArray>()
1385 {
1386 Ok((times.values().clone(), times.nulls().cloned()))
1387 } else if let Some(times) =
1388 array.downcast_array_ref::<arrow::array::DurationNanosecondArray>()
1389 {
1390 Ok((times.values().clone(), times.nulls().cloned()))
1391 } else {
1392 Err(TimeColumnError::UnsupportedDataType(
1393 array.data_type().clone(),
1394 ))
1395 }
1396 }
1397}
1398
1399impl Chunk {
1402 #[inline]
1403 pub fn id(&self) -> ChunkId {
1404 self.id
1405 }
1406
1407 #[inline]
1408 pub fn entity_path(&self) -> &EntityPath {
1409 &self.entity_path
1410 }
1411
1412 #[inline]
1414 pub fn num_columns(&self) -> usize {
1415 let Self {
1416 id: _,
1417 entity_path: _, heap_size_bytes: _,
1419 is_sorted: _,
1420 row_ids: _,
1421 timelines,
1422 components,
1423 } = self;
1424
1425 1 + timelines.len() + components.len()
1426 }
1427
1428 #[inline]
1429 pub fn num_controls(&self) -> usize {
1430 _ = self;
1431 1 }
1433
1434 #[inline]
1435 pub fn num_timelines(&self) -> usize {
1436 self.timelines.len()
1437 }
1438
1439 #[inline]
1440 pub fn num_components(&self) -> usize {
1441 self.components.len()
1442 }
1443
1444 #[inline]
1445 pub fn num_rows(&self) -> usize {
1446 self.row_ids.len()
1447 }
1448
1449 #[inline]
1450 pub fn is_empty(&self) -> bool {
1451 self.num_rows() == 0
1452 }
1453
1454 #[inline]
1455 pub fn row_ids_array(&self) -> &FixedSizeBinaryArray {
1456 &self.row_ids
1457 }
1458
1459 #[inline]
1460 pub fn row_ids_slice(&self) -> &[RowId] {
1461 RowId::slice_from_arrow(&self.row_ids)
1462 }
1463
1464 #[inline]
1468 pub fn row_ids(&self) -> impl ExactSizeIterator<Item = RowId> + '_ {
1469 self.row_ids_slice().iter().copied()
1470 }
1471
1472 #[inline]
1477 pub fn row_index_of(&self, row_id: RowId) -> Option<usize> {
1478 if self.is_row_ids_sorted() {
1479 self.row_ids_slice().binary_search(&row_id).ok()
1480 } else {
1481 self.row_ids_slice().iter().position(|r| *r == row_id)
1482 }
1483 }
1484
1485 #[inline]
1490 pub fn component_row_ids(
1491 &self,
1492 component: ComponentIdentifier,
1493 ) -> impl Iterator<Item = RowId> + '_ + use<'_> {
1494 let Some(list_array) = self.components.get_array(component) else {
1495 return Either::Left(std::iter::empty());
1496 };
1497
1498 let row_ids = self.row_ids();
1499
1500 if let Some(validity) = list_array.nulls() {
1501 Either::Right(Either::Left(
1502 row_ids
1503 .enumerate()
1504 .filter_map(|(i, o)| validity.is_valid(i).then_some(o)),
1505 ))
1506 } else {
1507 Either::Right(Either::Right(row_ids))
1508 }
1509 }
1510
1511 #[inline]
1517 pub fn row_id_range(&self) -> Option<(RowId, RowId)> {
1518 if self.is_empty() {
1519 return None;
1520 }
1521
1522 let row_ids = self.row_ids_slice();
1523
1524 #[expect(clippy::unwrap_used)] Some(if self.is_row_ids_sorted() {
1526 (
1527 row_ids.first().copied().unwrap(),
1528 row_ids.last().copied().unwrap(),
1529 )
1530 } else {
1531 (
1532 row_ids.iter().min().copied().unwrap(),
1533 row_ids.iter().max().copied().unwrap(),
1534 )
1535 })
1536 }
1537
1538 #[inline]
1539 pub fn is_static(&self) -> bool {
1540 self.timelines.is_empty()
1541 }
1542
1543 #[inline]
1544 pub fn timelines(&self) -> &IntMap<TimelineName, TimeColumn> {
1545 &self.timelines
1546 }
1547
1548 #[inline]
1549 pub fn components_identifiers(&self) -> impl Iterator<Item = ComponentIdentifier> + '_ {
1550 self.components.keys().copied()
1551 }
1552
1553 #[inline]
1554 pub fn components(&self) -> &ChunkComponents {
1555 &self.components
1556 }
1557}
1558
1559impl std::fmt::Display for Chunk {
1560 #[inline]
1561 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1562 let batch = self.to_record_batch().map_err(|err| {
1563 re_log::error_once!("couldn't display Chunk: {err}");
1564 std::fmt::Error
1565 })?;
1566 re_arrow_util::format_record_batch_with_width(&batch, f.width(), f.sign_minus()).fmt(f)
1567 }
1568}
1569
1570impl TimeColumn {
1571 #[inline]
1572 pub fn timeline(&self) -> &Timeline {
1573 &self.timeline
1574 }
1575
1576 #[inline]
1577 pub fn name(&self) -> &str {
1578 self.timeline.name()
1579 }
1580
1581 #[inline]
1582 pub fn time_range(&self) -> AbsoluteTimeRange {
1583 self.time_range
1584 }
1585
1586 #[inline]
1587 pub fn times_buffer(&self) -> &ArrowScalarBuffer<i64> {
1588 &self.times
1589 }
1590
1591 #[inline]
1593 pub fn times_array(&self) -> ArrowArrayRef {
1594 self.timeline.typ().make_arrow_array(self.times.clone())
1595 }
1596
1597 #[inline]
1600 pub fn times_raw(&self) -> &[i64] {
1601 self.times.as_ref()
1602 }
1603
1604 #[inline]
1607 pub fn times_nonmin(&self) -> impl DoubleEndedIterator<Item = NonMinI64> + '_ {
1608 self.times_raw()
1609 .iter()
1610 .copied()
1611 .map(NonMinI64::saturating_from_i64)
1612 }
1613
1614 #[inline]
1615 pub fn times(&self) -> impl DoubleEndedIterator<Item = TimeInt> + '_ {
1616 self.times_raw().iter().copied().map(TimeInt::new_temporal)
1617 }
1618
1619 #[inline]
1620 pub fn num_rows(&self) -> usize {
1621 self.times.len()
1622 }
1623
1624 #[inline]
1625 pub fn is_empty(&self) -> bool {
1626 self.num_rows() == 0
1627 }
1628
1629 pub fn time_range_per_component(
1638 &self,
1639 components: &ChunkComponents,
1640 ) -> IntMap<ComponentIdentifier, AbsoluteTimeRange> {
1641 let times = self.times_raw();
1642 components
1643 .iter()
1644 .filter_map(|(component, column)| {
1645 if let Some(validity) = column.list_array.nulls() {
1646 if validity.is_empty() {
1649 return None;
1650 }
1651
1652 let is_dense = validity.null_count() == 0;
1653 if is_dense {
1654 return Some((*component, self.time_range));
1655 }
1656
1657 let time_min = {
1658 let mut valid_times = times
1659 .iter()
1660 .enumerate()
1661 .filter(|(i, _time)| validity.is_valid(*i));
1662 if times.is_sorted() {
1663 valid_times.next()
1664 } else {
1665 valid_times.min_by_key(|(_i, time)| *time)
1666 }
1667 .map_or(TimeInt::MAX, |(_i, time)| TimeInt::new_temporal(*time))
1668 };
1669
1670 let time_max = {
1671 let mut valid_times_inv = times
1672 .iter()
1673 .enumerate()
1674 .rev()
1675 .filter(|(i, _time)| validity.is_valid(*i));
1676 if times.is_sorted() {
1677 valid_times_inv.next()
1678 } else {
1679 valid_times_inv.max_by_key(|(_i, time)| *time)
1680 }
1681 .map_or(TimeInt::MIN, |(_i, time)| TimeInt::new_temporal(*time))
1682 };
1683
1684 Some((*component, AbsoluteTimeRange::new(time_min, time_max)))
1685 } else {
1686 Some((*component, self.time_range))
1689 }
1690 })
1691 .collect()
1692 }
1693
1694 pub fn find_next_time(&self, after: TimeInt) -> Option<TimeInt> {
1696 if self.is_sorted() {
1697 let times = self.times_raw();
1698 let idx = times.partition_point(|&t| t <= after.as_i64());
1699 (idx < times.len()).then(|| TimeInt::new_temporal(times[idx]))
1700 } else {
1701 self.times_raw()
1702 .iter()
1703 .filter(|&&t| t > after.as_i64())
1704 .min()
1705 .map(|&t| TimeInt::new_temporal(t))
1706 }
1707 }
1708
1709 pub fn find_prev_time(&self, before: TimeInt) -> Option<TimeInt> {
1711 if self.is_sorted() {
1712 let times = self.times_raw();
1713 let idx = times.partition_point(|&t| t < before.as_i64());
1714 (idx > 0).then(|| TimeInt::new_temporal(times[idx - 1]))
1715 } else {
1716 self.times_raw()
1717 .iter()
1718 .filter(|&&t| t < before.as_i64())
1719 .max()
1720 .map(|&t| TimeInt::new_temporal(t))
1721 }
1722 }
1723
1724 pub fn offset_by_nanos(&self, offset_ns: i64) -> Self {
1728 let new_times: Vec<i64> = self
1729 .times
1730 .iter()
1731 .map(|&t| NonMinI64::saturating_from_i64(t.saturating_add(offset_ns)).get())
1732 .collect();
1733
1734 Self::new(
1735 Some(self.is_sorted),
1736 self.timeline,
1737 ArrowScalarBuffer::from(new_times),
1738 )
1739 }
1740}
1741
1742impl Chunk {
1743 fn heap_size_bytes_inner(&self) -> u64 {
1744 re_tracing::profile_function!();
1745
1746 let Self {
1747 id,
1748 entity_path,
1749 heap_size_bytes: _,
1750 is_sorted,
1751 row_ids,
1752 timelines,
1753 components,
1754 } = self;
1755
1756 id.heap_size_bytes()
1757 + entity_path.heap_size_bytes()
1758 + is_sorted.heap_size_bytes()
1759 + row_ids.heap_size_bytes()
1760 + timelines.heap_size_bytes()
1761 + components.heap_size_bytes()
1762 }
1763}
1764
1765impl re_byte_size::SizeBytes for Chunk {
1766 #[inline]
1767 fn heap_size_bytes(&self) -> u64 {
1768 let mut size_bytes = self.heap_size_bytes.load(Ordering::Relaxed);
1769
1770 if size_bytes == 0 {
1771 size_bytes = self.heap_size_bytes_inner();
1772 self.heap_size_bytes.store(size_bytes, Ordering::Relaxed);
1773 }
1774
1775 size_bytes
1776 }
1777}
1778
1779impl Chunk {
1782 #[track_caller]
1792 pub fn warn_if_out_of_order(&self) {
1793 if !self.is_row_ids_sorted() {
1794 if re_log::is_rerun_very_strict() {
1796 panic!("Found chunk that wasn't sorted by RowId. This is a bug");
1797 } else {
1798 re_log::debug_warn_once!("Found chunk that wasn't sorted by RowId. This is a bug");
1799 }
1800 }
1801
1802 let unsorted_timelines: Vec<_> = self.unsorted_timelines().collect();
1803 if !unsorted_timelines.is_empty() {
1804 if re_log::is_rerun_very_strict() {
1805 panic!(
1806 "Found out-of-order timelines for entity '{}': {:?}. Out-of-order timelines are sometimes unavoidable, but they may cause performance problems",
1807 self.entity_path, unsorted_timelines
1808 );
1809 } else {
1810 re_log::debug_warn_once!(
1812 "Found out-of-order timelines: {unsorted_timelines:?}. Out-of-order timelines are sometimes unavoidable, but they may cause performance problems",
1813 );
1814 }
1815 }
1816 }
1817
1818 #[track_caller]
1822 pub fn sanity_check(&self) -> ChunkResult<()> {
1823 re_tracing::profile_function!();
1824
1825 let Self {
1826 id: _,
1827 entity_path: _,
1828 heap_size_bytes,
1829 is_sorted,
1830 row_ids,
1831 timelines,
1832 components,
1833 } = self;
1834
1835 self.warn_if_out_of_order();
1836
1837 if cfg!(debug_assertions) {
1838 let measured = self.heap_size_bytes_inner();
1839 let advertised = heap_size_bytes.load(Ordering::Relaxed);
1840 if advertised != 0 && advertised != measured {
1842 return Err(ChunkError::Malformed {
1843 reason: format!(
1844 "Chunk advertises a heap size of {} but we measure {} instead",
1845 re_format::format_bytes(advertised as _),
1846 re_format::format_bytes(measured as _),
1847 ),
1848 });
1849 }
1850 }
1851
1852 {
1854 if *row_ids.data_type() != RowId::arrow_data_type() {
1855 return Err(ChunkError::Malformed {
1856 reason: format!(
1857 "RowId data has the wrong datatype: expected {} but got {} instead",
1858 RowId::arrow_data_type(),
1859 row_ids.data_type(),
1860 ),
1861 });
1862 }
1863
1864 #[expect(clippy::collapsible_if)] if cfg!(debug_assertions) {
1866 if *is_sorted != self.is_row_ids_sorted_uncached() {
1867 return Err(ChunkError::Malformed {
1868 reason: format!(
1869 "Chunk is marked as {}sorted but isn't: {row_ids:?}",
1870 if *is_sorted { "" } else { "un" },
1871 ),
1872 });
1873 }
1874 }
1875 }
1876
1877 for (timeline_name, time_column) in timelines {
1879 if time_column.times.len() != row_ids.len() {
1880 return Err(ChunkError::Malformed {
1881 reason: format!(
1882 "All timelines in a chunk must have the same number of timestamps, matching the number of row IDs. \
1883 Found {} row IDs but {} timestamps for timeline '{timeline_name}'",
1884 row_ids.len(),
1885 time_column.times.len(),
1886 ),
1887 });
1888 }
1889
1890 time_column.sanity_check()?;
1891 }
1892
1893 for (component, column) in components.iter() {
1896 let SerializedComponentColumn {
1897 list_array,
1898 descriptor,
1899 } = column;
1900
1901 if descriptor.component != *component {
1902 return Err(ChunkError::Malformed {
1903 reason: format!(
1904 "Component key & descriptor mismatch. Descriptor: {descriptor:?}. Key: {component:?}",
1905 ),
1906 });
1907 }
1908
1909 if let arrow::datatypes::DataType::List(field) = list_array.data_type() {
1911 if cfg!(debug_assertions)
1917 && let canonical =
1918 re_arrow_util::canonical_component_list_field(field.data_type().clone())
1919 && field.as_ref() != &canonical
1920 {
1921 return Err(ChunkError::Malformed {
1922 reason: format!(
1923 "Component column {component} must describe its outer list as {canonical:?}, got {field:?}",
1924 ),
1925 });
1926 }
1927 } else {
1928 return Err(ChunkError::Malformed {
1929 reason: format!(
1930 "The inner array in a chunked component batch must be a list, got {:?}",
1931 list_array.data_type(),
1932 ),
1933 });
1934 }
1935
1936 if list_array.len() != row_ids.len() {
1937 return Err(ChunkError::Malformed {
1938 reason: format!(
1939 "All component batches in a chunk must have the same number of rows, matching the number of row IDs. \
1940 Found {} row IDs but {} rows for component batch {component}",
1941 row_ids.len(),
1942 list_array.len(),
1943 ),
1944 });
1945 }
1946
1947 let validity_is_empty = list_array
1948 .nulls()
1949 .is_some_and(|validity| validity.is_empty());
1950 if !self.is_empty() && validity_is_empty {
1951 return Err(ChunkError::Malformed {
1952 reason: format!(
1953 "All component batches in a chunk must contain at least one non-null entry.\
1954 Found a completely empty column for {component}",
1955 ),
1956 });
1957 }
1958 }
1959
1960 Ok(())
1961 }
1962}
1963
1964impl TimeColumn {
1965 #[track_caller]
1969 pub fn sanity_check(&self) -> ChunkResult<()> {
1970 let Self {
1971 timeline: _,
1972 times,
1973 is_sorted,
1974 time_range,
1975 } = self;
1976
1977 let times = times.as_ref();
1978
1979 if cfg!(debug_assertions) && *is_sorted != times.array_windows().all(|[a, b]| a <= b) {
1980 return Err(ChunkError::Malformed {
1981 reason: format!(
1982 "Time column is marked as {}sorted but isn't: {times:?}",
1983 if *is_sorted { "" } else { "un" },
1984 ),
1985 });
1986 }
1987
1988 if cfg!(debug_assertions) {
1989 let is_tight_lower_bound = times.iter().any(|&time| time == time_range.min().as_i64());
1990 let is_tight_upper_bound = times.iter().any(|&time| time == time_range.max().as_i64());
1991 let is_tight_bound = is_tight_lower_bound && is_tight_upper_bound;
1992
1993 if !self.is_empty() && !is_tight_bound {
1994 return Err(ChunkError::Malformed {
1995 reason: "Time column's cached time range isn't a tight bound.".to_owned(),
1996 });
1997 }
1998
1999 for &time in times {
2000 if time < time_range.min().as_i64() || time > time_range.max().as_i64() {
2001 return Err(ChunkError::Malformed {
2002 reason: format!(
2003 "Time column's cached time range is wrong.\
2004 Found a time value of {time} while its time range is {time_range:?}",
2005 ),
2006 });
2007 }
2008
2009 if time == TimeInt::STATIC.as_i64() {
2010 return Err(ChunkError::Malformed {
2011 reason: "A chunk's timeline should never contain a static time value."
2012 .to_owned(),
2013 });
2014 }
2015 }
2016 }
2017
2018 Ok(())
2019 }
2020}