1use std::sync::Arc;
19
20use arrow_array::{Array, ArrayRef, RecordBatch, StructArray, make_array};
21use arrow_buffer::NullBuffer;
22use arrow_schema::{DataType, Field, FieldRef, Fields, Schema, SchemaRef};
23
24use crate::arrow::schema::{schema_to_arrow_schema, try_get_field_id_from_metadata};
25use crate::error::{Result, invalid_data};
26use crate::spec::Schema as IcebergSchema;
27use crate::{Error, ErrorKind};
28
29#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct RecordBatchProjector {
32 field_indices: Vec<Vec<usize>>,
36 projected_schema: SchemaRef,
38}
39
40impl RecordBatchProjector {
41 pub(crate) fn new<F1, F2>(
48 original_schema: SchemaRef,
49 field_ids: &[i32],
50 field_id_fetch_func: F1,
51 searchable_field_func: F2,
52 ) -> Result<Self>
53 where
54 F1: Fn(&Field) -> Result<Option<i64>>,
55 F2: Fn(&Field) -> bool,
56 {
57 let mut field_indices = Vec::with_capacity(field_ids.len());
58 let mut fields = Vec::with_capacity(field_ids.len());
59 for &id in field_ids {
60 let mut field_index = vec![];
61 let field = Self::fetch_field_index(
62 original_schema.fields(),
63 &mut field_index,
64 id as i64,
65 &field_id_fetch_func,
66 &searchable_field_func,
67 )?
68 .ok_or_else(|| {
69 Error::new(ErrorKind::Unexpected, "Field not found")
70 .with_context("field_id", id.to_string())
71 })?;
72 fields.push(field.clone());
73 field_indices.push(field_index);
74 }
75 let delete_arrow_schema = Arc::new(Schema::new(fields));
76 Ok(Self {
77 field_indices,
78 projected_schema: delete_arrow_schema,
79 })
80 }
81
82 pub fn from_iceberg_schema(
91 iceberg_schema: Arc<IcebergSchema>,
92 target_field_ids: &[i32],
93 ) -> Result<Self> {
94 let arrow_schema_with_ids = Arc::new(schema_to_arrow_schema(&iceberg_schema)?);
95
96 let field_id_fetch_func = |field: &Field| -> Result<Option<i64>> {
97 Ok(try_get_field_id_from_metadata(field)?.map(|id| id as i64))
98 };
99
100 let searchable_field_func = |_field: &Field| -> bool { true };
101
102 Self::new(
103 arrow_schema_with_ids,
104 target_field_ids,
105 field_id_fetch_func,
106 searchable_field_func,
107 )
108 }
109
110 fn fetch_field_index<F1, F2>(
111 fields: &Fields,
112 index_vec: &mut Vec<usize>,
113 target_field_id: i64,
114 field_id_fetch_func: &F1,
115 searchable_field_func: &F2,
116 ) -> Result<Option<FieldRef>>
117 where
118 F1: Fn(&Field) -> Result<Option<i64>>,
119 F2: Fn(&Field) -> bool,
120 {
121 for (pos, field) in fields.iter().enumerate() {
122 let id = field_id_fetch_func(field)?;
123 if let Some(id) = id
124 && target_field_id == id
125 {
126 index_vec.push(pos);
127 return Ok(Some(field.clone()));
128 }
129 if let DataType::Struct(inner) = field.data_type()
130 && searchable_field_func(field)
131 && let Some(res) = Self::fetch_field_index(
132 inner,
133 index_vec,
134 target_field_id,
135 field_id_fetch_func,
136 searchable_field_func,
137 )?
138 {
139 index_vec.push(pos);
140 return Ok(Some(res));
141 }
142 }
143 Ok(None)
144 }
145
146 pub(crate) fn projected_schema_ref(&self) -> &SchemaRef {
148 &self.projected_schema
149 }
150
151 pub(crate) fn project_batch(&self, batch: RecordBatch) -> Result<RecordBatch> {
153 RecordBatch::try_new(
154 self.projected_schema.clone(),
155 self.project_column(batch.columns())?,
156 )
157 .map_err(|err| invalid_data!("{err}"))
158 }
159
160 pub fn project_column(&self, batch: &[ArrayRef]) -> Result<Vec<ArrayRef>> {
162 let mut columns = Vec::with_capacity(self.field_indices.len());
163 for index_vec in &self.field_indices {
164 columns.push(Self::get_column_by_field_index(batch, index_vec)?);
165 }
166 Ok(columns)
167 }
168
169 fn get_column_by_field_index(batch: &[ArrayRef], field_index: &[usize]) -> Result<ArrayRef> {
170 let mut rev_iterator = field_index.iter().rev();
171 let mut array = &batch[*rev_iterator.next().unwrap()];
172 let mut ancestor_nulls = None;
173 for idx in rev_iterator {
174 let struct_array = array
175 .as_any()
176 .downcast_ref::<StructArray>()
177 .ok_or_else(|| {
178 Error::new(ErrorKind::Unexpected, "Cannot convert Array to StructArray")
179 })?;
180 ancestor_nulls = NullBuffer::union(ancestor_nulls.as_ref(), struct_array.nulls());
181 array = struct_array.column(*idx);
182 }
183 let Some(ancestor_nulls) = ancestor_nulls else {
184 return Ok(Arc::clone(array));
187 };
188 let null_buffer = NullBuffer::union(Some(&ancestor_nulls), array.logical_nulls().as_ref());
189 Ok(make_array(
190 array.to_data().into_builder().nulls(null_buffer).build()?,
191 ))
192 }
193}
194
195#[cfg(test)]
196mod test {
197 use std::sync::Arc;
198
199 use arrow_array::{Array, ArrayRef, Int32Array, RecordBatch, StringArray, StructArray};
200 use arrow_buffer::NullBuffer;
201 use arrow_schema::{DataType, Field, Fields, Schema};
202
203 use crate::arrow::record_batch_projector::RecordBatchProjector;
204 use crate::spec::{NestedField, PrimitiveType, Schema as IcebergSchema, Type};
205 use crate::{Error, ErrorKind};
206
207 fn nested_projector() -> (RecordBatchProjector, Field, Field) {
208 let leaf_field = Field::new("leaf", DataType::Int32, true);
209 let inner_field = Field::new(
210 "inner",
211 DataType::Struct(Fields::from(vec![leaf_field.clone()])),
212 true,
213 );
214 let outer_field = Field::new(
215 "outer",
216 DataType::Struct(Fields::from(vec![inner_field.clone()])),
217 true,
218 );
219 let schema = Arc::new(Schema::new(vec![outer_field]));
220 let field_id_fetch_func = |field: &Field| -> crate::Result<Option<i64>> {
221 match field.name().as_str() {
222 "outer" => Ok(Some(1)),
223 "inner" => Ok(Some(2)),
224 "leaf" => Ok(Some(3)),
225 _ => Ok(None),
226 }
227 };
228 let projector =
229 RecordBatchProjector::new(schema, &[3], field_id_fetch_func, |_| true).unwrap();
230 (projector, inner_field, leaf_field)
231 }
232
233 #[test]
234 fn test_record_batch_projector_nested_level() {
235 let inner_fields = vec![
236 Field::new("inner_field1", DataType::Int32, false),
237 Field::new("inner_field2", DataType::Utf8, false),
238 ];
239 let fields = vec![
240 Field::new("field1", DataType::Int32, false),
241 Field::new(
242 "field2",
243 DataType::Struct(Fields::from(inner_fields.clone())),
244 false,
245 ),
246 ];
247 let schema = Arc::new(Schema::new(fields));
248
249 let field_id_fetch_func = |field: &Field| match field.name().as_str() {
250 "field1" => Ok(Some(1)),
251 "field2" => Ok(Some(2)),
252 "inner_field1" => Ok(Some(3)),
253 "inner_field2" => Ok(Some(4)),
254 _ => Err(Error::new(ErrorKind::Unexpected, "Field id not found")),
255 };
256 let projector =
257 RecordBatchProjector::new(schema.clone(), &[1, 3], field_id_fetch_func, |_| true)
258 .unwrap();
259
260 assert_eq!(projector.field_indices.len(), 2);
261 assert_eq!(projector.field_indices[0], vec![0]);
262 assert_eq!(projector.field_indices[1], vec![0, 1]);
263
264 let int_array = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
265 let inner_int_array = Arc::new(Int32Array::from(vec![4, 5, 6])) as ArrayRef;
266 let inner_string_array = Arc::new(StringArray::from(vec!["x", "y", "z"])) as ArrayRef;
267 let struct_array = Arc::new(StructArray::from(vec![
268 (
269 Arc::new(inner_fields[0].clone()),
270 inner_int_array as ArrayRef,
271 ),
272 (
273 Arc::new(inner_fields[1].clone()),
274 inner_string_array as ArrayRef,
275 ),
276 ])) as ArrayRef;
277 let batch = RecordBatch::try_new(schema, vec![int_array, struct_array]).unwrap();
278
279 let projected_batch = projector.project_batch(batch).unwrap();
280 assert_eq!(projected_batch.num_columns(), 2);
281 let projected_int_array = projected_batch
282 .column(0)
283 .as_any()
284 .downcast_ref::<Int32Array>()
285 .unwrap();
286 let projected_inner_int_array = projected_batch
287 .column(1)
288 .as_any()
289 .downcast_ref::<Int32Array>()
290 .unwrap();
291
292 assert_eq!(projected_int_array.values(), &[1, 2, 3]);
293 assert_eq!(projected_inner_int_array.values(), &[4, 5, 6]);
294 }
295
296 #[test]
297 fn test_record_batch_projector_top_level_nullable_column() {
298 let iceberg_schema = IcebergSchema::builder()
299 .with_schema_id(0)
300 .with_fields(vec![
301 NestedField::optional(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
302 ])
303 .build()
304 .unwrap();
305 let projector =
306 RecordBatchProjector::from_iceberg_schema(Arc::new(iceberg_schema), &[1]).unwrap();
307 let input = Arc::new(Int32Array::from(vec![Some(10), None, Some(30)])) as ArrayRef;
308
309 let projected = projector
310 .project_column(std::slice::from_ref(&input))
311 .unwrap();
312 assert!(Arc::ptr_eq(&projected[0], &input));
313 let projected_array = projected[0].as_any().downcast_ref::<Int32Array>().unwrap();
314
315 assert_eq!(projected_array.value(0), 10);
316 assert_eq!(projected_array.null_count(), 1);
317 assert!(projected_array.is_null(1));
318 assert_eq!(projected_array.value(2), 30);
319 }
320
321 #[test]
322 fn test_record_batch_projector_nested_nullable_leaf_without_parent_nulls() {
323 let (projector, inner_field, leaf_field) = nested_projector();
324 let leaf = Arc::new(Int32Array::from(vec![Some(10), None, Some(30)])) as ArrayRef;
325 let inner = Arc::new(StructArray::new(
326 Fields::from(vec![leaf_field]),
327 vec![leaf.clone()],
328 None,
329 )) as ArrayRef;
330 let outer = Arc::new(StructArray::new(
331 Fields::from(vec![inner_field]),
332 vec![inner],
333 None,
334 )) as ArrayRef;
335
336 let projected = projector.project_column(&[outer]).unwrap();
337 assert!(Arc::ptr_eq(&projected[0], &leaf));
338 }
339
340 #[test]
341 fn test_record_batch_projector_nested_all_valid_slice_reuses_leaf() {
342 let (projector, inner_field, leaf_field) = nested_projector();
343 let leaf = Arc::new(Int32Array::from(vec![Some(0), Some(10), None, Some(30)])) as ArrayRef;
344 let inner = Arc::new(StructArray::new(
345 Fields::from(vec![leaf_field]),
346 vec![leaf],
347 None,
348 )) as ArrayRef;
349 let outer = StructArray::new(
350 Fields::from(vec![inner_field]),
351 vec![inner],
352 Some(NullBuffer::from(vec![false, true, true, true])),
353 )
354 .slice(1, 3);
355 assert_eq!(outer.null_count(), 0);
356 assert!(outer.nulls().is_some());
357
358 let sliced_inner = outer
359 .column(0)
360 .as_any()
361 .downcast_ref::<StructArray>()
362 .unwrap();
363 let sliced_leaf = sliced_inner.column(0).clone();
364 let projected = projector
365 .project_column(&[Arc::new(outer) as ArrayRef])
366 .unwrap();
367 assert!(Arc::ptr_eq(&projected[0], &sliced_leaf));
368 }
369
370 #[test]
371 fn test_record_batch_projector_propagates_nested_parent_nulls() {
372 let (projector, inner_field, leaf_field) = nested_projector();
373 let leaf = Arc::new(Int32Array::from(vec![Some(10), Some(20), Some(30), None])) as ArrayRef;
374 let inner = Arc::new(StructArray::new(
375 Fields::from(vec![leaf_field]),
376 vec![leaf],
377 Some(NullBuffer::from(vec![true, true, false, true])),
378 )) as ArrayRef;
379 let outer = Arc::new(StructArray::new(
380 Fields::from(vec![inner_field]),
381 vec![inner],
382 Some(NullBuffer::from(vec![true, false, true, true])),
383 )) as ArrayRef;
384
385 let projected = projector.project_column(&[outer]).unwrap();
386 assert_eq!(
387 projected[0].as_ref(),
388 &Int32Array::from(vec![Some(10), None, None, None])
389 );
390 }
391
392 #[test]
393 fn test_field_not_found() {
394 let inner_fields = vec![
395 Field::new("inner_field1", DataType::Int32, false),
396 Field::new("inner_field2", DataType::Utf8, false),
397 ];
398
399 let fields = vec![
400 Field::new("field1", DataType::Int32, false),
401 Field::new(
402 "field2",
403 DataType::Struct(Fields::from(inner_fields.clone())),
404 false,
405 ),
406 ];
407 let schema = Arc::new(Schema::new(fields));
408
409 let field_id_fetch_func = |field: &Field| match field.name().as_str() {
410 "field1" => Ok(Some(1)),
411 "field2" => Ok(Some(2)),
412 "inner_field1" => Ok(Some(3)),
413 "inner_field2" => Ok(Some(4)),
414 _ => Err(Error::new(ErrorKind::Unexpected, "Field id not found")),
415 };
416 let projector =
417 RecordBatchProjector::new(schema.clone(), &[1, 5], field_id_fetch_func, |_| true);
418
419 assert!(projector.is_err());
420 }
421
422 #[test]
423 fn test_field_not_reachable() {
424 let inner_fields = vec![
425 Field::new("inner_field1", DataType::Int32, false),
426 Field::new("inner_field2", DataType::Utf8, false),
427 ];
428
429 let fields = vec![
430 Field::new("field1", DataType::Int32, false),
431 Field::new(
432 "field2",
433 DataType::Struct(Fields::from(inner_fields.clone())),
434 false,
435 ),
436 ];
437 let schema = Arc::new(Schema::new(fields));
438
439 let field_id_fetch_func = |field: &Field| match field.name().as_str() {
440 "field1" => Ok(Some(1)),
441 "field2" => Ok(Some(2)),
442 "inner_field1" => Ok(Some(3)),
443 "inner_field2" => Ok(Some(4)),
444 _ => Err(Error::new(ErrorKind::Unexpected, "Field id not found")),
445 };
446 let projector =
447 RecordBatchProjector::new(schema.clone(), &[3], field_id_fetch_func, |_| false);
448 assert!(projector.is_err());
449
450 let projector =
451 RecordBatchProjector::new(schema.clone(), &[3], field_id_fetch_func, |_| true);
452 assert!(projector.is_ok());
453 }
454
455 #[test]
456 fn test_from_iceberg_schema() {
457 let iceberg_schema = IcebergSchema::builder()
458 .with_schema_id(0)
459 .with_fields(vec![
460 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
461 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
462 NestedField::optional(3, "age", Type::Primitive(PrimitiveType::Int)).into(),
463 ])
464 .build()
465 .unwrap();
466
467 let projector =
468 RecordBatchProjector::from_iceberg_schema(Arc::new(iceberg_schema), &[1, 3]).unwrap();
469
470 assert_eq!(projector.field_indices.len(), 2);
471 assert_eq!(projector.projected_schema_ref().fields().len(), 2);
472 assert_eq!(projector.projected_schema_ref().field(0).name(), "id");
473 assert_eq!(projector.projected_schema_ref().field(1).name(), "age");
474 }
475}