Skip to content

Commit 9477694

Browse files
committed
deduplicator
1 parent aa872d1 commit 9477694

4 files changed

Lines changed: 108 additions & 64 deletions

File tree

kernel/src/action_reconciliation/log_replay.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
//! actions selected
3232
//!
3333
use crate::engine_data::{FilteredEngineData, GetData, RowVisitor, TypedGetData as _};
34+
use crate::log_replay::deduplicator::Deduplicator;
3435
use crate::log_replay::{
3536
ActionsBatch, FileActionDeduplicator, FileActionKey, HasSelectionVector, LogReplayProcessor,
3637
};

kernel/src/log_replay.rs

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
//! This module provides structures for efficient batch processing, focusing on file action
1414
//! deduplication with `FileActionDeduplicator` which tracks unique files across log batches
1515
//! to minimize memory usage for tables with extensive history.
16-
use crate::actions::deletion_vector::DeletionVectorDescriptor;
17-
use crate::engine_data::{GetData, TypedGetData};
16+
use crate::engine_data::GetData;
17+
use crate::log_replay::deduplicator::Deduplicator;
1818
use crate::scan::data_skipping::DataSkippingFilter;
1919
use crate::{DeltaResult, EngineData};
2020

@@ -24,6 +24,8 @@ use std::collections::HashSet;
2424

2525
use tracing::debug;
2626

27+
pub(crate) mod deduplicator;
28+
2729
/// The subset of file action fields that uniquely identifies it in the log, used for deduplication
2830
/// of adds and removes during log replay.
2931
#[derive(Debug, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize, Clone)]
@@ -56,7 +58,8 @@ pub(crate) struct FileActionDeduplicator<'seen> {
5658
seen_file_keys: &'seen mut HashSet<FileActionKey>,
5759
// TODO: Consider renaming to `is_commit_batch`, `deduplicate_batch`, or `save_batch`
5860
// to better reflect its role in deduplication logic.
59-
/// Whether we're processing a log batch (as opposed to a checkpoint)
61+
/// Whether we're processing a commit log JSON file (`true`) or a checkpoint file (`false`).
62+
/// When `true`, file actions are added to `seen_file_keys` as they're processed.
6063
is_log_batch: bool,
6164
/// Index of the getter containing the add.path column
6265
add_path_index: usize,
@@ -86,12 +89,15 @@ impl<'seen> FileActionDeduplicator<'seen> {
8689
remove_dv_start_index,
8790
}
8891
}
92+
}
8993

94+
impl<'seen> Deduplicator for FileActionDeduplicator<'seen> {
95+
type Key = FileActionKey;
9096
/// Checks if log replay already processed this logical file (in which case the current action
9197
/// should be ignored). If not already seen, register it so we can recognize future duplicates.
9298
/// Returns `true` if we have seen the file and should ignore it, `false` if we have not seen it
9399
/// and should process it.
94-
pub(crate) fn check_and_record_seen(&mut self, key: FileActionKey) -> bool {
100+
fn check_and_record_seen(&mut self, key: FileActionKey) -> bool {
95101
// Note: each (add.path + add.dv_unique_id()) pair has a
96102
// unique Add + Remove pair in the log. For example:
97103
// https://github.com/delta-io/delta/blob/master/spark/src/test/resources/delta/table-with-dv-large/_delta_log/00000000000000000001.json
@@ -117,35 +123,6 @@ impl<'seen> FileActionDeduplicator<'seen> {
117123
}
118124
}
119125

120-
/// Extracts the deletion vector unique ID if it exists.
121-
///
122-
/// This function retrieves the necessary fields for constructing a deletion vector unique ID
123-
/// by accessing `getters` at `dv_start_index` and the following two indices. Specifically:
124-
/// - `dv_start_index` retrieves the storage type (`deletionVector.storageType`).
125-
/// - `dv_start_index + 1` retrieves the path or inline deletion vector (`deletionVector.pathOrInlineDv`).
126-
/// - `dv_start_index + 2` retrieves the optional offset (`deletionVector.offset`).
127-
fn extract_dv_unique_id<'a>(
128-
&self,
129-
i: usize,
130-
getters: &[&'a dyn GetData<'a>],
131-
dv_start_index: usize,
132-
) -> DeltaResult<Option<String>> {
133-
match getters[dv_start_index].get_opt(i, "deletionVector.storageType")? {
134-
Some(storage_type) => {
135-
let path_or_inline =
136-
getters[dv_start_index + 1].get(i, "deletionVector.pathOrInlineDv")?;
137-
let offset = getters[dv_start_index + 2].get_opt(i, "deletionVector.offset")?;
138-
139-
Ok(Some(DeletionVectorDescriptor::unique_id_from_parts(
140-
storage_type,
141-
path_or_inline,
142-
offset,
143-
)))
144-
}
145-
None => Ok(None),
146-
}
147-
}
148-
149126
/// Extracts a file action key and determines if it's an add operation.
150127
/// This method examines the data at the given index using the provided getters
151128
/// to identify whether a file action exists and what type it is.
@@ -159,7 +136,7 @@ impl<'seen> FileActionDeduplicator<'seen> {
159136
/// - `Ok(Some((key, is_add)))`: When a file action is found, returns the key and whether it's an add operation
160137
/// - `Ok(None)`: When no file action is found
161138
/// - `Err(...)`: On any error during extraction
162-
pub(crate) fn extract_file_action<'a>(
139+
fn extract_file_action<'a>(
163140
&self,
164141
i: usize,
165142
getters: &[&'a dyn GetData<'a>],
@@ -190,7 +167,7 @@ impl<'seen> FileActionDeduplicator<'seen> {
190167
///
191168
/// `true` indicates we are processing a batch from a commit file.
192169
/// `false` indicates we are processing a batch from a checkpoint.
193-
pub(crate) fn is_log_batch(&self) -> bool {
170+
fn is_log_batch(&self) -> bool {
194171
self.is_log_batch
195172
}
196173
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//! Deduplication abstraction for log replay processors.
2+
//!
3+
//! The [`Deduplicator`] trait supports two deduplication strategies:
4+
//!
5+
//! - **JSON commit files** (`is_log_batch = true`): Tracks (path, dv_unique_id) and updates
6+
//! the hashmap as files are seen. Implementation: [`FileActionDeduplicator`]
7+
//!
8+
//! - **Checkpoint files** (`is_log_batch = false`): Uses (path, dv_unique_id) to filter actions
9+
//! using a read-only hashmap pre-populated from the commit log phase. Future implementation.
10+
//!
11+
//! [`FileActionDeduplicator`]: crate::log_replay::FileActionDeduplicator
12+
13+
use crate::{
14+
actions::deletion_vector::DeletionVectorDescriptor,
15+
engine_data::{GetData, TypedGetData},
16+
DeltaResult,
17+
};
18+
19+
pub(crate) trait Deduplicator {
20+
/// Key type for identifying file actions.
21+
type Key;
22+
23+
/// Extracts a file action key from the data. Returns `(key, is_add)` if found.
24+
fn extract_file_action<'a>(
25+
&self,
26+
i: usize,
27+
getters: &[&'a dyn GetData<'a>],
28+
skip_removes: bool,
29+
) -> DeltaResult<Option<(Self::Key, bool)>>;
30+
31+
/// Checks if this file has been seen. When `is_log_batch() = true`, updates the hashmap
32+
/// to track new files. Returns `true` if the file should be filtered out.
33+
fn check_and_record_seen(&mut self, key: Self::Key) -> bool;
34+
35+
/// Returns `true` for commit log batches (updates hashmap), `false` for checkpoints (read-only).
36+
fn is_log_batch(&self) -> bool;
37+
38+
/// Extracts the deletion vector unique ID if it exists.
39+
///
40+
/// This function retrieves the necessary fields for constructing a deletion vector unique ID
41+
/// by accessing `getters` at `dv_start_index` and the following two indices. Specifically:
42+
/// - `dv_start_index` retrieves the storage type (`deletionVector.storageType`).
43+
/// - `dv_start_index + 1` retrieves the path or inline deletion vector (`deletionVector.pathOrInlineDv`).
44+
/// - `dv_start_index + 2` retrieves the optional offset (`deletionVector.offset`).
45+
fn extract_dv_unique_id<'a>(
46+
&self,
47+
i: usize,
48+
getters: &[&'a dyn GetData<'a>],
49+
dv_start_index: usize,
50+
) -> DeltaResult<Option<String>> {
51+
match getters[dv_start_index].get_opt(i, "deletionVector.storageType")? {
52+
Some(storage_type) => {
53+
let path_or_inline =
54+
getters[dv_start_index + 1].get(i, "deletionVector.pathOrInlineDv")?;
55+
let offset = getters[dv_start_index + 2].get_opt(i, "deletionVector.offset")?;
56+
57+
Ok(Some(DeletionVectorDescriptor::unique_id_from_parts(
58+
storage_type,
59+
path_or_inline,
60+
offset,
61+
)))
62+
}
63+
None => Ok(None),
64+
}
65+
}
66+
}

kernel/src/scan/log_replay.rs

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::actions::get_log_add_schema;
1212
use crate::engine_data::{GetData, RowVisitor, TypedGetData as _};
1313
use crate::expressions::{column_name, ColumnName, Expression, ExpressionRef, PredicateRef};
1414
use crate::kernel_predicates::{DefaultKernelPredicateEvaluator, KernelPredicateEvaluator as _};
15+
use crate::log_replay::deduplicator::Deduplicator;
1516
use crate::log_replay::{ActionsBatch, FileActionDeduplicator, FileActionKey, LogReplayProcessor};
1617
use crate::scan::Scalar;
1718
use crate::schema::ToSchema as _;
@@ -80,6 +81,15 @@ pub(crate) struct ScanLogReplayProcessor {
8081
}
8182

8283
impl ScanLogReplayProcessor {
84+
// These index positions correspond to the order of columns defined in
85+
// `selected_column_names_and_types()`
86+
const ADD_PATH_INDEX: usize = 0; // Position of "add.path" in getters
87+
const ADD_PARTITION_VALUES_INDEX: usize = 1; // Position of "add.partitionValues" in getters
88+
const ADD_DV_START_INDEX: usize = 2; // Start position of add deletion vector columns
89+
const BASE_ROW_ID_INDEX: usize = 5; // Position of add.baseRowId in getters
90+
const REMOVE_PATH_INDEX: usize = 6; // Position of "remove.path" in getters
91+
const REMOVE_DV_START_INDEX: usize = 7; // Start position of remove deletion vector columns
92+
8393
/// Create a new [`ScanLogReplayProcessor`] instance
8494
pub(crate) fn new(engine: &dyn Engine, state_info: Arc<StateInfo>) -> DeltaResult<Self> {
8595
Self::new_with_seen_files(engine, state_info, Default::default())
@@ -226,40 +236,23 @@ impl ScanLogReplayProcessor {
226236
/// replay visits actions newest-first, so once we've seen a file action for a given (path, dvId)
227237
/// pair, we should ignore all subsequent (older) actions for that same (path, dvId) pair. If the
228238
/// first action for a given file is a remove, then that file does not show up in the result at all.
229-
struct AddRemoveDedupVisitor<'seen> {
230-
deduplicator: FileActionDeduplicator<'seen>,
239+
struct AddRemoveDedupVisitor<D: Deduplicator> {
240+
deduplicator: D,
231241
selection_vector: Vec<bool>,
232242
state_info: Arc<StateInfo>,
233243
partition_filter: Option<PredicateRef>,
234244
row_transform_exprs: Vec<Option<ExpressionRef>>,
235245
}
236246

237-
impl AddRemoveDedupVisitor<'_> {
238-
// These index positions correspond to the order of columns defined in
239-
// `selected_column_names_and_types()`
240-
const ADD_PATH_INDEX: usize = 0; // Position of "add.path" in getters
241-
const ADD_PARTITION_VALUES_INDEX: usize = 1; // Position of "add.partitionValues" in getters
242-
const ADD_DV_START_INDEX: usize = 2; // Start position of add deletion vector columns
243-
const BASE_ROW_ID_INDEX: usize = 5; // Position of add.baseRowId in getters
244-
const REMOVE_PATH_INDEX: usize = 6; // Position of "remove.path" in getters
245-
const REMOVE_DV_START_INDEX: usize = 7; // Start position of remove deletion vector columns
246-
247+
impl<D: Deduplicator> AddRemoveDedupVisitor<D> {
247248
fn new(
248-
seen: &mut HashSet<FileActionKey>,
249+
deduplicator: D,
249250
selection_vector: Vec<bool>,
250251
state_info: Arc<StateInfo>,
251252
partition_filter: Option<PredicateRef>,
252-
is_log_batch: bool,
253-
) -> AddRemoveDedupVisitor<'_> {
253+
) -> AddRemoveDedupVisitor<D> {
254254
AddRemoveDedupVisitor {
255-
deduplicator: FileActionDeduplicator::new(
256-
seen,
257-
is_log_batch,
258-
Self::ADD_PATH_INDEX,
259-
Self::REMOVE_PATH_INDEX,
260-
Self::ADD_DV_START_INDEX,
261-
Self::REMOVE_DV_START_INDEX,
262-
),
255+
deduplicator,
263256
selection_vector,
264257
state_info,
265258
partition_filter,
@@ -311,8 +304,8 @@ impl AddRemoveDedupVisitor<'_> {
311304
// encounter if the table's schema was replaced after the most recent checkpoint.
312305
let partition_values = match &self.state_info.transform_spec {
313306
Some(transform) if is_add => {
314-
let partition_values =
315-
getters[Self::ADD_PARTITION_VALUES_INDEX].get(i, "add.partitionValues")?;
307+
let partition_values = getters[ScanLogReplayProcessor::ADD_PARTITION_VALUES_INDEX]
308+
.get(i, "add.partitionValues")?;
316309
let partition_values = parse_partition_values(
317310
&self.state_info.logical_schema,
318311
transform,
@@ -332,7 +325,7 @@ impl AddRemoveDedupVisitor<'_> {
332325
return Ok(false);
333326
}
334327
let base_row_id: Option<i64> =
335-
getters[Self::BASE_ROW_ID_INDEX].get_opt(i, "add.baseRowId")?;
328+
getters[ScanLogReplayProcessor::BASE_ROW_ID_INDEX].get_opt(i, "add.baseRowId")?;
336329
let transform = self
337330
.state_info
338331
.transform_spec
@@ -355,7 +348,7 @@ impl AddRemoveDedupVisitor<'_> {
355348
}
356349
}
357350

358-
impl RowVisitor for AddRemoveDedupVisitor<'_> {
351+
impl<D: Deduplicator> RowVisitor for AddRemoveDedupVisitor<D> {
359352
fn selected_column_names_and_types(&self) -> (&'static [ColumnName], &'static [DataType]) {
360353
// NOTE: The visitor assumes a schema with adds first and removes optionally afterward.
361354
static NAMES_AND_TYPES: LazyLock<ColumnNamesAndTypes> = LazyLock::new(|| {
@@ -501,12 +494,19 @@ impl LogReplayProcessor for ScanLogReplayProcessor {
501494
let selection_vector = self.build_selection_vector(actions.as_ref())?;
502495
assert_eq!(selection_vector.len(), actions.len());
503496

504-
let mut visitor = AddRemoveDedupVisitor::new(
497+
let deduplicator = FileActionDeduplicator::new(
505498
&mut self.seen_file_keys,
499+
is_log_batch,
500+
Self::ADD_PATH_INDEX,
501+
Self::REMOVE_PATH_INDEX,
502+
Self::ADD_DV_START_INDEX,
503+
Self::REMOVE_DV_START_INDEX,
504+
);
505+
let mut visitor = AddRemoveDedupVisitor::new(
506+
deduplicator,
506507
selection_vector,
507508
self.state_info.clone(),
508509
self.partition_filter.clone(),
509-
is_log_batch,
510510
);
511511
visitor.visit_rows_of(actions.as_ref())?;
512512

0 commit comments

Comments
 (0)