1use std::collections::{HashMap, HashSet};
12use std::sync::Arc;
13
14use ailake_catalog::{read_equality_delete_values, EqualityDeleteFile};
15use ailake_core::{AilakeError, AilakeResult};
16use ailake_store::Store;
17use arrow_array::{
18 Array, BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array, RecordBatch,
19 StringArray,
20};
21use arrow_schema::DataType;
22
23pub struct EqualityDeleteFilter {
36 filters: Vec<(i64, HashMap<String, HashSet<String>>)>,
38}
39
40impl EqualityDeleteFilter {
41 pub async fn from_files(
49 store: &Arc<dyn Store>,
50 files: &[EqualityDeleteFile],
51 ) -> AilakeResult<Self> {
52 let mut filters: Vec<(i64, HashMap<String, HashSet<String>>)> = Vec::new();
53 for edf in files {
54 let bytes = store.get(&edf.path).await?;
55 let pairs = read_equality_delete_values(&bytes)
56 .map_err(|e| AilakeError::Catalog(e.to_string()))?;
57 let mut cols: HashMap<String, HashSet<String>> = HashMap::new();
58 for (col, val) in pairs {
59 cols.entry(col).or_default().insert(val);
60 }
61 filters.push((edf.sequence_number, cols));
62 }
63 Ok(Self { filters })
64 }
65
66 pub fn empty() -> Self {
67 Self {
68 filters: Vec::new(),
69 }
70 }
71
72 pub fn is_empty(&self) -> bool {
73 self.filters.is_empty()
74 }
75
76 fn row_matches(
80 cols: &HashMap<String, HashSet<String>>,
81 batch: &RecordBatch,
82 row_idx: usize,
83 ) -> bool {
84 let mut any_column_found = false;
85 for (col_name, delete_values) in cols {
86 let col_idx = match batch.schema().index_of(col_name.as_str()) {
87 Ok(i) => i,
88 Err(_) => continue, };
90 any_column_found = true;
91 let array = batch.column(col_idx);
92 if array.is_null(row_idx) {
93 return false; }
95 let val_str: Option<String> = match array.data_type() {
96 DataType::Utf8 => array
97 .as_any()
98 .downcast_ref::<StringArray>()
99 .map(|a| a.value(row_idx).to_string()),
100 DataType::LargeUtf8 => array
101 .as_any()
102 .downcast_ref::<arrow_array::LargeStringArray>()
103 .map(|a| a.value(row_idx).to_string()),
104 DataType::Int32 => array
105 .as_any()
106 .downcast_ref::<Int32Array>()
107 .map(|a| a.value(row_idx).to_string()),
108 DataType::Int64 => array
109 .as_any()
110 .downcast_ref::<Int64Array>()
111 .map(|a| a.value(row_idx).to_string()),
112 DataType::Float32 => array
113 .as_any()
114 .downcast_ref::<Float32Array>()
115 .map(|a| a.value(row_idx).to_string()),
116 DataType::Float64 => array
117 .as_any()
118 .downcast_ref::<Float64Array>()
119 .map(|a| a.value(row_idx).to_string()),
120 _ => None,
121 };
122 match val_str {
123 Some(s) if delete_values.contains(&s) => {} Some(_) => return false, None => {} }
127 }
128 any_column_found }
130
131 pub fn should_delete_row(
142 &self,
143 batch: &RecordBatch,
144 row_idx: usize,
145 data_file_sequence_number: i64,
146 ) -> bool {
147 for (delete_seq, cols) in &self.filters {
148 if *delete_seq <= data_file_sequence_number {
149 continue; }
151 if Self::row_matches(cols, batch, row_idx) {
152 return true;
153 }
154 }
155 false
156 }
157
158 pub fn apply(
162 &self,
163 batch: RecordBatch,
164 data_file_sequence_number: i64,
165 ) -> AilakeResult<RecordBatch> {
166 if self.filters.is_empty() {
167 return Ok(batch);
168 }
169 let n = batch.num_rows();
170 let keep: Vec<bool> = (0..n)
171 .map(|i| !self.should_delete_row(&batch, i, data_file_sequence_number))
172 .collect();
173 let mask = BooleanArray::from(keep);
174 arrow_select::filter::filter_record_batch(&batch, &mask)
175 .map_err(|e| AilakeError::Arrow(e.to_string()))
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use std::sync::Arc;
182
183 use arrow_array::{Int32Array, RecordBatch, StringArray};
184 use arrow_schema::{DataType, Field, Schema};
185
186 use super::EqualityDeleteFilter;
187 use std::collections::{HashMap, HashSet};
188
189 fn make_batch() -> RecordBatch {
190 let schema = Arc::new(Schema::new(vec![
191 Field::new("doc_id", DataType::Utf8, true),
192 Field::new("score", DataType::Int32, true),
193 ]));
194 RecordBatch::try_new(
195 schema,
196 vec![
197 Arc::new(StringArray::from(vec!["doc-a", "doc-b", "doc-c", "doc-d"])),
198 Arc::new(Int32Array::from(vec![1, 2, 3, 4])),
199 ],
200 )
201 .unwrap()
202 }
203
204 fn filter_with(filters: HashMap<String, HashSet<String>>) -> EqualityDeleteFilter {
209 EqualityDeleteFilter {
210 filters: vec![(1, filters)],
211 }
212 }
213
214 #[test]
215 fn empty_filter_is_no_op() {
216 let batch = make_batch();
217 let f = filter_with(HashMap::new());
218 let result = f.apply(batch.clone(), 0).unwrap();
219 assert_eq!(result.num_rows(), 4);
220 }
221
222 #[test]
223 fn single_value_deleted() {
224 let mut filters = HashMap::new();
225 filters.insert("doc_id".into(), ["doc-b".to_string()].into());
226 let f = filter_with(filters);
227 let result = f.apply(make_batch(), 0).unwrap();
228 assert_eq!(result.num_rows(), 3);
229 let ids = result
230 .column(0)
231 .as_any()
232 .downcast_ref::<StringArray>()
233 .unwrap();
234 assert_eq!(ids.value(0), "doc-a");
235 assert_eq!(ids.value(1), "doc-c");
236 assert_eq!(ids.value(2), "doc-d");
237 }
238
239 #[test]
240 fn multiple_values_deleted() {
241 let mut filters = HashMap::new();
242 filters.insert(
243 "doc_id".into(),
244 ["doc-a".to_string(), "doc-c".to_string()].into(),
245 );
246 let f = filter_with(filters);
247 let result = f.apply(make_batch(), 0).unwrap();
248 assert_eq!(result.num_rows(), 2);
249 let ids = result
250 .column(0)
251 .as_any()
252 .downcast_ref::<StringArray>()
253 .unwrap();
254 assert_eq!(ids.value(0), "doc-b");
255 assert_eq!(ids.value(1), "doc-d");
256 }
257
258 #[test]
259 fn column_absent_from_batch_is_skipped() {
260 let mut filters = HashMap::new();
261 filters.insert("nonexistent_col".into(), ["x".to_string()].into());
262 let f = filter_with(filters);
263 let result = f.apply(make_batch(), 0).unwrap();
264 assert_eq!(result.num_rows(), 4); }
266
267 #[test]
268 fn numeric_column_deletion() {
269 let mut filters = HashMap::new();
270 filters.insert("score".into(), ["2".to_string(), "4".to_string()].into());
271 let f = filter_with(filters);
272 let result = f.apply(make_batch(), 0).unwrap();
273 assert_eq!(result.num_rows(), 2);
274 let ids = result
275 .column(0)
276 .as_any()
277 .downcast_ref::<StringArray>()
278 .unwrap();
279 assert_eq!(ids.value(0), "doc-a");
280 assert_eq!(ids.value(1), "doc-c");
281 }
282
283 #[test]
286 fn delete_does_not_mask_a_data_file_with_equal_sequence_number() {
287 let mut filters = HashMap::new();
293 filters.insert("doc_id".into(), ["doc-b".to_string()].into());
294 let f = EqualityDeleteFilter {
295 filters: vec![(5, filters)],
296 };
297 let result = f.apply(make_batch(), 5).unwrap();
298 assert_eq!(result.num_rows(), 4, "equal sequence number must not mask");
299 }
300
301 #[test]
302 fn delete_does_not_mask_a_data_file_committed_after_it() {
303 let mut filters = HashMap::new();
308 filters.insert("doc_id".into(), ["doc-b".to_string()].into());
309 let f = EqualityDeleteFilter {
310 filters: vec![(2, filters)],
311 };
312 let result = f.apply(make_batch(), 5).unwrap();
313 assert_eq!(
314 result.num_rows(),
315 4,
316 "a delete must not mask a data file committed after it"
317 );
318 }
319
320 #[test]
321 fn delete_masks_only_data_files_committed_strictly_before_it() {
322 let mut filters = HashMap::new();
323 filters.insert("doc_id".into(), ["doc-b".to_string()].into());
324 let f = EqualityDeleteFilter {
325 filters: vec![(5, filters)],
326 };
327 let result = f.apply(make_batch(), 0).unwrap();
328 assert_eq!(result.num_rows(), 3, "older data file must still be masked");
329 }
330
331 #[test]
332 fn multiple_delete_files_each_scoped_to_their_own_sequence_number() {
333 let mut del_a = HashMap::new();
334 del_a.insert("doc_id".into(), ["doc-a".to_string()].into());
335 let mut del_c = HashMap::new();
336 del_c.insert("doc_id".into(), ["doc-c".to_string()].into());
337 let f = EqualityDeleteFilter {
340 filters: vec![(1, del_a), (10, del_c)],
341 };
342 let result = f.apply(make_batch(), 5).unwrap();
345 let ids = result
346 .column(0)
347 .as_any()
348 .downcast_ref::<StringArray>()
349 .unwrap();
350 let survivors: Vec<&str> = (0..result.num_rows()).map(|i| ids.value(i)).collect();
351 assert_eq!(survivors, vec!["doc-a", "doc-b", "doc-d"]);
352 }
353}