Skip to main content

risingwave_common/array/
stream_chunk.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Display;
16use std::marker::PhantomData;
17use std::mem::size_of;
18use std::ops::{Deref, DerefMut};
19use std::sync::Arc;
20use std::{fmt, mem};
21
22use either::Either;
23use enum_as_inner::EnumAsInner;
24use itertools::Itertools;
25use rand::prelude::SmallRng;
26use rand::{Rng, SeedableRng};
27use risingwave_common_estimate_size::EstimateSize;
28use risingwave_pb::data::{PbOp, PbStreamChunk};
29
30use super::stream_chunk_builder::StreamChunkBuilder;
31use super::{ArrayImpl, ArrayRef, ArrayResult, DataChunkTestExt, RowRef};
32use crate::array::DataChunk;
33use crate::bitmap::{Bitmap, BitmapBuilder};
34use crate::catalog::Schema;
35use crate::field_generator::VarcharProperty;
36use crate::row::Row;
37use crate::types::{DataType, DefaultOrdered, ToText};
38
39/// `Op` represents three operations in `StreamChunk`.
40///
41/// `UpdateDelete` and `UpdateInsert` are semantically equivalent to `Delete` and `Insert`
42/// but always appear in pairs to represent an update operation. It's guaranteed that
43/// they are adjacent to each other in the same `StreamChunk`, and the stream key of the two
44/// rows are the same.
45#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, EnumAsInner)]
46pub enum Op {
47    Insert,
48    Delete,
49    UpdateDelete,
50    UpdateInsert,
51}
52
53impl Op {
54    pub fn to_protobuf(self) -> PbOp {
55        match self {
56            Op::Insert => PbOp::Insert,
57            Op::Delete => PbOp::Delete,
58            Op::UpdateInsert => PbOp::UpdateInsert,
59            Op::UpdateDelete => PbOp::UpdateDelete,
60        }
61    }
62
63    pub fn from_protobuf(prost: &i32) -> ArrayResult<Op> {
64        let op = match PbOp::try_from(*prost) {
65            Ok(PbOp::Insert) => Op::Insert,
66            Ok(PbOp::Delete) => Op::Delete,
67            Ok(PbOp::UpdateInsert) => Op::UpdateInsert,
68            Ok(PbOp::UpdateDelete) => Op::UpdateDelete,
69            Ok(PbOp::Unspecified) => unreachable!(),
70            Err(_) => bail!("No such op type"),
71        };
72        Ok(op)
73    }
74
75    /// convert `UpdateDelete` to `Delete` and `UpdateInsert` to Insert
76    pub fn normalize_update(self) -> Op {
77        match self {
78            Op::Insert => Op::Insert,
79            Op::Delete => Op::Delete,
80            Op::UpdateDelete => Op::Delete,
81            Op::UpdateInsert => Op::Insert,
82        }
83    }
84
85    pub fn to_i16(self) -> i16 {
86        match self {
87            Op::Insert => 1,
88            Op::Delete => 2,
89            Op::UpdateInsert => 3,
90            Op::UpdateDelete => 4,
91        }
92    }
93
94    pub fn to_varchar(self) -> String {
95        match self {
96            Op::Insert => "Insert",
97            Op::Delete => "Delete",
98            Op::UpdateInsert => "UpdateInsert",
99            Op::UpdateDelete => "UpdateDelete",
100        }
101        .to_owned()
102    }
103}
104
105/// `StreamChunk` is used to pass data over the streaming pathway.
106#[derive(Clone, PartialEq)]
107pub struct StreamChunk {
108    // TODO: Optimize using bitmap
109    ops: Arc<[Op]>,
110    data: DataChunk,
111}
112
113impl Default for StreamChunk {
114    /// Create a 0-row-0-col `StreamChunk`. Only used in some existing tests.
115    /// This is NOT the same as an **empty** chunk, which has 0 rows but with
116    /// columns aligned with executor schema.
117    fn default() -> Self {
118        Self {
119            ops: Arc::new([]),
120            data: DataChunk::new(vec![], 0),
121        }
122    }
123}
124
125impl StreamChunk {
126    /// Create a new `StreamChunk` with given ops and columns.
127    pub fn new(ops: impl Into<Arc<[Op]>>, columns: Vec<ArrayRef>) -> Self {
128        let ops = ops.into();
129        let visibility = Bitmap::ones(ops.len());
130        Self::with_visibility(ops, columns, visibility)
131    }
132
133    /// Create a new `StreamChunk` with given ops, columns and visibility.
134    pub fn with_visibility(
135        ops: impl Into<Arc<[Op]>>,
136        columns: Vec<ArrayRef>,
137        visibility: Bitmap,
138    ) -> Self {
139        let ops = ops.into();
140        for col in &columns {
141            assert_eq!(col.len(), ops.len());
142        }
143        let data = DataChunk::new(columns, visibility);
144        StreamChunk { ops, data }
145    }
146
147    /// Build a `StreamChunk` from rows.
148    ///
149    /// Panics if the `rows` is empty.
150    ///
151    /// Should prefer using [`StreamChunkBuilder`] instead to avoid unnecessary
152    /// allocation of rows.
153    pub fn from_rows(rows: &[(Op, impl Row)], data_types: &[DataType]) -> Self {
154        let mut builder = StreamChunkBuilder::unlimited(data_types.to_vec(), Some(rows.len()));
155
156        for (op, row) in rows {
157            let none = builder.append_row(*op, row);
158            debug_assert!(none.is_none());
159        }
160
161        builder.take().expect("chunk should not be empty")
162    }
163
164    pub fn empty(data_types: &[DataType]) -> Self {
165        StreamChunkBuilder::build_empty(data_types.to_vec())
166    }
167
168    /// Get the reference of the underlying data chunk.
169    pub fn data_chunk(&self) -> &DataChunk {
170        &self.data
171    }
172
173    /// Removes the invisible rows based on `visibility`. Returns a new compacted chunk
174    /// with all rows visible.
175    ///
176    /// This does not change the visible content of the chunk. Not to be confused with
177    /// `StreamChunkCompactor`, which removes unnecessary changes based on the key.
178    ///
179    /// See [`DataChunk::compact_vis`] for more details.
180    pub fn compact_vis(self) -> Self {
181        if self.is_vis_compacted() {
182            return self;
183        }
184
185        let (ops, columns, visibility) = self.into_inner();
186
187        let cardinality = visibility
188            .iter()
189            .fold(0, |vis_cnt, vis| vis_cnt + vis as usize);
190        let columns: Vec<_> = columns
191            .into_iter()
192            .map(|col| col.compact_vis(&visibility, cardinality).into())
193            .collect();
194        let mut new_ops = Vec::with_capacity(cardinality);
195        for idx in visibility.iter_ones() {
196            new_ops.push(ops[idx]);
197        }
198        StreamChunk::new(new_ops, columns)
199    }
200
201    /// Split the `StreamChunk` into multiple chunks with the given size at most.
202    ///
203    /// When the total cardinality of all the chunks is not evenly divided by the `size`,
204    /// the last new chunk will be the remainder.
205    ///
206    /// For consecutive `UpdateDelete` and `UpdateInsert`, they will be kept in one chunk.
207    /// As a result, some chunks may have `size + 1` rows.
208    pub fn split(&self, size: usize) -> Vec<Self> {
209        let mut builder = StreamChunkBuilder::new(size, self.data_types());
210        let mut outputs = Vec::new();
211
212        // TODO: directly append the chunk.
213        for (op, row) in self.rows() {
214            if let Some(chunk) = builder.append_row(op, row) {
215                outputs.push(chunk);
216            }
217        }
218        if let Some(output) = builder.take() {
219            outputs.push(output);
220        }
221
222        outputs
223    }
224
225    pub fn into_parts(self) -> (DataChunk, Arc<[Op]>) {
226        (self.data, self.ops)
227    }
228
229    pub fn from_parts(ops: impl Into<Arc<[Op]>>, data_chunk: DataChunk) -> Self {
230        let (columns, vis) = data_chunk.into_parts();
231        Self::with_visibility(ops, columns, vis)
232    }
233
234    pub fn into_inner(self) -> (Arc<[Op]>, Vec<ArrayRef>, Bitmap) {
235        let (columns, vis) = self.data.into_parts();
236        (self.ops, columns, vis)
237    }
238
239    pub fn to_protobuf(&self) -> PbStreamChunk {
240        if !self.is_vis_compacted() {
241            return self.clone().compact_vis().to_protobuf();
242        }
243        PbStreamChunk {
244            cardinality: self.cardinality() as u32,
245            ops: self.ops.iter().map(|op| op.to_protobuf() as i32).collect(),
246            columns: self.columns().iter().map(|col| col.to_protobuf()).collect(),
247        }
248    }
249
250    pub fn from_protobuf(prost: &PbStreamChunk) -> ArrayResult<Self> {
251        let cardinality = prost.get_cardinality() as usize;
252        let mut ops = Vec::with_capacity(cardinality);
253        for op in prost.get_ops() {
254            ops.push(Op::from_protobuf(op)?);
255        }
256        let mut columns = vec![];
257        for column in prost.get_columns() {
258            columns.push(ArrayImpl::from_protobuf(column, cardinality)?.into());
259        }
260        Ok(StreamChunk::new(ops, columns))
261    }
262
263    pub fn ops(&self) -> &[Op] {
264        &self.ops
265    }
266
267    /// Returns a table-like text representation of the `StreamChunk`.
268    pub fn to_pretty(&self) -> impl Display + use<> {
269        self.to_pretty_inner(None)
270    }
271
272    /// Returns a table-like text representation of the `StreamChunk` with a header of column names
273    /// from the given `schema`.
274    pub fn to_pretty_with_schema(&self, schema: &Schema) -> impl Display + use<> {
275        self.to_pretty_inner(Some(schema))
276    }
277
278    fn to_pretty_inner(&self, schema: Option<&Schema>) -> impl Display + use<> {
279        use comfy_table::{Cell, CellAlignment, Table};
280
281        if self.cardinality() == 0 {
282            return Either::Left("(empty)");
283        }
284
285        let mut table = Table::new();
286        table.load_preset(DataChunk::PRETTY_TABLE_PRESET);
287
288        if let Some(schema) = schema {
289            assert_eq!(self.dimension(), schema.len());
290            let cells = std::iter::once(String::new())
291                .chain(schema.fields().iter().map(|f| f.name.clone()));
292            table.set_header(cells);
293        }
294
295        for (op, row_ref) in self.rows() {
296            let mut cells = Vec::with_capacity(row_ref.len() + 1);
297            cells.push(
298                Cell::new(match op {
299                    Op::Insert => "+",
300                    Op::Delete => "-",
301                    Op::UpdateDelete => "U-",
302                    Op::UpdateInsert => "U+",
303                })
304                .set_alignment(CellAlignment::Right),
305            );
306            for datum in row_ref.iter() {
307                let str = match datum {
308                    None => "".to_owned(), // NULL
309                    Some(scalar) => scalar.to_text(),
310                };
311                cells.push(Cell::new(str));
312            }
313            table.add_row(cells);
314        }
315
316        Either::Right(table)
317    }
318
319    /// Reorder (and possibly remove) columns.
320    ///
321    /// e.g. if `indices` is `[2, 1, 0]`, and the chunk contains column `[a, b, c]`, then the output
322    /// will be `[c, b, a]`. If `indices` is [2, 0], then the output will be `[c, a]`.
323    /// If the input mapping is identity mapping, no reorder will be performed.
324    pub fn project(&self, indices: &[usize]) -> Self {
325        Self {
326            ops: self.ops.clone(),
327            data: self.data.project(indices),
328        }
329    }
330
331    /// Remove the adjacent delete-insert and insert-deletes if their row value are the same.
332    pub fn eliminate_adjacent_noop_update(self) -> Self {
333        let len = self.data_chunk().capacity();
334        let mut c: StreamChunkMut = self.into();
335        let mut prev_r = None;
336        for curr in 0..len {
337            if !c.vis(curr) {
338                continue;
339            }
340            if let Some(prev) = prev_r
341                && (
342                    // 1. Delete then Insert
343                    (matches!(c.op(prev), Op::UpdateDelete | Op::Delete)
344                    && matches!(c.op(curr), Op::UpdateInsert | Op::Insert))
345                    ||
346                    // 2. Insert then Delete
347                    //
348                    // Note that after eliminating `U+` and `U-` here, we will get a new
349                    // pair of `U-` and `U+` consisting of `prev.prev` and the `next` row.
350                    // `prev.prev` and `prev`, `curr` and `next` share the same stream key
351                    // because they are `U-` and `U+` pairs, while `prev` and `curr` share
352                    // the same stream key because they are equal, so `prev-prev` and `next`
353                    // must also share the same stream key. Therefore, it's okay to leave
354                    // their `Update` ops unchanged.
355                    //
356                    // Also, they can't be the same, otherwise these 4 rows are the same,
357                    // which should already be eliminated by the first branch.
358                    (matches!(c.op(prev), Op::UpdateInsert | Op::Insert)
359                    && matches!(c.op(curr), Op::UpdateDelete | Op::Delete))
360                )
361                && c.row_ref(prev) == c.row_ref(curr)
362            {
363                c.set_vis(prev, false);
364                c.set_vis(curr, false);
365                prev_r = None;
366            } else {
367                prev_r = Some(curr);
368            }
369        }
370
371        // Normalize update pairs that became partially invisible.
372        // If only U- is visible, turn it into Delete; if only U+ is visible, turn it into Insert.
373        for idx in 0..len.saturating_sub(1) {
374            if c.op(idx) == Op::UpdateDelete && c.op(idx + 1) == Op::UpdateInsert {
375                let delete_vis = c.vis(idx);
376                let insert_vis = c.vis(idx + 1);
377                if delete_vis && !insert_vis {
378                    c.set_op(idx, Op::Delete);
379                } else if !delete_vis && insert_vis {
380                    c.set_op(idx + 1, Op::Insert);
381                }
382            }
383        }
384        c.into()
385    }
386
387    /// Reorder columns and set visibility.
388    pub fn project_with_vis(&self, indices: &[usize], vis: Bitmap) -> Self {
389        Self {
390            ops: self.ops.clone(),
391            data: self.data.project_with_vis(indices, vis),
392        }
393    }
394
395    /// Clone the `StreamChunk` with a new visibility.
396    pub fn clone_with_vis(&self, vis: Bitmap) -> Self {
397        Self {
398            ops: self.ops.clone(),
399            data: self.data.with_visibility(vis),
400        }
401    }
402}
403
404impl Deref for StreamChunk {
405    type Target = DataChunk;
406
407    fn deref(&self) -> &Self::Target {
408        &self.data
409    }
410}
411
412impl DerefMut for StreamChunk {
413    fn deref_mut(&mut self) -> &mut Self::Target {
414        &mut self.data
415    }
416}
417
418/// `StreamChunk` can be created from `DataChunk` with all operations set to `Insert`.
419impl From<DataChunk> for StreamChunk {
420    fn from(data: DataChunk) -> Self {
421        Self::from_parts(vec![Op::Insert; data.capacity()], data)
422    }
423}
424
425impl fmt::Debug for StreamChunk {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        if f.alternate() {
428            write!(
429                f,
430                "StreamChunk {{ cardinality: {}, capacity: {}, data:\n{}\n }}",
431                self.cardinality(),
432                self.capacity(),
433                self.to_pretty()
434            )
435        } else {
436            f.debug_struct("StreamChunk")
437                .field("cardinality", &self.cardinality())
438                .field("capacity", &self.capacity())
439                .finish_non_exhaustive()
440        }
441    }
442}
443
444impl EstimateSize for StreamChunk {
445    fn estimated_heap_size(&self) -> usize {
446        self.data.estimated_heap_size() + self.ops.len() * size_of::<Op>()
447    }
448}
449
450enum OpsMutState {
451    ArcRef(Arc<[Op]>),
452    Mut(Vec<Op>),
453}
454
455impl OpsMutState {
456    const UNDEFINED: Self = Self::Mut(Vec::new());
457}
458
459pub struct OpsMut {
460    state: OpsMutState,
461}
462
463impl OpsMut {
464    pub fn new(ops: Arc<[Op]>) -> Self {
465        Self {
466            state: OpsMutState::ArcRef(ops),
467        }
468    }
469
470    pub fn len(&self) -> usize {
471        match &self.state {
472            OpsMutState::ArcRef(v) => v.len(),
473            OpsMutState::Mut(v) => v.len(),
474        }
475    }
476
477    pub fn is_empty(&self) -> bool {
478        self.len() == 0
479    }
480
481    pub fn set(&mut self, n: usize, val: Op) {
482        debug_assert!(n < self.len());
483        if let OpsMutState::Mut(v) = &mut self.state {
484            v[n] = val;
485        } else {
486            let state = mem::replace(&mut self.state, OpsMutState::UNDEFINED); // intermediate state
487            let mut v = match state {
488                OpsMutState::ArcRef(v) => v.to_vec(),
489                OpsMutState::Mut(_) => unreachable!(),
490            };
491            v[n] = val;
492            self.state = OpsMutState::Mut(v);
493        }
494    }
495
496    pub fn get(&self, n: usize) -> Op {
497        debug_assert!(n < self.len());
498        match &self.state {
499            OpsMutState::ArcRef(v) => v[n],
500            OpsMutState::Mut(v) => v[n],
501        }
502    }
503}
504impl From<OpsMut> for Arc<[Op]> {
505    fn from(v: OpsMut) -> Self {
506        match v.state {
507            OpsMutState::ArcRef(a) => a,
508            OpsMutState::Mut(v) => v.into(),
509        }
510    }
511}
512
513/// A mutable wrapper for `StreamChunk`. can only set the visibilities and ops in place, can not
514/// change the length.
515pub struct StreamChunkMut {
516    columns: Arc<[ArrayRef]>,
517    ops: OpsMut,
518    vis: BitmapBuilder,
519}
520
521impl From<StreamChunk> for StreamChunkMut {
522    fn from(c: StreamChunk) -> Self {
523        let (c, ops) = c.into_parts();
524        let (columns, vis) = c.into_parts_v2();
525        Self {
526            columns,
527            ops: OpsMut::new(ops),
528            vis: vis.into(),
529        }
530    }
531}
532
533impl From<StreamChunkMut> for StreamChunk {
534    fn from(c: StreamChunkMut) -> Self {
535        StreamChunk::from_parts(c.ops, DataChunk::from_parts(c.columns, c.vis.finish()))
536    }
537}
538
539/// A handle to one row of a [`StreamChunkMut`] that can update the row's op and
540/// visibility in place. It holds a raw pointer instead of `&mut` since multiple
541/// handles to the same chunk coexist (see [`StreamChunkMut::to_rows_mut`]).
542pub struct OpRowMutRef<'a> {
543    c: *mut StreamChunkMut,
544    i: usize,
545    _phantom: PhantomData<&'a mut StreamChunkMut>,
546}
547
548impl PartialEq for OpRowMutRef<'_> {
549    fn eq(&self, other: &Self) -> bool {
550        self.row_ref() == other.row_ref()
551    }
552}
553impl Eq for OpRowMutRef<'_> {}
554
555impl<'a> OpRowMutRef<'a> {
556    pub fn index(&self) -> usize {
557        self.i
558    }
559
560    // SAFETY of derefs below: `self.c` is valid for `'a`, and each access reborrows
561    // a single disjoint field only.
562
563    pub fn vis(&self) -> bool {
564        let vis = unsafe { &(*self.c).vis };
565        vis.is_set(self.i)
566    }
567
568    pub fn op(&self) -> Op {
569        let ops = unsafe { &(*self.c).ops };
570        ops.get(self.i)
571    }
572
573    pub fn set_vis(&mut self, val: bool) {
574        let vis = unsafe { &mut (*self.c).vis };
575        vis.set(self.i, val);
576    }
577
578    pub fn set_op(&mut self, val: Op) {
579        let ops = unsafe { &mut (*self.c).ops };
580        ops.set(self.i, val);
581    }
582
583    pub fn row_ref(&self) -> RowRef<'_> {
584        RowRef::with_columns(unsafe { &(*self.c).columns }, self.i)
585    }
586
587    /// return if the two row ref is in the same chunk
588    pub fn same_chunk(&self, other: &Self) -> bool {
589        std::ptr::eq(self.c, other.c)
590    }
591}
592
593impl StreamChunkMut {
594    pub fn capacity(&self) -> usize {
595        self.vis.len()
596    }
597
598    pub fn vis(&self, i: usize) -> bool {
599        self.vis.is_set(i)
600    }
601
602    pub fn op(&self, i: usize) -> Op {
603        self.ops.get(i)
604    }
605
606    pub fn row_ref(&self, i: usize) -> RowRef<'_> {
607        RowRef::with_columns(self.columns(), i)
608    }
609
610    pub fn set_vis(&mut self, n: usize, val: bool) {
611        self.vis.set(n, val);
612    }
613
614    pub fn set_op(&mut self, n: usize, val: Op) {
615        self.ops.set(n, val);
616    }
617
618    pub fn columns(&self) -> &[ArrayRef] {
619        &self.columns
620    }
621
622    /// get the mut reference of the stream chunk.
623    pub fn to_rows_mut(&mut self) -> impl Iterator<Item = (RowRef<'_>, OpRowMutRef<'_>)> {
624        // SAFETY: the pointer is derived from `&mut self`, which stays exclusively
625        // borrowed by the returned iterator.
626        unsafe { Self::rows_mut_ptr(self) }
627    }
628
629    /// # Safety
630    ///
631    /// `p` must be derived from an exclusive reference valid for `'a`, and the chunk must
632    /// not be accessed in other ways while the iterator or any yielded item is alive.
633    unsafe fn rows_mut_ptr<'a>(
634        p: *mut Self,
635    ) -> impl Iterator<Item = (RowRef<'a>, OpRowMutRef<'a>)> {
636        let len = {
637            let vis = unsafe { &(*p).vis };
638            vis.len()
639        };
640        (0..len)
641            .filter(move |i| {
642                let vis = unsafe { &(*p).vis };
643                vis.is_set(*i)
644            })
645            .map(move |i| {
646                (
647                    RowRef::with_columns(unsafe { &(*p).columns }, i),
648                    OpRowMutRef {
649                        c: p,
650                        i,
651                        _phantom: PhantomData,
652                    },
653                )
654            })
655    }
656}
657
658/// Test utilities for [`StreamChunk`].
659#[easy_ext::ext(StreamChunkTestExt)]
660impl StreamChunk {
661    /// Parse a chunk from string.
662    ///
663    /// See also [`DataChunkTestExt::from_pretty`].
664    ///
665    /// # Format
666    ///
667    /// The first line is a header indicating the column types.
668    /// The following lines indicate rows within the chunk.
669    /// Each line starts with an operation followed by values.
670    /// NULL values are represented as `.`.
671    ///
672    /// # Example
673    /// ```
674    /// use risingwave_common::array::StreamChunk;
675    /// use risingwave_common::array::stream_chunk::StreamChunkTestExt as _;
676    /// let chunk = StreamChunk::from_pretty(
677    ///     "  I I I I      // type chars
678    ///     U- 2 5 . .      // '.' means NULL
679    ///     U+ 2 5 2 6 D    // 'D' means deleted in visibility
680    ///     +  . . 4 8      // ^ comments are ignored
681    ///     -  . . 3 4",
682    /// );
683    /// //  ^ operations:
684    /// //     +: Insert
685    /// //     -: Delete
686    /// //    U+: UpdateInsert
687    /// //    U-: UpdateDelete
688    ///
689    /// // type chars:
690    /// //     I: i64
691    /// //     i: i32
692    /// //     F: f64
693    /// //     f: f32
694    /// //     T: str
695    /// //    TS: Timestamp
696    /// //    TZ: Timestamptz
697    /// //   SRL: Serial
698    /// //   x[]: array of x
699    /// // <i,f>: struct
700    /// ```
701    pub fn from_pretty(s: &str) -> Self {
702        let mut chunk_str = String::new();
703        let mut ops = vec![];
704
705        let (header, body) = match s.split_once('\n') {
706            Some(pair) => pair,
707            None => {
708                // empty chunk
709                return StreamChunk {
710                    ops: Arc::new([]),
711                    data: DataChunk::from_pretty(s),
712                };
713            }
714        };
715        chunk_str.push_str(header);
716        chunk_str.push('\n');
717
718        for line in body.split_inclusive('\n') {
719            if line.trim_start().is_empty() {
720                continue;
721            }
722            let (op, row) = line
723                .trim_start()
724                .split_once(|c: char| c.is_ascii_whitespace())
725                .ok_or_else(|| panic!("missing operation: {line:?}"))
726                .unwrap();
727            ops.push(match op {
728                "+" => Op::Insert,
729                "-" => Op::Delete,
730                "U+" => Op::UpdateInsert,
731                "U-" => Op::UpdateDelete,
732                t => panic!("invalid op: {t:?}"),
733            });
734            chunk_str.push_str(row);
735        }
736        StreamChunk {
737            ops: ops.into(),
738            data: DataChunk::from_pretty(&chunk_str),
739        }
740    }
741
742    /// Validate the `StreamChunk` layout.
743    pub fn valid(&self) -> bool {
744        let len = self.ops.len();
745        let data = &self.data;
746        data.visibility().len() == len && data.columns().iter().all(|col| col.len() == len)
747    }
748
749    /// Concatenate multiple `StreamChunk` into one.
750    ///
751    /// Panics if `chunks` is empty.
752    pub fn concat(chunks: Vec<StreamChunk>) -> StreamChunk {
753        let data_types = chunks[0].data_types();
754        let size = chunks.iter().map(|c| c.cardinality()).sum::<usize>();
755
756        let mut builder = StreamChunkBuilder::unlimited(data_types, Some(size));
757
758        for chunk in chunks {
759            // TODO: directly append chunks.
760            for (op, row) in chunk.rows() {
761                let none = builder.append_row(op, row);
762                debug_assert!(none.is_none());
763            }
764        }
765
766        builder.take().expect("chunk should not be empty")
767    }
768
769    /// Sort rows.
770    pub fn sort_rows(self) -> Self {
771        if self.capacity() == 0 {
772            return self;
773        }
774        let rows = self.rows().collect_vec();
775        let mut idx = (0..self.capacity()).collect_vec();
776        idx.sort_by_key(|&i| {
777            let (op, row_ref) = rows[i];
778            (op, DefaultOrdered(row_ref))
779        });
780        StreamChunk {
781            ops: idx.iter().map(|&i| self.ops[i]).collect(),
782            data: self.data.reorder_rows(&idx),
783        }
784    }
785
786    /// Generate `num_of_chunks` data chunks with type `data_types`,
787    /// where each data chunk has cardinality of `chunk_size`.
788    /// TODO(kwannoel): Generate different types of op, different vis.
789    pub fn gen_stream_chunks(
790        num_of_chunks: usize,
791        chunk_size: usize,
792        data_types: &[DataType],
793        varchar_properties: &VarcharProperty,
794    ) -> Vec<StreamChunk> {
795        Self::gen_stream_chunks_inner(
796            num_of_chunks,
797            chunk_size,
798            data_types,
799            varchar_properties,
800            1.0,
801            1.0,
802        )
803    }
804
805    pub fn gen_stream_chunks_inner(
806        num_of_chunks: usize,
807        chunk_size: usize,
808        data_types: &[DataType],
809        varchar_properties: &VarcharProperty,
810        visibility_percent: f64, // % of rows that are visible
811        inserts_percent: f64,    // Rest will be deletes.
812    ) -> Vec<StreamChunk> {
813        let ops = if inserts_percent == 0.0 {
814            vec![Op::Delete; chunk_size]
815        } else if inserts_percent == 1.0 {
816            vec![Op::Insert; chunk_size]
817        } else {
818            let mut rng = SmallRng::from_seed([0; 32]);
819            let mut ops = vec![];
820            for _ in 0..chunk_size {
821                ops.push(if rng.random_bool(inserts_percent) {
822                    Op::Insert
823                } else {
824                    Op::Delete
825                });
826            }
827            ops
828        };
829        DataChunk::gen_data_chunks(
830            num_of_chunks,
831            chunk_size,
832            data_types,
833            varchar_properties,
834            visibility_percent,
835        )
836        .into_iter()
837        .map(|chunk| StreamChunk::from_parts(ops.clone(), chunk))
838        .collect()
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845
846    #[test]
847    fn test_to_pretty_string() {
848        let chunk = StreamChunk::from_pretty(
849            "  I I
850             + 1 6
851             - 2 .
852            U- 3 7
853            U+ 4 .",
854        );
855        assert_eq!(
856            chunk.to_pretty().to_string(),
857            "\
858+----+---+---+
859|  + | 1 | 6 |
860|  - | 2 |   |
861| U- | 3 | 7 |
862| U+ | 4 |   |
863+----+---+---+"
864        );
865    }
866
867    #[test]
868    fn test_split_1() {
869        let chunk = StreamChunk::from_pretty(
870            "  I I
871             + 1 6
872             - 2 .
873            U- 3 7
874            U+ 4 .",
875        );
876        let results = chunk.split(2);
877        assert_eq!(2, results.len());
878        assert_eq!(
879            results[0].to_pretty().to_string(),
880            "\
881+---+---+---+
882| + | 1 | 6 |
883| - | 2 |   |
884+---+---+---+"
885        );
886        assert_eq!(
887            results[1].to_pretty().to_string(),
888            "\
889+----+---+---+
890| U- | 3 | 7 |
891| U+ | 4 |   |
892+----+---+---+"
893        );
894    }
895
896    #[test]
897    fn test_split_2() {
898        let chunk = StreamChunk::from_pretty(
899            "  I I
900             + 1 6
901            U- 3 7
902            U+ 4 .
903             - 2 .",
904        );
905        let results = chunk.split(2);
906        assert_eq!(2, results.len());
907        assert_eq!(
908            results[0].to_pretty().to_string(),
909            "\
910+----+---+---+
911|  + | 1 | 6 |
912| U- | 3 | 7 |
913| U+ | 4 |   |
914+----+---+---+"
915        );
916        assert_eq!(
917            results[1].to_pretty().to_string(),
918            "\
919+---+---+---+
920| - | 2 |   |
921+---+---+---+"
922        );
923    }
924
925    #[test]
926    fn test_eliminate_adjacent_noop_update() {
927        let c = StreamChunk::from_pretty(
928            "  I I
929            - 1 6 D
930            - 2 2
931            + 2 3
932            - 2 3
933            + 1 6
934            - 1 7
935            + 1 10 D
936            + 1 7
937            U- 3 7
938            U+ 3 7
939            + 2 3",
940        );
941        let c = c.eliminate_adjacent_noop_update();
942        assert_eq!(
943            c.to_pretty().to_string(),
944            "\
945+---+---+---+
946| - | 2 | 2 |
947| + | 1 | 6 |
948| + | 2 | 3 |
949+---+---+---+"
950        );
951    }
952
953    #[test]
954    fn test_eliminate_adjacent_noop_update_normalize_update_pair() {
955        let c = StreamChunk::from_pretty(
956            "  I I
957            + 1 10
958            U- 1 10
959            U+ 1 20",
960        );
961        let c = c.eliminate_adjacent_noop_update();
962        assert_eq!(
963            c.to_pretty().to_string(),
964            "\
965+---+---+----+
966| + | 1 | 20 |
967+---+---+----+"
968        );
969    }
970}