delta_kernel/parallel/
sequential_phase.rs1#![allow(unused)]
11
12use std::sync::Arc;
13
14use delta_kernel_derive::internal_api;
15use itertools::Itertools;
16
17use crate::log_reader::checkpoint_manifest::CheckpointManifestReader;
18use crate::log_reader::commit::CommitReader;
19use crate::log_replay::LogReplayProcessor;
20use crate::log_segment::LogSegment;
21use crate::scan::COMMIT_READ_SCHEMA;
22use crate::utils::require;
23use crate::{DeltaResult, Engine, Error, FileMeta};
24
25#[internal_api]
68pub(crate) struct SequentialPhase<P: LogReplayProcessor> {
69 processor: P,
71 commit_phase: Option<CommitReader>,
73 checkpoint_manifest_phase: Option<CheckpointManifestReader>,
76 is_finished: bool,
78 checkpoint_parts: Vec<FileMeta>,
80}
81
82#[internal_api]
85pub(crate) enum AfterSequential<P: LogReplayProcessor> {
86 Done(P),
88 Parallel { processor: P, files: Vec<FileMeta> },
90}
91
92impl<P: LogReplayProcessor> SequentialPhase<P> {
93 #[internal_api]
100 pub(crate) fn try_new(
101 processor: P,
102 log_segment: &LogSegment,
103 engine: Arc<dyn Engine>,
104 ) -> DeltaResult<Self> {
105 let commit_phase = Some(CommitReader::try_new(
106 engine.as_ref(),
107 log_segment,
108 COMMIT_READ_SCHEMA.clone(),
109 None,
110 )?);
111
112 let checkpoint_manifest_phase = match log_segment.listed.checkpoint_parts.as_slice() {
115 [single_part] => Some(CheckpointManifestReader::try_new(
116 engine,
117 single_part,
118 log_segment.log_root.clone(),
119 )?),
120 _ => None,
121 };
122
123 let checkpoint_parts = log_segment
124 .listed
125 .checkpoint_parts
126 .iter()
127 .map(|path| path.location.clone())
128 .collect_vec();
129
130 Ok(Self {
131 processor,
132 commit_phase,
133 checkpoint_manifest_phase,
134 is_finished: false,
135 checkpoint_parts,
136 })
137 }
138
139 #[internal_api]
150 pub(crate) fn finish(self) -> DeltaResult<AfterSequential<P>> {
151 if !self.is_finished {
152 return Err(Error::generic(
153 "Must exhaust iterator before calling finish()",
154 ));
155 }
156
157 let parallel_files = match self.checkpoint_manifest_phase {
158 Some(manifest_reader) => manifest_reader.extract_sidecars()?,
159 None => {
160 let parts = self.checkpoint_parts;
161 require!(
162 parts.len() != 1,
163 Error::generic(
164 "Invariant violation: If there is exactly one checkpoint part,
165 there must be a manifest reader"
166 )
167 );
168 parts
170 }
171 };
172
173 if parallel_files.is_empty() {
174 Ok(AfterSequential::Done(self.processor))
175 } else {
176 Ok(AfterSequential::Parallel {
177 processor: self.processor,
178 files: parallel_files,
179 })
180 }
181 }
182}
183
184impl<P: LogReplayProcessor> Iterator for SequentialPhase<P> {
185 type Item = DeltaResult<P::Output>;
186
187 fn next(&mut self) -> Option<Self::Item> {
188 let next = self
189 .commit_phase
190 .as_mut()
191 .and_then(|commit_phase| commit_phase.next())
192 .or_else(|| {
193 self.commit_phase = None;
194 self.checkpoint_manifest_phase.as_mut()?.next()
195 });
196
197 let Some(result) = next else {
198 self.is_finished = true;
199 return None;
200 };
201
202 Some(result.and_then(|batch| self.processor.process_actions_batch(batch)))
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::scan::AfterSequentialScanMetadata;
210 use crate::unit_test_utils::{assert_result_error_with_message, load_test_table};
211
212 fn verify_sequential_processing(
214 table_name: &str,
215 expected_adds: &[&str],
216 expected_sidecars: &[&str],
217 ) -> DeltaResult<()> {
218 let (engine, snapshot, _tempdir) = load_test_table(table_name)?;
219
220 let scan = snapshot.scan_builder().build()?;
221 let mut sequential = scan.parallel_scan_metadata(engine)?;
222
223 let mut file_paths = Vec::new();
225 for result in sequential.by_ref() {
226 let metadata = result?;
227 file_paths =
228 metadata.visit_scan_files(file_paths, |ps: &mut Vec<String>, file_stat| {
229 ps.push(file_stat.path);
230 })?;
231 }
232
233 file_paths.sort();
235 assert_eq!(
236 file_paths, expected_adds,
237 "Sequential phase should collect expected Add file paths"
238 );
239
240 let result = sequential.finish()?;
242 match (expected_sidecars, result) {
243 (sidecars, AfterSequentialScanMetadata::Done) => {
244 assert!(
245 sidecars.is_empty(),
246 "Expected Done but got sidecars {sidecars:?}"
247 );
248 }
249 (expected_sidecars, AfterSequentialScanMetadata::Parallel { files, .. }) => {
250 assert_eq!(
251 files.len(),
252 expected_sidecars.len(),
253 "Should collect exactly {} sidecar files",
254 expected_sidecars.len()
255 );
256
257 let mut collected_paths = files
259 .iter()
260 .map(|fm| {
261 fm.location
262 .path_segments()
263 .and_then(|mut segments| segments.next_back())
264 .unwrap_or("")
265 .to_string()
266 })
267 .collect_vec();
268
269 collected_paths.sort();
270 assert_eq!(collected_paths, expected_sidecars);
271 }
272 }
273
274 Ok(())
275 }
276
277 #[test]
278 fn test_sequential_v2_with_commits_only() -> DeltaResult<()> {
279 verify_sequential_processing(
280 "table-without-dv-small",
281 &["part-00000-517f5d32-9c95-48e8-82b4-0229cc194867-c000.snappy.parquet"],
282 &[], )
284 }
285
286 #[test]
287 fn test_sequential_v2_with_sidecars() -> DeltaResult<()> {
288 verify_sequential_processing(
289 "v2-checkpoints-json-with-sidecars",
290 &[], &[
292 "00000000000000000006.checkpoint.0000000001.0000000002.19af1366-a425-47f4-8fa6-8d6865625573.parquet",
293 "00000000000000000006.checkpoint.0000000002.0000000002.5008b69f-aa8a-4a66-9299-0733a56a7e63.parquet",
294 ],
295 )
296 }
297
298 #[test]
299 fn test_sequential_finish_before_exhaustion_error() -> DeltaResult<()> {
300 let (engine, snapshot, _tempdir) = load_test_table("table-without-dv-small")?;
301
302 let scan = snapshot.scan_builder().build()?;
303 let sequential = scan.parallel_scan_metadata(engine)?;
304
305 let result = sequential.finish();
307 assert_result_error_with_message(result, "Must exhaust iterator before calling finish()");
308
309 Ok(())
310 }
311
312 #[test]
313 fn test_sequential_checkpoint_without_sidecars() -> DeltaResult<()> {
314 verify_sequential_processing(
315 "v2-checkpoints-json-without-sidecars",
316 &[
317 "test%25file%25prefix-part-00000-0e32f92c-e232-4daa-b734-369d1a800502-c000.snappy.parquet",
319 "test%25file%25prefix-part-00000-91daf7c5-9ba0-4f76-aefd-0c3b21d33c6c-c000.snappy.parquet",
320 "test%25file%25prefix-part-00001-a5c41be1-ded0-4b18-a638-a927d233876e-c000.snappy.parquet",
321 ],
322 &[], )
324 }
325
326 #[test]
327 fn test_sequential_parquet_checkpoint_with_sidecars() -> DeltaResult<()> {
328 verify_sequential_processing(
329 "v2-checkpoints-parquet-with-sidecars",
330 &[], &[
332 "00000000000000000006.checkpoint.0000000001.0000000002.76931b15-ead3-480d-b86c-afe55a577fc3.parquet",
334 "00000000000000000006.checkpoint.0000000002.0000000002.4367b29c-0e87-447f-8e81-9814cc01ad1f.parquet",
335 ],
336 )
337 }
338
339 #[test]
340 fn test_sequential_checkpoint_no_commits() -> DeltaResult<()> {
341 verify_sequential_processing(
342 "with_checkpoint_no_last_checkpoint",
343 &["part-00000-70b1dcdf-0236-4f63-a072-124cdbafd8a0-c000.snappy.parquet"], &[], )
346 }
347}