risingwave_stream/common/
compact_chunk.rs1use 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
27pub struct StreamChunkCompactor {
29 chunks: Vec<StreamChunk>,
30 key: Vec<usize>,
31 input_kind: StreamKind,
32}
33
34impl StreamChunkCompactor {
35 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 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 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 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 UPSERT => new_row.set_vis(true),
90 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 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
143pub 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 #[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 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}