re_chunk/
merge.rs

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