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;
21
22pub use super::change_buffer::InconsistencyBehavior;
23use crate::common::change_buffer::output_kind::{RETRACT, UPSERT};
24use crate::common::change_buffer::{ChangeBuffer, OutputKind};
25
26pub struct StreamChunkCompactor {
28 chunks: Vec<StreamChunk>,
29 key: Vec<usize>,
30}
31
32impl StreamChunkCompactor {
33 pub fn new(key: Vec<usize>, chunks: Vec<StreamChunk>) -> Self {
34 Self { chunks, key }
35 }
36
37 pub fn into_inner(self) -> (Vec<StreamChunk>, Vec<usize>) {
38 (self.chunks, self.key)
39 }
40
41 pub fn into_compacted_chunks_inline<const KIND: OutputKind>(
43 self,
44 ib: InconsistencyBehavior,
45 ) -> Vec<StreamChunk> {
46 let (chunks, key_indices) = self.into_inner();
47
48 let estimate_size = chunks.iter().map(|c| c.cardinality()).sum();
49 let mut cb = ChangeBuffer::with_capacity(estimate_size).with_inconsistency_behavior(ib);
50
51 let mut chunks = chunks.into_iter().map(StreamChunkMut::from).collect_vec();
52 for chunk in &mut chunks {
53 for (row, mut op_row) in chunk.to_rows_mut() {
54 let op = op_row.op().normalize_update();
55 let key = row.project(&key_indices);
56 op_row.set_vis(false);
58 op_row.set_op(op);
59 cb.apply_op_row(op, key, op_row);
60 }
61 }
62
63 for record in cb.into_records() {
65 match record {
66 Record::Insert { mut new_row } => new_row.set_vis(true),
67 Record::Delete { mut old_row } => old_row.set_vis(true),
68 Record::Update {
69 mut old_row,
70 mut new_row,
71 } => {
72 match KIND {
73 UPSERT => new_row.set_vis(true),
75 RETRACT => {
78 old_row.set_vis(true);
79 new_row.set_vis(true);
80 if old_row.same_chunk(&new_row)
81 && old_row.index() + 1 == new_row.index()
82 {
83 old_row.set_op(Op::UpdateDelete);
84 new_row.set_op(Op::UpdateInsert);
85 }
86 }
87 }
88 }
89 }
90 }
91
92 chunks.into_iter().map(|c| c.into()).collect()
93 }
94
95 pub fn into_compacted_chunks_reconstructed<const KIND: OutputKind>(
98 self,
99 chunk_size: usize,
100 data_types: Vec<DataType>,
101 ib: InconsistencyBehavior,
102 ) -> Vec<StreamChunk> {
103 let (chunks, key_indices) = self.into_inner();
104
105 let estimate_size = chunks.iter().map(|c| c.cardinality()).sum();
106 let mut cb = ChangeBuffer::with_capacity(estimate_size).with_inconsistency_behavior(ib);
107
108 for chunk in &chunks {
109 for record in chunk.records() {
110 cb.apply_record(record, |&row| row.project(&key_indices));
111 }
112 }
113
114 cb.into_chunks::<KIND>(data_types, chunk_size)
115 }
116}
117
118pub fn compact_chunk_inline<const KIND: OutputKind>(
122 stream_chunk: StreamChunk,
123 key_indices: &[usize],
124 ib: InconsistencyBehavior,
125) -> StreamChunk {
126 Itertools::exactly_one(
127 StreamChunkCompactor::new(key_indices.to_vec(), vec![stream_chunk])
128 .into_compacted_chunks_inline::<KIND>(ib)
129 .into_iter(),
130 )
131 .unwrap_or_else(|_| unreachable!("should have exactly one chunk in the output"))
132}
133
134#[cfg(test)]
135mod tests {
136 use risingwave_common::test_prelude::StreamChunkTestExt;
137
138 use super::*;
139
140 #[test]
141 fn test_compact_chunk_inline_upsert() {
142 test_compact_chunk_inline::<UPSERT>();
143 }
144
145 #[test]
146 fn test_compact_chunk_inline_retract() {
147 test_compact_chunk_inline::<RETRACT>();
148 }
149
150 fn test_compact_chunk_inline<const KIND: OutputKind>() {
151 let key = [0, 1];
152 let chunks = vec![
153 StreamChunk::from_pretty(
154 " I I I
155 - 1 1 1
156 + 1 1 2
157 + 2 5 7
158 + 4 9 2
159 - 2 5 7
160 + 2 5 5
161 - 6 6 9
162 + 6 6 9
163 - 9 9 1",
164 ),
165 StreamChunk::from_pretty(
166 " I I I
167 - 6 6 9
168 + 9 9 9
169 - 9 9 4
170 + 2 2 2
171 + 9 9 1",
172 ),
173 ];
174 let compactor = StreamChunkCompactor::new(key.to_vec(), chunks);
175 let mut iter = compactor
176 .into_compacted_chunks_inline::<KIND>(InconsistencyBehavior::Panic)
177 .into_iter();
178
179 let chunk = iter.next().unwrap().compact_vis();
180 let expected = match KIND {
181 RETRACT => StreamChunk::from_pretty(
182 " I I I
183 U- 1 1 1
184 U+ 1 1 2
185 + 4 9 2
186 + 2 5 5
187 - 6 6 9",
188 ),
189 UPSERT => StreamChunk::from_pretty(
190 " I I I
191 + 1 1 2
192 + 4 9 2
193 + 2 5 5
194 - 6 6 9",
195 ),
196 };
197 assert_eq!(chunk, expected, "{}", chunk.to_pretty());
198
199 let chunk = iter.next().unwrap().compact_vis();
200 assert_eq!(
201 chunk,
202 StreamChunk::from_pretty(
203 " I I I
204 + 2 2 2",
205 ),
206 "{}",
207 chunk.to_pretty()
208 );
209
210 assert_eq!(iter.next(), None);
211 }
212
213 #[test]
214 fn test_compact_chunk_reconstructed_upsert() {
215 test_compact_chunk_reconstructed::<UPSERT>();
216 }
217
218 #[test]
219 fn test_compact_chunk_reconstructed_retract() {
220 test_compact_chunk_reconstructed::<RETRACT>();
221 }
222
223 fn test_compact_chunk_reconstructed<const KIND: OutputKind>() {
224 let key = [0, 1];
225 let chunks = vec![
226 StreamChunk::from_pretty(
227 " I I I
228 - 1 1 1
229 + 1 1 2
230 + 2 5 7
231 + 4 9 2
232 - 2 5 7
233 + 2 5 5
234 - 6 6 9
235 + 6 6 9
236 - 9 9 1",
237 ),
238 StreamChunk::from_pretty(
239 " I I I
240 - 6 6 9
241 + 9 9 9
242 - 9 9 4
243 + 2 2 2
244 + 9 9 1",
245 ),
246 ];
247 let compactor = StreamChunkCompactor::new(key.to_vec(), chunks);
248
249 let chunks = compactor.into_compacted_chunks_reconstructed::<KIND>(
250 100,
251 vec![DataType::Int64, DataType::Int64, DataType::Int64],
252 InconsistencyBehavior::Panic,
253 );
254 let chunk = chunks.into_iter().next().unwrap();
255 let expected = match KIND {
256 RETRACT => StreamChunk::from_pretty(
257 " I I I
258 U- 1 1 1
259 U+ 1 1 2
260 + 4 9 2
261 + 2 5 5
262 - 6 6 9
263 + 2 2 2",
264 ),
265 UPSERT => StreamChunk::from_pretty(
266 " I I I
267 + 1 1 2
268 + 4 9 2
269 + 2 5 5
270 - 6 6 9
271 + 2 2 2",
272 ),
273 };
274 assert_eq!(chunk, expected, "{}", chunk.to_pretty());
275 }
276}