Skip to main content

risingwave_stream/common/
compact_chunk.rs

1// Copyright 2024 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 itertools::Itertools;
16use risingwave_common::array::stream_chunk::StreamChunkMut;
17use risingwave_common::array::stream_record::Record;
18use risingwave_common::array::{Op, StreamChunk};
19use risingwave_common::row::RowExt;
20use risingwave_common::types::DataType;
21use risingwave_pb::stream_plan::stream_node::StreamKind;
22
23pub use super::change_buffer::InconsistencyBehavior;
24use crate::common::change_buffer::output_kind::{RETRACT, UPSERT};
25use crate::common::change_buffer::{ChangeBuffer, OutputKind};
26
27/// A helper to remove unnecessary changes in the stream chunks based on the key.
28pub struct StreamChunkCompactor {
29    chunks: Vec<StreamChunk>,
30    key: Vec<usize>,
31    input_kind: StreamKind,
32}
33
34impl StreamChunkCompactor {
35    /// For upsert input, compaction retains the final operation per `key`. Lossless compaction
36    /// of the input stream requires its stream key; using another key requires caller-defined
37    /// reconciliation. Delete rows only guarantee the stream key columns, and inserts may
38    /// overwrite rows from earlier batches. Upsert input always produces upsert operations,
39    /// since the old rows needed for retract output are unavailable.
40    pub fn new(key: Vec<usize>, chunks: Vec<StreamChunk>, input_kind: StreamKind) -> Self {
41        Self {
42            chunks,
43            key,
44            input_kind,
45        }
46    }
47
48    /// Remove unnecessary changes in the given chunks, by modifying the visibility and ops in place.
49    pub fn into_compacted_chunks_inline<const KIND: OutputKind>(
50        self,
51        ib: InconsistencyBehavior,
52    ) -> Vec<StreamChunk> {
53        let Self {
54            chunks,
55            key: key_indices,
56            input_kind,
57        } = self;
58
59        let estimate_size = chunks.iter().map(|c| c.cardinality()).sum();
60        let mut cb = ChangeBuffer::with_capacity(estimate_size).with_inconsistency_behavior(ib);
61
62        let mut chunks = chunks.into_iter().map(StreamChunkMut::from).collect_vec();
63        for chunk in &mut chunks {
64            for (row, mut op_row) in chunk.to_rows_mut() {
65                let op = op_row.op().normalize_update();
66                let key = row.project(&key_indices);
67                // Make all rows invisible first.
68                op_row.set_vis(false);
69                op_row.set_op(op);
70                if input_kind == StreamKind::Upsert {
71                    cb.apply_upsert_op_row(op, key, op_row);
72                } else {
73                    cb.apply_op_row(op, key, op_row);
74                }
75            }
76        }
77
78        // For the rows that survive compaction, make them visible.
79        for record in cb.into_records() {
80            match record {
81                Record::Insert { mut new_row } => new_row.set_vis(true),
82                Record::Delete { mut old_row } => old_row.set_vis(true),
83                Record::Update {
84                    mut old_row,
85                    mut new_row,
86                } => {
87                    match KIND {
88                        // For upsert output, we only keep the new row with normalized op (`Insert`).
89                        UPSERT => new_row.set_vis(true),
90                        // For retract output, we keep both the old and new row, and rewrite the ops
91                        // to `U-` and `U+` if they are adjacent.
92                        RETRACT => {
93                            old_row.set_vis(true);
94                            new_row.set_vis(true);
95                            if old_row.same_chunk(&new_row)
96                                && old_row.index() + 1 == new_row.index()
97                            {
98                                old_row.set_op(Op::UpdateDelete);
99                                new_row.set_op(Op::UpdateInsert);
100                            }
101                        }
102                    }
103                }
104            }
105        }
106
107        chunks.into_iter().map(|c| c.into()).collect()
108    }
109
110    /// Remove unnecessary changes in the given chunks, by filtering them out and constructing new
111    /// chunks, with the given chunk size.
112    pub fn into_compacted_chunks_reconstructed<const KIND: OutputKind>(
113        self,
114        chunk_size: usize,
115        data_types: Vec<DataType>,
116        ib: InconsistencyBehavior,
117    ) -> Vec<StreamChunk> {
118        let Self {
119            chunks,
120            key: key_indices,
121            input_kind,
122        } = self;
123
124        let estimate_size = chunks.iter().map(|c| c.cardinality()).sum();
125        let mut cb = ChangeBuffer::with_capacity(estimate_size).with_inconsistency_behavior(ib);
126
127        for chunk in &chunks {
128            if input_kind == StreamKind::Upsert {
129                for (op, row) in chunk.rows() {
130                    cb.apply_upsert_op_row(op, row.project(&key_indices), row);
131                }
132            } else {
133                for record in chunk.records() {
134                    cb.apply_record(record, |&row| row.project(&key_indices));
135                }
136            }
137        }
138
139        cb.into_chunks::<KIND>(data_types, chunk_size)
140    }
141}
142
143/// Remove unnecessary changes in the given chunk, by modifying the visibility and ops in place.
144///
145/// This is the same as [`StreamChunkCompactor::into_compacted_chunks_inline`] with only one chunk.
146pub fn compact_chunk_inline<const KIND: OutputKind>(
147    stream_chunk: StreamChunk,
148    key_indices: &[usize],
149    input_kind: StreamKind,
150    ib: InconsistencyBehavior,
151) -> StreamChunk {
152    Itertools::exactly_one(
153        StreamChunkCompactor::new(key_indices.to_vec(), vec![stream_chunk], input_kind)
154            .into_compacted_chunks_inline::<KIND>(ib)
155            .into_iter(),
156    )
157    .unwrap_or_else(|_| unreachable!("should have exactly one chunk in the output"))
158}
159
160#[cfg(test)]
161mod tests {
162    use risingwave_common::test_prelude::StreamChunkTestExt;
163
164    use super::*;
165
166    #[test]
167    fn test_upsert_input_retains_final_operation() {
168        let input = StreamChunk::from_pretty(
169            "I I
170            + 1 10
171            - 1 .
172            + 2 10
173            + 2 20
174            - 3 .
175            + 3 20
176            - 4 .
177            - 4 .",
178        );
179        let expected = StreamChunk::from_pretty("I I\n- 1 .\n+ 2 20\n+ 3 20\n- 4 .");
180        for reconstructed in [false, true] {
181            let chunks = compact_upsert(vec![input.clone()], reconstructed);
182            assert_eq!(StreamChunk::concat(chunks).compact_vis(), expected);
183        }
184    }
185
186    fn compact_upsert(chunks: Vec<StreamChunk>, reconstructed: bool) -> Vec<StreamChunk> {
187        let compactor = StreamChunkCompactor::new(vec![0], chunks, StreamKind::Upsert);
188        if reconstructed {
189            compactor.into_compacted_chunks_reconstructed::<UPSERT>(
190                2,
191                vec![DataType::Int64; 2],
192                InconsistencyBehavior::Panic,
193            )
194        } else {
195            compactor.into_compacted_chunks_inline::<UPSERT>(InconsistencyBehavior::Panic)
196        }
197    }
198
199    /// Compaction must preserve the effect on downstream state, including when chunks are
200    /// independently compacted by the writer and then merged and compacted by the log reader.
201    #[test]
202    fn test_upsert_compaction_composes() {
203        use std::collections::HashMap;
204
205        use risingwave_common::row::{OwnedRow, Row};
206
207        fn apply(
208            mut state: HashMap<OwnedRow, OwnedRow>,
209            chunks: &[StreamChunk],
210        ) -> HashMap<OwnedRow, OwnedRow> {
211            for (op, row) in chunks.iter().flat_map(|c| c.rows()) {
212                let key = row.project(&[0]).into_owned_row();
213                match op {
214                    Op::Insert => {
215                        state.insert(key, row.to_owned_row());
216                    }
217                    Op::Delete => {
218                        state.remove(&key);
219                    }
220                    _ => panic!("upsert compaction must normalize operations"),
221                }
222            }
223            state
224        }
225
226        let events = ["+ 1 10", "+ 1 20", "- 1 .", "+ 2 10", "+ 2 20", "- 2 ."]
227            .map(|event| StreamChunk::from_pretty(&format!("I I\n{event}")));
228        let initial_states = ["", "+ 1 99", "+ 2 99", "+ 1 99\n+ 2 99"].map(|rows| {
229            apply(
230                HashMap::new(),
231                &[StreamChunk::from_pretty(&format!("I I\n{rows}"))],
232            )
233        });
234
235        // Exhaust all sequences of up to four operations across two independent keys, with
236        // two different inserted values and key-only tombstones.
237        for len in 1..=4 {
238            for mut sequence in 0..events.len().pow(len) {
239                let input = (0..len)
240                    .map(|_| {
241                        let event = events[sequence % events.len()].clone();
242                        sequence /= events.len();
243                        event
244                    })
245                    .collect_vec();
246                for reconstructed in [false, true] {
247                    let once = compact_upsert(input.clone(), reconstructed);
248                    for split in 0..=input.len() {
249                        let mut partitions = compact_upsert(input[..split].to_vec(), reconstructed);
250                        partitions.extend(compact_upsert(input[split..].to_vec(), reconstructed));
251                        let twice =
252                            compact_upsert(vec![StreamChunk::concat(partitions)], reconstructed);
253                        for initial in &initial_states {
254                            let expected = apply(initial.clone(), &input);
255                            assert_eq!(apply(initial.clone(), &once), expected);
256                            assert_eq!(apply(initial.clone(), &twice), expected);
257                        }
258                    }
259                }
260            }
261        }
262    }
263
264    #[test]
265    fn test_compact_chunk_inline_upsert() {
266        test_compact_chunk_inline::<UPSERT>();
267    }
268
269    #[test]
270    fn test_compact_chunk_inline_retract() {
271        test_compact_chunk_inline::<RETRACT>();
272    }
273
274    fn test_compact_chunk_inline<const KIND: OutputKind>() {
275        let key = [0, 1];
276        let chunks = vec![
277            StreamChunk::from_pretty(
278                " I I I
279                - 1 1 1
280                + 1 1 2
281                + 2 5 7
282                + 4 9 2
283                - 2 5 7
284                + 2 5 5
285                - 6 6 9
286                + 6 6 9
287                - 9 9 1",
288            ),
289            StreamChunk::from_pretty(
290                " I I I
291                - 6 6 9
292                + 9 9 9
293                - 9 9 4
294                + 2 2 2
295                + 9 9 1",
296            ),
297        ];
298        let compactor = StreamChunkCompactor::new(key.to_vec(), chunks, StreamKind::Retract);
299        let mut iter = compactor
300            .into_compacted_chunks_inline::<KIND>(InconsistencyBehavior::Panic)
301            .into_iter();
302
303        let chunk = iter.next().unwrap().compact_vis();
304        let expected = match KIND {
305            RETRACT => StreamChunk::from_pretty(
306                " I I I
307                U- 1 1 1
308                U+ 1 1 2
309                + 4 9 2
310                + 2 5 5
311                - 6 6 9",
312            ),
313            UPSERT => StreamChunk::from_pretty(
314                " I I I
315                + 1 1 2
316                + 4 9 2
317                + 2 5 5
318                - 6 6 9",
319            ),
320        };
321        assert_eq!(chunk, expected, "{}", chunk.to_pretty());
322
323        let chunk = iter.next().unwrap().compact_vis();
324        assert_eq!(
325            chunk,
326            StreamChunk::from_pretty(
327                " I I I
328                + 2 2 2",
329            ),
330            "{}",
331            chunk.to_pretty()
332        );
333
334        assert_eq!(iter.next(), None);
335    }
336
337    #[test]
338    fn test_compact_chunk_reconstructed_upsert() {
339        test_compact_chunk_reconstructed::<UPSERT>();
340    }
341
342    #[test]
343    fn test_compact_chunk_reconstructed_retract() {
344        test_compact_chunk_reconstructed::<RETRACT>();
345    }
346
347    fn test_compact_chunk_reconstructed<const KIND: OutputKind>() {
348        let key = [0, 1];
349        let chunks = vec![
350            StreamChunk::from_pretty(
351                " I I I
352            - 1 1 1
353            + 1 1 2
354            + 2 5 7
355            + 4 9 2
356            - 2 5 7
357            + 2 5 5
358            - 6 6 9
359            + 6 6 9
360            - 9 9 1",
361            ),
362            StreamChunk::from_pretty(
363                " I I I
364            - 6 6 9
365            + 9 9 9
366            - 9 9 4
367            + 2 2 2
368            + 9 9 1",
369            ),
370        ];
371        let compactor = StreamChunkCompactor::new(key.to_vec(), chunks, StreamKind::Retract);
372
373        let chunks = compactor.into_compacted_chunks_reconstructed::<KIND>(
374            100,
375            vec![DataType::Int64, DataType::Int64, DataType::Int64],
376            InconsistencyBehavior::Panic,
377        );
378        let chunk = chunks.into_iter().next().unwrap();
379        let expected = match KIND {
380            RETRACT => StreamChunk::from_pretty(
381                "  I I I
382                 U- 1 1 1
383                 U+ 1 1 2
384                 + 4 9 2
385                 + 2 5 5
386                 - 6 6 9
387                 + 2 2 2",
388            ),
389            UPSERT => StreamChunk::from_pretty(
390                "  I I I
391                 + 1 1 2
392                 + 4 9 2
393                 + 2 5 5
394                 - 6 6 9
395                 + 2 2 2",
396            ),
397        };
398        assert_eq!(chunk, expected, "{}", chunk.to_pretty());
399    }
400}