Skip to main content

re_chunk/
merge.rs

1use arrow::array::{Array as _, FixedSizeBinaryArray, ListArray as ArrowListArray};
2use arrow::buffer::ScalarBuffer as ArrowScalarBuffer;
3use itertools::{Itertools as _, izip};
4use nohash_hasher::IntMap;
5use re_arrow_util::ArrowArrayDowncastRef as _;
6use re_types_core::SerializedComponentColumn;
7
8use crate::chunk::ChunkComponents;
9use crate::{Chunk, ChunkError, ChunkId, ChunkResult, TimeColumn};
10
11// ---
12
13impl Chunk {
14    /// Concatenates two `Chunk`s into a new one.
15    ///
16    /// The order of the arguments matter: `self`'s contents will precede `rhs`' contents in the
17    /// returned `Chunk`.
18    ///
19    /// This will return an error if the chunks are not [concatenable].
20    ///
21    /// [concatenable]: [`Chunk::concatenable`]
22    pub fn concatenated(&self, rhs: &Self) -> ChunkResult<Self> {
23        re_tracing::profile_function!(format!(
24            "lhs={} rhs={}",
25            re_format::format_uint(self.num_rows()),
26            re_format::format_uint(rhs.num_rows())
27        ));
28
29        let cl = self;
30        let cr = rhs;
31
32        if !cl.concatenable(cr) {
33            return Err(ChunkError::Malformed {
34                reason: format!("cannot concatenate incompatible Chunks:\n{cl}\n{cr}"),
35            });
36        }
37
38        let Some((_cl0, cl1)) = cl.row_id_range() else {
39            return Ok(cr.clone()); // `cl` is empty (`cr` might be too, that's fine)
40        };
41        let Some((cr0, _cr1)) = cr.row_id_range() else {
42            return Ok(cl.clone());
43        };
44
45        let is_sorted = cl.is_sorted && cr.is_sorted && cl1 <= cr0;
46
47        let row_ids = {
48            re_tracing::profile_scope!("row_ids");
49
50            let row_ids = re_arrow_util::concat_arrays(&[&cl.row_ids, &cr.row_ids])?;
51            #[expect(clippy::unwrap_used)]
52            // concatenating 2 RowId arrays must yield another RowId array
53            row_ids
54                .downcast_array_ref::<FixedSizeBinaryArray>()
55                .unwrap()
56                .clone()
57        };
58
59        // NOTE: We know they are the same set, and they are in a btree => we can zip them.
60        let timelines = {
61            re_tracing::profile_scope!("timelines");
62            izip!(self.timelines.iter(), rhs.timelines.iter())
63                .filter_map(
64                    |((lhs_timeline, lhs_time_chunk), (rhs_timeline, rhs_time_chunk))| {
65                        re_log::debug_assert_eq!(lhs_timeline, rhs_timeline);
66                        lhs_time_chunk
67                            .concatenated(rhs_time_chunk)
68                            .map(|time_column| (*lhs_timeline, time_column))
69                    },
70                )
71                .collect()
72        };
73
74        let lhs_per_component: IntMap<_, _> = cl
75            .components
76            .iter()
77            .map(|(component, list_array)| (*component, list_array))
78            .collect();
79        let rhs_per_component: IntMap<_, _> = cr
80            .components
81            .iter()
82            .map(|(component, list_array)| (*component, list_array))
83            .collect();
84
85        // First pass: concat right onto left.
86        let mut components: ChunkComponents = {
87            re_tracing::profile_scope!("components (r2l)");
88            lhs_per_component
89                .values()
90                .filter_map(|lhs_column| {
91                    re_tracing::profile_scope!(lhs_column.descriptor.to_string());
92                    if let Some(&rhs_column) =
93                        rhs_per_component.get(&lhs_column.descriptor.component)
94                    {
95                        if lhs_column.descriptor != rhs_column.descriptor {
96                            re_log::warn_once!("lhs and rhs have different component descriptors for the same component: {} != {}", lhs_column.descriptor, rhs_column.descriptor);
97                        }
98
99                        re_tracing::profile_scope!(format!(
100                            "concat (lhs={} rhs={})",
101                            re_format::format_uint(lhs_column.list_array.values().len()),
102                            re_format::format_uint(rhs_column.list_array.values().len()),
103                        ));
104
105                        let list_array =
106                            re_arrow_util::concat_arrays(&[&lhs_column.list_array, &rhs_column.list_array]).ok()?;
107                        let list_array = list_array.downcast_array_ref::<ArrowListArray>()?.clone();
108
109                        Some((lhs_column.descriptor.clone(), list_array))
110                    } else {
111                        re_tracing::profile_scope!("pad");
112                        Some((
113                            lhs_column.descriptor.clone(),
114                            re_arrow_util::pad_list_array_back(
115                                &lhs_column.list_array,
116                                self.num_rows() + rhs.num_rows(),
117                            ),
118                        ))
119                    }
120                })
121                .collect()
122        };
123
124        // Second pass: concat left onto right, where necessary.
125        {
126            re_tracing::profile_scope!("components (l2r)");
127            let rhs = rhs_per_component
128                .values()
129                .filter_map(|rhs_column| {
130                    if components.contains_key(&rhs_column.descriptor.component) {
131                        // Already did that one during the first pass.
132                        return None;
133                    }
134
135                    re_tracing::profile_scope!(rhs_column.descriptor.component.to_string());
136
137                    if let Some(&lhs_column) =
138                        lhs_per_component.get(&rhs_column.descriptor.component)
139                    {
140                        if lhs_column.descriptor != rhs_column.descriptor {
141                            re_log::warn_once!("lhs and rhs have different component descriptors for the same component: {} != {}", lhs_column.descriptor, rhs_column.descriptor);
142                        }
143
144                        re_tracing::profile_scope!(format!(
145                            "concat (lhs={} rhs={})",
146                            re_format::format_uint(lhs_column.list_array.values().len()),
147                            re_format::format_uint(rhs_column.list_array.values().len()),
148                        ));
149
150                        let list_array =
151                            re_arrow_util::concat_arrays(&[&lhs_column.list_array, &rhs_column.list_array]).ok()?;
152                        let list_array = list_array.downcast_array_ref::<ArrowListArray>()?.clone();
153
154                        Some((rhs_column.descriptor.component, SerializedComponentColumn::new(list_array, rhs_column.descriptor.clone())))
155                    } else {
156                        re_tracing::profile_scope!("pad");
157                        Some((
158                            rhs_column.descriptor.component,
159                            SerializedComponentColumn::new(
160                                re_arrow_util::pad_list_array_front(
161                                    &rhs_column.list_array,
162                                    self.num_rows() + rhs.num_rows(),
163                                ),
164                                rhs_column.descriptor.clone(),
165                            ),
166                        ))
167                    }
168                })
169                .collect_vec();
170            components.extend(rhs);
171        }
172
173        let chunk = Self {
174            id: ChunkId::new(),
175            entity_path: cl.entity_path.clone(),
176            heap_size_bytes: Default::default(),
177            is_sorted,
178            row_ids,
179            timelines,
180            components,
181        };
182
183        chunk.sanity_check()?;
184
185        Ok(chunk)
186    }
187
188    /// Returns `true` if `self` and `rhs` overlap on their `RowId` range.
189    #[inline]
190    pub fn overlaps_on_row_id(&self, rhs: &Self) -> bool {
191        let cl = self;
192        let cr = rhs;
193
194        let Some((cl0, cl1)) = cl.row_id_range() else {
195            return false;
196        };
197        let Some((cr0, cr1)) = cr.row_id_range() else {
198            return false;
199        };
200
201        cl0 <= cr1 && cr0 <= cl1
202    }
203
204    /// Returns `true` if `self` and `rhs` overlap on any of their time range(s).
205    ///
206    /// This does not imply that they share the same exact set of timelines.
207    #[inline]
208    pub fn overlaps_on_time(&self, rhs: &Self) -> bool {
209        self.timelines.iter().any(|(timeline, cl_time_chunk)| {
210            if let Some(cr_time_chunk) = rhs.timelines.get(timeline) {
211                cl_time_chunk
212                    .time_range()
213                    .intersects(cr_time_chunk.time_range())
214            } else {
215                false
216            }
217        })
218    }
219
220    /// Returns `true` if both chunks share the same entity path.
221    #[inline]
222    pub fn same_entity_paths(&self, rhs: &Self) -> bool {
223        self.entity_path() == rhs.entity_path()
224    }
225
226    /// Returns `true` if both chunks contains the same set of timelines.
227    #[inline]
228    pub fn same_timelines(&self, rhs: &Self) -> bool {
229        self.timelines.len() == rhs.timelines.len()
230            && self.timelines.keys().collect_vec() == rhs.timelines.keys().collect_vec()
231    }
232
233    /// Returns `true` if both chunks share the same datatypes for the components that
234    /// _they have in common_.
235    ///
236    /// Ignores potential differences in component descriptors.
237    #[inline]
238    pub fn same_datatypes(&self, rhs: &Self) -> bool {
239        self.components.values().all(|lhs_column| {
240            if let Some(rhs_column) = rhs.components.get(lhs_column.descriptor.component) {
241                lhs_column.list_array.data_type() == rhs_column.list_array.data_type()
242            } else {
243                true
244            }
245        })
246    }
247
248    /// Returns true if two chunks are concatenable.
249    ///
250    /// To be concatenable, two chunks must:
251    /// * Share the same entity path.
252    /// * Share the same exact set of timelines.
253    /// * Use the same datatypes for the components they have in common.
254    #[inline]
255    pub fn concatenable(&self, rhs: &Self) -> bool {
256        self.same_entity_paths(rhs) && self.same_timelines(rhs) && self.same_datatypes(rhs)
257    }
258}
259
260impl TimeColumn {
261    /// Concatenates two [`TimeColumn`]s into a new one.
262    ///
263    /// The order of the arguments matter: `self`'s contents will precede `rhs`' contents in the
264    /// returned [`TimeColumn`].
265    ///
266    /// This will return `None` if the time chunks do not share the same timeline.
267    pub fn concatenated(&self, rhs: &Self) -> Option<Self> {
268        if self.timeline != rhs.timeline {
269            return None;
270        }
271        re_tracing::profile_function!();
272
273        let is_sorted =
274            self.is_sorted && rhs.is_sorted && self.time_range.max() <= rhs.time_range.min();
275
276        let time_range = self.time_range.union(rhs.time_range);
277
278        let times = self
279            .times_raw()
280            .iter()
281            .chain(rhs.times_raw())
282            .copied()
283            .collect_vec();
284        let times = ArrowScalarBuffer::from(times);
285
286        Some(Self {
287            timeline: self.timeline,
288            times,
289            is_sorted,
290            time_range,
291        })
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use re_log_types::example_components::{MyColor, MyLabel, MyPoint, MyPoint64, MyPoints};
298
299    use super::*;
300    use crate::{Chunk, RowId, Timeline};
301
302    #[test]
303    fn homogeneous() -> anyhow::Result<()> {
304        let entity_path = "my/entity";
305
306        let row_id1 = RowId::new();
307        let row_id2 = RowId::new();
308        let row_id3 = RowId::new();
309        let row_id4 = RowId::new();
310        let row_id5 = RowId::new();
311
312        let timepoint1 = [
313            (Timeline::log_time(), 1000),
314            (Timeline::new_sequence("frame"), 1),
315        ];
316        let timepoint2 = [
317            (Timeline::log_time(), 1032),
318            (Timeline::new_sequence("frame"), 3),
319        ];
320        let timepoint3 = [
321            (Timeline::log_time(), 1064),
322            (Timeline::new_sequence("frame"), 5),
323        ];
324        let timepoint4 = [
325            (Timeline::log_time(), 1096),
326            (Timeline::new_sequence("frame"), 7),
327        ];
328        let timepoint5 = [
329            (Timeline::log_time(), 1128),
330            (Timeline::new_sequence("frame"), 9),
331        ];
332
333        let points1 = &[MyPoint::new(1.0, 1.0), MyPoint::new(2.0, 2.0)];
334        let points3 = &[
335            MyPoint::new(3.0, 3.0),
336            MyPoint::new(4.0, 4.0),
337            MyPoint::new(5.0, 5.0),
338        ];
339        let points5 = &[MyPoint::new(6.0, 7.0)];
340
341        let colors2 = &[MyColor::from_rgb(1, 1, 1)];
342        let colors4 = &[MyColor::from_rgb(2, 2, 2), MyColor::from_rgb(3, 3, 3)];
343
344        let labels2 = &[
345            MyLabel("a".into()),
346            MyLabel("b".into()),
347            MyLabel("c".into()),
348        ];
349        let labels5 = &[MyLabel("d".into())];
350
351        let chunk1 = Chunk::builder(entity_path)
352            .with_component_batches(
353                row_id1,
354                timepoint1,
355                [(MyPoints::descriptor_points(), points1 as _)],
356            )
357            .with_component_batches(
358                row_id2,
359                timepoint2,
360                [
361                    (MyPoints::descriptor_colors(), colors2 as _),
362                    (MyPoints::descriptor_labels(), labels2 as _),
363                ],
364            )
365            .with_component_batches(
366                row_id3,
367                timepoint3,
368                [(MyPoints::descriptor_points(), points3 as _)],
369            )
370            .build()?;
371
372        let chunk2 = Chunk::builder(entity_path)
373            .with_component_batches(
374                row_id4,
375                timepoint4,
376                [(MyPoints::descriptor_colors(), colors4 as _)],
377            )
378            .with_component_batches(
379                row_id5,
380                timepoint5,
381                [
382                    (MyPoints::descriptor_points(), points5 as _),
383                    (MyPoints::descriptor_labels(), labels5 as _),
384                ],
385            )
386            .build()?;
387
388        eprintln!("chunk1:\n{chunk1}");
389        eprintln!("chunk2:\n{chunk2}");
390
391        {
392            assert!(chunk1.concatenable(&chunk2));
393
394            let got = chunk1.concatenated(&chunk2).unwrap();
395            let expected = Chunk::builder_with_id(got.id(), entity_path)
396                .with_sparse_component_batches(
397                    row_id1,
398                    timepoint1,
399                    [
400                        (MyPoints::descriptor_points(), Some(points1 as _)),
401                        (MyPoints::descriptor_colors(), None),
402                        (MyPoints::descriptor_labels(), None),
403                    ],
404                )
405                .with_sparse_component_batches(
406                    row_id2,
407                    timepoint2,
408                    [
409                        (MyPoints::descriptor_points(), None),
410                        (MyPoints::descriptor_colors(), Some(colors2 as _)),
411                        (MyPoints::descriptor_labels(), Some(labels2 as _)),
412                    ],
413                )
414                .with_sparse_component_batches(
415                    row_id3,
416                    timepoint3,
417                    [
418                        (MyPoints::descriptor_points(), Some(points3 as _)),
419                        (MyPoints::descriptor_colors(), None),
420                        (MyPoints::descriptor_labels(), None),
421                    ],
422                )
423                .with_sparse_component_batches(
424                    row_id4,
425                    timepoint4,
426                    [
427                        (MyPoints::descriptor_points(), None),
428                        (MyPoints::descriptor_colors(), Some(colors4 as _)),
429                        (MyPoints::descriptor_labels(), None),
430                    ],
431                )
432                .with_sparse_component_batches(
433                    row_id5,
434                    timepoint5,
435                    [
436                        (MyPoints::descriptor_points(), Some(points5 as _)),
437                        (MyPoints::descriptor_colors(), None),
438                        (MyPoints::descriptor_labels(), Some(labels5 as _)),
439                    ],
440                )
441                .build()?;
442
443            eprintln!("got:\n{got}");
444            eprintln!("expected:\n{expected}");
445
446            assert_eq!(
447                expected,
448                got,
449                "{}",
450                similar_asserts::SimpleDiff::from_str(
451                    &format!("{got}"),
452                    &format!("{expected}"),
453                    "got",
454                    "expected",
455                ),
456            );
457
458            assert!(got.is_sorted());
459            assert!(got.is_time_sorted());
460        }
461        {
462            assert!(chunk2.concatenable(&chunk1));
463
464            let got = chunk2.concatenated(&chunk1).unwrap();
465            let expected = Chunk::builder_with_id(got.id(), entity_path)
466                .with_sparse_component_batches(
467                    row_id4,
468                    timepoint4,
469                    [
470                        (MyPoints::descriptor_points(), None),
471                        (MyPoints::descriptor_colors(), Some(colors4 as _)),
472                        (MyPoints::descriptor_labels(), None),
473                    ],
474                )
475                .with_sparse_component_batches(
476                    row_id5,
477                    timepoint5,
478                    [
479                        (MyPoints::descriptor_points(), Some(points5 as _)),
480                        (MyPoints::descriptor_colors(), None),
481                        (MyPoints::descriptor_labels(), Some(labels5 as _)),
482                    ],
483                )
484                .with_sparse_component_batches(
485                    row_id1,
486                    timepoint1,
487                    [
488                        (MyPoints::descriptor_points(), Some(points1 as _)),
489                        (MyPoints::descriptor_colors(), None),
490                        (MyPoints::descriptor_labels(), None),
491                    ],
492                )
493                .with_sparse_component_batches(
494                    row_id2,
495                    timepoint2,
496                    [
497                        (MyPoints::descriptor_points(), None),
498                        (MyPoints::descriptor_colors(), Some(colors2 as _)),
499                        (MyPoints::descriptor_labels(), Some(labels2 as _)),
500                    ],
501                )
502                .with_sparse_component_batches(
503                    row_id3,
504                    timepoint3,
505                    [
506                        (MyPoints::descriptor_points(), Some(points3 as _)),
507                        (MyPoints::descriptor_colors(), None),
508                        (MyPoints::descriptor_labels(), None),
509                    ],
510                )
511                .build()?;
512
513            eprintln!("got:\n{got}");
514            eprintln!("expected:\n{expected}");
515
516            assert_eq!(
517                expected,
518                got,
519                "{}",
520                similar_asserts::SimpleDiff::from_str(
521                    &format!("{got}"),
522                    &format!("{expected}"),
523                    "got",
524                    "expected",
525                ),
526            );
527
528            assert!(!got.is_sorted());
529            assert!(!got.is_time_sorted());
530        }
531
532        Ok(())
533    }
534
535    #[test]
536    fn heterogeneous() -> anyhow::Result<()> {
537        let entity_path = "my/entity";
538
539        let row_id1 = RowId::new();
540        let row_id2 = RowId::new();
541        let row_id3 = RowId::new();
542        let row_id4 = RowId::new();
543        let row_id5 = RowId::new();
544
545        let timepoint1 = [
546            (Timeline::log_time(), 1000),
547            (Timeline::new_sequence("frame"), 1),
548        ];
549        let timepoint2 = [
550            (Timeline::log_time(), 1032),
551            (Timeline::new_sequence("frame"), 3),
552        ];
553        let timepoint3 = [
554            (Timeline::log_time(), 1064),
555            (Timeline::new_sequence("frame"), 5),
556        ];
557        let timepoint4 = [
558            (Timeline::log_time(), 1096),
559            (Timeline::new_sequence("frame"), 7),
560        ];
561        let timepoint5 = [
562            (Timeline::log_time(), 1128),
563            (Timeline::new_sequence("frame"), 9),
564        ];
565
566        let points1 = &[MyPoint::new(1.0, 1.0), MyPoint::new(2.0, 2.0)];
567        let points3 = &[MyPoint::new(6.0, 7.0)];
568
569        let colors4 = &[MyColor::from_rgb(1, 1, 1)];
570        let colors5 = &[MyColor::from_rgb(2, 2, 2), MyColor::from_rgb(3, 3, 3)];
571
572        let labels1 = &[MyLabel("a".into())];
573        let labels2 = &[MyLabel("b".into())];
574        let labels3 = &[MyLabel("c".into())];
575        let labels4 = &[MyLabel("d".into())];
576        let labels5 = &[MyLabel("e".into())];
577
578        let chunk1 = Chunk::builder(entity_path)
579            .with_component_batches(
580                row_id1,
581                timepoint1,
582                [
583                    (MyPoints::descriptor_points(), points1 as _),
584                    (MyPoints::descriptor_labels(), labels1 as _),
585                ],
586            )
587            .with_component_batches(
588                row_id2,
589                timepoint2,
590                [(MyPoints::descriptor_labels(), labels2 as _)],
591            )
592            .with_component_batches(
593                row_id3,
594                timepoint3,
595                [
596                    (MyPoints::descriptor_points(), points3 as _),
597                    (MyPoints::descriptor_labels(), labels3 as _),
598                ],
599            )
600            .build()?;
601
602        let chunk2 = Chunk::builder(entity_path)
603            .with_component_batches(
604                row_id4,
605                timepoint4,
606                [
607                    (MyPoints::descriptor_colors(), colors4 as _),
608                    (MyPoints::descriptor_labels(), labels4 as _),
609                ],
610            )
611            .with_component_batches(
612                row_id5,
613                timepoint5,
614                [
615                    (MyPoints::descriptor_colors(), colors5 as _),
616                    (MyPoints::descriptor_labels(), labels5 as _),
617                ],
618            )
619            .build()?;
620
621        eprintln!("chunk1:\n{chunk1}");
622        eprintln!("chunk2:\n{chunk2}");
623
624        {
625            assert!(chunk1.concatenable(&chunk2));
626
627            let got = chunk1.concatenated(&chunk2).unwrap();
628            let expected = Chunk::builder_with_id(got.id(), entity_path)
629                .with_sparse_component_batches(
630                    row_id1,
631                    timepoint1,
632                    [
633                        (MyPoints::descriptor_points(), Some(points1 as _)),
634                        (MyPoints::descriptor_colors(), None),
635                        (MyPoints::descriptor_labels(), Some(labels1 as _)),
636                    ],
637                )
638                .with_sparse_component_batches(
639                    row_id2,
640                    timepoint2,
641                    [
642                        (MyPoints::descriptor_points(), None),
643                        (MyPoints::descriptor_colors(), None),
644                        (MyPoints::descriptor_labels(), Some(labels2 as _)),
645                    ],
646                )
647                .with_sparse_component_batches(
648                    row_id3,
649                    timepoint3,
650                    [
651                        (MyPoints::descriptor_points(), Some(points3 as _)),
652                        (MyPoints::descriptor_colors(), None),
653                        (MyPoints::descriptor_labels(), Some(labels3 as _)),
654                    ],
655                )
656                .with_sparse_component_batches(
657                    row_id4,
658                    timepoint4,
659                    [
660                        (MyPoints::descriptor_points(), None),
661                        (MyPoints::descriptor_colors(), Some(colors4 as _)),
662                        (MyPoints::descriptor_labels(), Some(labels4 as _)),
663                    ],
664                )
665                .with_sparse_component_batches(
666                    row_id5,
667                    timepoint5,
668                    [
669                        (MyPoints::descriptor_points(), None),
670                        (MyPoints::descriptor_colors(), Some(colors5 as _)),
671                        (MyPoints::descriptor_labels(), Some(labels5 as _)),
672                    ],
673                )
674                .build()?;
675
676            eprintln!("got:\n{got}");
677            eprintln!("expected:\n{expected}");
678
679            assert_eq!(
680                expected,
681                got,
682                "{}",
683                similar_asserts::SimpleDiff::from_str(
684                    &format!("{got}"),
685                    &format!("{expected}"),
686                    "got",
687                    "expected",
688                ),
689            );
690
691            assert!(got.is_sorted());
692            assert!(got.is_time_sorted());
693        }
694        {
695            assert!(chunk2.concatenable(&chunk1));
696
697            let got = chunk2.concatenated(&chunk1).unwrap();
698            let expected = Chunk::builder_with_id(got.id(), entity_path)
699                .with_sparse_component_batches(
700                    row_id4,
701                    timepoint4,
702                    [
703                        (MyPoints::descriptor_points(), None),
704                        (MyPoints::descriptor_colors(), Some(colors4 as _)),
705                        (MyPoints::descriptor_labels(), Some(labels4 as _)),
706                    ],
707                )
708                .with_sparse_component_batches(
709                    row_id5,
710                    timepoint5,
711                    [
712                        (MyPoints::descriptor_points(), None),
713                        (MyPoints::descriptor_colors(), Some(colors5 as _)),
714                        (MyPoints::descriptor_labels(), Some(labels5 as _)),
715                    ],
716                )
717                .with_sparse_component_batches(
718                    row_id1,
719                    timepoint1,
720                    [
721                        (MyPoints::descriptor_points(), Some(points1 as _)),
722                        (MyPoints::descriptor_colors(), None),
723                        (MyPoints::descriptor_labels(), Some(labels1 as _)),
724                    ],
725                )
726                .with_sparse_component_batches(
727                    row_id2,
728                    timepoint2,
729                    [
730                        (MyPoints::descriptor_points(), None),
731                        (MyPoints::descriptor_colors(), None),
732                        (MyPoints::descriptor_labels(), Some(labels2 as _)),
733                    ],
734                )
735                .with_sparse_component_batches(
736                    row_id3,
737                    timepoint3,
738                    [
739                        (MyPoints::descriptor_points(), Some(points3 as _)),
740                        (MyPoints::descriptor_colors(), None),
741                        (MyPoints::descriptor_labels(), Some(labels3 as _)),
742                    ],
743                )
744                .build()?;
745
746            eprintln!("got:\n{got}");
747            eprintln!("expected:\n{expected}");
748
749            assert_eq!(
750                expected,
751                got,
752                "{}",
753                similar_asserts::SimpleDiff::from_str(
754                    &format!("{got}"),
755                    &format!("{expected}"),
756                    "got",
757                    "expected",
758                ),
759            );
760
761            assert!(!got.is_sorted());
762            assert!(!got.is_time_sorted());
763        }
764
765        Ok(())
766    }
767
768    #[test]
769    fn malformed() -> anyhow::Result<()> {
770        // Different entity paths
771        {
772            let entity_path1 = "ent1";
773            let entity_path2 = "ent2";
774
775            let row_id1 = RowId::new();
776            let row_id2 = RowId::new();
777
778            let timepoint1 = [
779                (Timeline::log_time(), 1000),
780                (Timeline::new_sequence("frame"), 1),
781            ];
782            let timepoint2 = [
783                (Timeline::log_time(), 1032),
784                (Timeline::new_sequence("frame"), 3),
785            ];
786
787            let points1 = &[MyPoint::new(1.0, 1.0)];
788            let points2 = &[MyPoint::new(2.0, 2.0)];
789
790            let chunk1 = Chunk::builder(entity_path1)
791                .with_component_batches(
792                    row_id1,
793                    timepoint1,
794                    [(MyPoints::descriptor_points(), points1 as _)],
795                )
796                .build()?;
797
798            let chunk2 = Chunk::builder(entity_path2)
799                .with_component_batches(
800                    row_id2,
801                    timepoint2,
802                    [(MyPoints::descriptor_points(), points2 as _)],
803                )
804                .build()?;
805
806            assert!(matches!(
807                chunk1.concatenated(&chunk2),
808                Err(ChunkError::Malformed { .. })
809            ));
810            assert!(matches!(
811                chunk2.concatenated(&chunk1),
812                Err(ChunkError::Malformed { .. })
813            ));
814        }
815
816        // Different timelines
817        {
818            let entity_path = "ent";
819
820            let row_id1 = RowId::new();
821            let row_id2 = RowId::new();
822
823            let timepoint1 = [(Timeline::new_sequence("frame"), 1)];
824            let timepoint2 = [(Timeline::log_time(), 1032)];
825
826            let points1 = &[MyPoint::new(1.0, 1.0)];
827            let points2 = &[MyPoint::new(2.0, 2.0)];
828
829            let chunk1 = Chunk::builder(entity_path)
830                .with_component_batches(
831                    row_id1,
832                    timepoint1,
833                    [(MyPoints::descriptor_points(), points1 as _)],
834                )
835                .build()?;
836
837            let chunk2 = Chunk::builder(entity_path)
838                .with_component_batches(
839                    row_id2,
840                    timepoint2,
841                    [(MyPoints::descriptor_points(), points2 as _)],
842                )
843                .build()?;
844
845            assert!(matches!(
846                chunk1.concatenated(&chunk2),
847                Err(ChunkError::Malformed { .. })
848            ));
849            assert!(matches!(
850                chunk2.concatenated(&chunk1),
851                Err(ChunkError::Malformed { .. })
852            ));
853        }
854
855        // Different datatypes
856        {
857            let entity_path = "ent";
858
859            let row_id1 = RowId::new();
860            let row_id2 = RowId::new();
861
862            let timepoint1 = [(Timeline::new_sequence("frame"), 1)];
863            let timepoint2 = [(Timeline::new_sequence("frame"), 2)];
864
865            let points32bit =
866                <MyPoint as re_types_core::ComponentBatch>::to_arrow(&MyPoint::new(1.0, 1.0))?;
867            let points64bit =
868                <MyPoint64 as re_types_core::ComponentBatch>::to_arrow(&MyPoint64::new(1.0, 1.0))?;
869
870            let chunk1 = Chunk::builder(entity_path)
871                .with_row(
872                    row_id1,
873                    timepoint1,
874                    [
875                        (MyPoints::descriptor_points(), points32bit), //
876                    ],
877                )
878                .build()?;
879
880            let chunk2 = Chunk::builder(entity_path)
881                .with_row(
882                    row_id2,
883                    timepoint2,
884                    [
885                        (MyPoints::descriptor_points(), points64bit), //
886                    ],
887                )
888                .build()?;
889
890            assert!(matches!(
891                chunk1.concatenated(&chunk2),
892                Err(ChunkError::Malformed { .. })
893            ));
894            assert!(matches!(
895                chunk2.concatenated(&chunk1),
896                Err(ChunkError::Malformed { .. })
897            ));
898        }
899
900        Ok(())
901    }
902}