1mod plan;
73mod rewriter;
74pub(crate) mod writer;
75
76use std::sync::Arc;
77
78use arrow_array::RecordBatch;
79use futures::TryStreamExt;
80pub use plan::CowRewriteFile;
81pub use rewriter::{CowBatchRewrite, CowBatchRewriter};
82
83use crate::expr::Predicate;
84use crate::scan::FileScanTaskStream;
85use crate::spec::{DataFile, PartitionKey};
86use crate::table::Table;
87use crate::{Error, ErrorKind, Result};
88
89#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
91pub struct CowRewriteStats {
92 pub candidate_files: usize,
94 pub rewritten_files: usize,
96 pub unchanged_files: usize,
98 pub input_rows: u64,
100 pub output_rows: u64,
105 pub changed_batches: u64,
108}
109
110#[derive(Debug, Default)]
112pub struct CowRewriteResult {
113 pub removed_data_files: Vec<DataFile>,
115 pub added_data_files: Vec<DataFile>,
117 pub unchanged_data_files: Vec<DataFile>,
126 pub stats: CowRewriteStats,
128}
129
130impl CowRewriteResult {
131 pub fn has_changes(&self) -> bool {
133 !self.removed_data_files.is_empty() || !self.added_data_files.is_empty()
134 }
135}
136
137pub struct CowRewriteBuilder<'a> {
139 table: &'a Table,
140 predicate: Predicate,
141 snapshot_id: Option<i64>,
142 batch_size: Option<usize>,
143 case_sensitive: bool,
144 rewriter: Option<Arc<dyn CowBatchRewriter>>,
145}
146
147impl<'a> CowRewriteBuilder<'a> {
148 pub fn new(table: &'a Table) -> Self {
150 Self {
151 table,
152 predicate: Predicate::AlwaysTrue,
153 snapshot_id: None,
154 batch_size: None,
155 case_sensitive: true,
156 rewriter: None,
157 }
158 }
159
160 pub fn with_predicate(mut self, predicate: Predicate) -> Self {
162 self.predicate = predicate;
163 self
164 }
165
166 pub fn with_snapshot_id(mut self, snapshot_id: i64) -> Self {
168 self.snapshot_id = Some(snapshot_id);
169 self
170 }
171
172 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
174 self.batch_size = Some(batch_size);
175 self
176 }
177
178 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
180 self.case_sensitive = case_sensitive;
181 self
182 }
183
184 pub fn with_rewriter(mut self, rewriter: Arc<dyn CowBatchRewriter>) -> Self {
186 self.rewriter = Some(rewriter);
187 self
188 }
189
190 pub async fn rewrite(self) -> Result<CowRewriteResult> {
204 let rewriter = self.rewriter.ok_or_else(|| {
205 Error::new(
206 ErrorKind::PreconditionFailed,
207 "COW rewrite requires a batch rewriter",
208 )
209 })?;
210 let files = plan::plan_cow_rewrite_files(
211 self.table,
212 Some(self.predicate),
213 self.snapshot_id,
214 self.case_sensitive,
215 )
216 .await?;
217
218 let mut result = CowRewriteResult {
219 stats: CowRewriteStats {
220 candidate_files: files.len(),
221 ..CowRewriteStats::default()
222 },
223 ..CowRewriteResult::default()
224 };
225
226 for file in files {
227 let CowRewriteFile {
228 old_data_file,
229 scan_task,
230 } = file;
231 let write_schema = scan_task.schema_ref();
238 let has_delete_files = !scan_task.deletes().is_empty();
239
240 let mut prefix: Vec<RecordBatch> = Vec::new();
255 let mut file_changed = false;
256 let mut file_input_rows = 0_u64;
257 let mut file_output_rows = 0_u64;
258 let mut writer: Option<Box<dyn crate::writer::IcebergWriter>> = None;
259 #[cfg(debug_assertions)]
262 let partition_guard =
263 DebugPartitionGuard::new(self.table, &old_data_file, &write_schema)?;
264
265 let tasks = Box::pin(futures::stream::iter(vec![Ok(scan_task)])) as FileScanTaskStream;
269
270 let mut reader_builder = self.table.reader_builder();
275 if let Some(batch_size) = self.batch_size {
276 reader_builder = reader_builder.with_batch_size(batch_size);
277 }
278
279 let mut batches = reader_builder.build().read(tasks)?.stream();
280 while let Some(batch) = batches.try_next().await? {
281 result.stats.input_rows += batch.num_rows() as u64;
282 file_input_rows += batch.num_rows() as u64;
283
284 let rewrite = rewriter.rewrite_batch(batch)?;
285 let changed = rewrite.changed || rewrite.output.is_none();
292 if changed {
293 file_changed = true;
294 result.stats.changed_batches += 1;
295 }
296
297 #[cfg(debug_assertions)]
301 if let (Some(guard), Some(output)) = (&partition_guard, &rewrite.output) {
302 guard.check(output)?;
303 }
304
305 if file_changed
309 && writer.is_none()
310 && (!prefix.is_empty() || rewrite.output.is_some())
311 {
312 let partition_key =
313 source_partition_key(self.table, &old_data_file, &write_schema)?;
314 writer = Some(
315 writer::build_replacement_writer(
316 self.table,
317 write_schema.clone(),
318 Some(partition_key),
319 )
320 .await?,
321 );
322 }
323
324 if let Some(writer) = writer.as_mut() {
325 for prefix_batch in prefix.drain(..) {
326 writer.write(prefix_batch).await?;
327 }
328 if let Some(output) = rewrite.output {
329 file_output_rows += output.num_rows() as u64;
330 writer.write(output).await?;
331 }
332 } else if let Some(output) = rewrite.output {
333 file_output_rows += output.num_rows() as u64;
334 prefix.push(output);
335 }
336 }
337
338 let fully_removed_by_deletes =
344 !file_changed && file_input_rows == 0 && has_delete_files;
345
346 if file_changed || fully_removed_by_deletes {
347 result.stats.rewritten_files += 1;
348 result.stats.output_rows += file_output_rows;
349 result.removed_data_files.push(old_data_file);
350
351 if let Some(mut writer) = writer {
352 let added_data_files = writer.close().await?;
353 result.added_data_files.extend(added_data_files);
354 }
355 } else {
358 result.stats.unchanged_files += 1;
359 result.unchanged_data_files.push(old_data_file);
360 }
362 }
363
364 Ok(result)
365 }
366}
367
368fn source_partition_key(
369 table: &Table,
370 data_file: &DataFile,
371 schema: &crate::spec::SchemaRef,
372) -> Result<PartitionKey> {
373 let spec = table
374 .metadata()
375 .partition_spec_by_id(data_file.partition_spec_id)
376 .ok_or_else(|| {
377 Error::new(
378 ErrorKind::DataInvalid,
379 format!(
380 "Missing partition spec {} for COW rewrite source file",
381 data_file.partition_spec_id
382 ),
383 )
384 })?
385 .as_ref()
386 .clone();
387 spec.partition_type(schema).map_err(|err| {
392 Error::new(
393 ErrorKind::DataInvalid,
394 format!(
395 "Cannot bind partition spec {} to the planned snapshot schema for COW rewrite",
396 data_file.partition_spec_id
397 ),
398 )
399 .with_source(err)
400 })?;
401
402 Ok(PartitionKey::new(
403 spec,
404 schema.clone(),
405 data_file.partition().clone(),
406 ))
407}
408
409#[cfg(debug_assertions)]
416struct DebugPartitionGuard {
417 calculator: crate::arrow::PartitionValueCalculator,
418 partition_type: crate::spec::StructType,
419 expected: crate::spec::Struct,
420}
421
422#[cfg(debug_assertions)]
423impl DebugPartitionGuard {
424 fn new(
428 table: &Table,
429 data_file: &DataFile,
430 schema: &crate::spec::SchemaRef,
431 ) -> Result<Option<Self>> {
432 let spec = table
433 .metadata()
434 .partition_spec_by_id(data_file.partition_spec_id)
435 .ok_or_else(|| {
436 Error::new(
437 ErrorKind::DataInvalid,
438 format!(
439 "Missing partition spec {} for COW rewrite source file",
440 data_file.partition_spec_id
441 ),
442 )
443 })?;
444 if spec.is_unpartitioned() {
445 return Ok(None);
446 }
447
448 Ok(Some(Self {
449 calculator: crate::arrow::PartitionValueCalculator::try_new(spec, schema)?,
450 partition_type: spec.partition_type(schema)?,
451 expected: data_file.partition().clone(),
452 }))
453 }
454
455 fn check(&self, output: &RecordBatch) -> Result<()> {
458 let partition_array = self.calculator.calculate(output)?;
459 let values = crate::arrow::arrow_struct_to_literal(&partition_array, &self.partition_type)?;
460 for value in values {
461 debug_assert_eq!(
462 value,
463 Some(crate::spec::Literal::Struct(self.expected.clone())),
464 "COW rewriter must preserve the source file's partition values: \
465 replacement files record the source partition verbatim, so \
466 mutating a partition-source column writes rows under a \
467 partition they do not belong to"
468 );
469 }
470 Ok(())
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use std::collections::{HashMap, HashSet};
477 use std::sync::Arc;
478
479 use arrow_array::{Array, ArrayRef, BooleanArray, Int32Array, RecordBatch};
480 use arrow_schema::{DataType, Field, Schema as ArrowSchema};
481 use futures::TryStreamExt;
482 use parquet::arrow::PARQUET_FIELD_ID_META_KEY;
483 use tempfile::TempDir;
484
485 use crate::cow_rewrite::{CowBatchRewrite, CowBatchRewriter, CowRewriteBuilder};
486 use crate::io::LocalFsStorageFactory;
487 use crate::memory::{MEMORY_CATALOG_WAREHOUSE, MemoryCatalogBuilder};
488 use crate::scan::{FileScanTask, FileScanTaskStream};
489 use crate::spec::{
490 DataFile, Literal, NestedField, PrimitiveType, Schema, Struct, TableProperties, Transform,
491 Type,
492 };
493 use crate::table::Table;
494 use crate::test_utils::scan::TableTestFixture;
495 use crate::transaction::{AddColumn, ApplyTransactionAction, Transaction};
496 use crate::{Catalog, CatalogBuilder, Error, ErrorKind, NamespaceIdent, Result, TableCreation};
497
498 struct KeepAll;
499
500 impl CowBatchRewriter for KeepAll {
501 fn rewrite_batch(&self, batch: RecordBatch) -> Result<CowBatchRewrite> {
502 Ok(CowBatchRewrite {
503 output: Some(batch),
504 changed: false,
505 })
506 }
507 }
508
509 struct DeleteEvenIds;
510
511 impl CowBatchRewriter for DeleteEvenIds {
512 fn rewrite_batch(&self, batch: RecordBatch) -> Result<CowBatchRewrite> {
513 let ids = batch
514 .column_by_name("id")
515 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing id column"))?
516 .as_any()
517 .downcast_ref::<Int32Array>()
518 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "id must be Int32"))?;
519
520 let keep =
521 BooleanArray::from_iter((0..ids.len()).map(|row| Some(ids.value(row) % 2 != 0)));
522 let filtered = arrow_select::filter::filter_record_batch(&batch, &keep)
523 .map_err(|err| Error::new(ErrorKind::Unexpected, err.to_string()))?;
524
525 Ok(CowBatchRewrite {
526 changed: filtered.num_rows() != batch.num_rows(),
527 output: (filtered.num_rows() > 0).then_some(filtered),
528 })
529 }
530 }
531
532 struct IncrementValueForEvenIds;
533
534 impl CowBatchRewriter for IncrementValueForEvenIds {
535 fn rewrite_batch(&self, batch: RecordBatch) -> Result<CowBatchRewrite> {
536 let ids = batch
537 .column_by_name("id")
538 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing id column"))?
539 .as_any()
540 .downcast_ref::<Int32Array>()
541 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "id must be Int32"))?;
542 let values = batch
543 .column_by_name("value")
544 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing value column"))?
545 .as_any()
546 .downcast_ref::<Int32Array>()
547 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "value must be Int32"))?;
548
549 let mut changed = false;
550 let updated_values = Int32Array::from_iter((0..values.len()).map(|row| {
551 let value = values.value(row);
552 if ids.value(row) % 2 == 0 {
553 changed = true;
554 Some(value + 10)
555 } else {
556 Some(value)
557 }
558 }));
559 let output = RecordBatch::try_new(batch.schema(), vec![
560 batch.column(0).clone(),
561 Arc::new(updated_values),
562 ])
563 .map_err(|err| Error::new(ErrorKind::Unexpected, err.to_string()))?;
564
565 Ok(CowBatchRewrite {
566 output: Some(output),
567 changed,
568 })
569 }
570 }
571
572 struct CowRewriteFixture {
573 _temp_dir: TempDir,
574 table: Table,
575 }
576
577 async fn test_table_with_ids(ids: Vec<i32>) -> Result<CowRewriteFixture> {
578 let temp_dir = TempDir::new().unwrap();
579 let warehouse = format!("file://{}", temp_dir.path().join("warehouse").display());
580 let catalog = MemoryCatalogBuilder::default()
581 .with_storage_factory(Arc::new(LocalFsStorageFactory))
582 .load(
583 "memory",
584 HashMap::from([(MEMORY_CATALOG_WAREHOUSE.to_string(), warehouse)]),
585 )
586 .await?;
587 let namespace = NamespaceIdent::new("ns".to_string());
588 catalog.create_namespace(&namespace, HashMap::new()).await?;
589
590 let schema = Schema::builder()
591 .with_fields(vec![
592 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
593 ])
594 .build()?;
595 let table = catalog
596 .create_table(
597 &namespace,
598 TableCreation::builder()
599 .name("cow_rewrite_fixture".to_string())
600 .schema(schema)
601 .build(),
602 )
603 .await?;
604
605 let arrow_schema = Arc::new(ArrowSchema::new(vec![
606 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
607 PARQUET_FIELD_ID_META_KEY.to_string(),
608 "1".to_string(),
609 )])),
610 ]));
611 let batch = RecordBatch::try_new(arrow_schema, vec![
612 Arc::new(Int32Array::from(ids)) as ArrayRef
613 ])?;
614 let data_files = super::writer::write_replacement_batches(
615 &table,
616 table.metadata().current_schema().clone(),
617 None,
618 futures::stream::iter(vec![Ok(batch)]),
619 )
620 .await?;
621
622 let tx = Transaction::new(&table);
623 let tx = tx.fast_append().add_data_files(data_files).apply(tx)?;
624 let table = tx.commit(&catalog).await?;
625
626 Ok(CowRewriteFixture {
627 _temp_dir: temp_dir,
628 table,
629 })
630 }
631
632 async fn test_table_with_id_batches(batches: Vec<Vec<i32>>) -> Result<CowRewriteFixture> {
633 let temp_dir = TempDir::new().unwrap();
634 let warehouse = format!("file://{}", temp_dir.path().join("warehouse").display());
635 let catalog = MemoryCatalogBuilder::default()
636 .with_storage_factory(Arc::new(LocalFsStorageFactory))
637 .load(
638 "memory",
639 HashMap::from([(MEMORY_CATALOG_WAREHOUSE.to_string(), warehouse)]),
640 )
641 .await?;
642 let namespace = NamespaceIdent::new("ns".to_string());
643 catalog.create_namespace(&namespace, HashMap::new()).await?;
644
645 let schema = Schema::builder()
646 .with_fields(vec![
647 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
648 ])
649 .build()?;
650 let table = catalog
651 .create_table(
652 &namespace,
653 TableCreation::builder()
654 .name("cow_rewrite_fixture".to_string())
655 .schema(schema)
656 .properties(HashMap::from([(
657 TableProperties::PROPERTY_WRITE_TARGET_FILE_SIZE_BYTES.to_string(),
658 "1".to_string(),
659 )]))
660 .build(),
661 )
662 .await?;
663
664 let arrow_schema = Arc::new(ArrowSchema::new(vec![
665 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
666 PARQUET_FIELD_ID_META_KEY.to_string(),
667 "1".to_string(),
668 )])),
669 ]));
670 let input = batches.into_iter().map(|ids| {
671 Ok(RecordBatch::try_new(arrow_schema.clone(), vec![
672 Arc::new(Int32Array::from(ids)) as ArrayRef,
673 ])?)
674 });
675 let data_files = super::writer::write_replacement_batches(
676 &table,
677 table.metadata().current_schema().clone(),
678 None,
679 futures::stream::iter(input),
680 )
681 .await?;
682
683 let tx = Transaction::new(&table);
684 let tx = tx.fast_append().add_data_files(data_files).apply(tx)?;
685 let table = tx.commit(&catalog).await?;
686
687 Ok(CowRewriteFixture {
688 _temp_dir: temp_dir,
689 table,
690 })
691 }
692
693 async fn test_table_with_id_value_rows(rows: Vec<(i32, i32)>) -> Result<CowRewriteFixture> {
694 let temp_dir = TempDir::new().unwrap();
695 let warehouse = format!("file://{}", temp_dir.path().join("warehouse").display());
696 let catalog = MemoryCatalogBuilder::default()
697 .with_storage_factory(Arc::new(LocalFsStorageFactory))
698 .load(
699 "memory",
700 HashMap::from([(MEMORY_CATALOG_WAREHOUSE.to_string(), warehouse)]),
701 )
702 .await?;
703 let namespace = NamespaceIdent::new("ns".to_string());
704 catalog.create_namespace(&namespace, HashMap::new()).await?;
705
706 let schema = Schema::builder()
707 .with_fields(vec![
708 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
709 NestedField::required(2, "value", Type::Primitive(PrimitiveType::Int)).into(),
710 ])
711 .build()?;
712 let table = catalog
713 .create_table(
714 &namespace,
715 TableCreation::builder()
716 .name("cow_rewrite_fixture".to_string())
717 .schema(schema)
718 .build(),
719 )
720 .await?;
721
722 let arrow_schema = Arc::new(ArrowSchema::new(vec![
723 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
724 PARQUET_FIELD_ID_META_KEY.to_string(),
725 "1".to_string(),
726 )])),
727 Field::new("value", DataType::Int32, false).with_metadata(HashMap::from([(
728 PARQUET_FIELD_ID_META_KEY.to_string(),
729 "2".to_string(),
730 )])),
731 ]));
732 let ids = rows.iter().map(|(id, _)| *id).collect::<Vec<_>>();
733 let values = rows.iter().map(|(_, value)| *value).collect::<Vec<_>>();
734 let batch = RecordBatch::try_new(arrow_schema, vec![
735 Arc::new(Int32Array::from(ids)) as ArrayRef,
736 Arc::new(Int32Array::from(values)) as ArrayRef,
737 ])?;
738 let data_files = super::writer::write_replacement_batches(
739 &table,
740 table.metadata().current_schema().clone(),
741 None,
742 futures::stream::iter(vec![Ok(batch)]),
743 )
744 .await?;
745
746 let tx = Transaction::new(&table);
747 let tx = tx.fast_append().add_data_files(data_files).apply(tx)?;
748 let table = tx.commit(&catalog).await?;
749
750 Ok(CowRewriteFixture {
751 _temp_dir: temp_dir,
752 table,
753 })
754 }
755
756 async fn read_ids(table: &Table, files: &[DataFile]) -> Result<Vec<i32>> {
757 let schema = table.metadata().current_schema().clone();
758 let project_field_ids = schema
759 .as_struct()
760 .fields()
761 .iter()
762 .map(|field| field.id)
763 .collect::<Vec<_>>();
764 let tasks = files
765 .iter()
766 .map(|data_file| {
767 FileScanTask::builder()
768 .with_file_size_in_bytes(data_file.file_size_in_bytes())
769 .with_start(0)
770 .with_length(data_file.file_size_in_bytes())
771 .with_record_count(Some(data_file.record_count()))
772 .with_data_file_path(data_file.file_path().to_string())
773 .with_data_file_format(data_file.file_format())
774 .with_schema(schema.clone())
775 .with_project_field_ids(project_field_ids.clone())
776 .with_case_sensitive(true)
777 .build()
778 })
779 .collect::<Vec<_>>();
780 let task_stream = Box::pin(futures::stream::iter(tasks)) as FileScanTaskStream;
781 let batches = table
782 .reader_builder()
783 .build()
784 .read(task_stream)?
785 .stream()
786 .try_collect::<Vec<_>>()
787 .await?;
788
789 let mut ids = Vec::new();
790 for batch in batches {
791 let column = batch
792 .column_by_name("id")
793 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing id column"))?
794 .as_any()
795 .downcast_ref::<Int32Array>()
796 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "id must be Int32"))?;
797 ids.extend((0..column.len()).map(|row| column.value(row)));
798 }
799 ids.sort_unstable();
800 Ok(ids)
801 }
802
803 async fn read_ids_and_values(table: &Table, files: &[DataFile]) -> Result<Vec<(i32, i32)>> {
804 let schema = table.metadata().current_schema().clone();
805 let project_field_ids = schema
806 .as_struct()
807 .fields()
808 .iter()
809 .map(|field| field.id)
810 .collect::<Vec<_>>();
811 let tasks = files
812 .iter()
813 .map(|data_file| {
814 FileScanTask::builder()
815 .with_file_size_in_bytes(data_file.file_size_in_bytes())
816 .with_start(0)
817 .with_length(data_file.file_size_in_bytes())
818 .with_record_count(Some(data_file.record_count()))
819 .with_data_file_path(data_file.file_path().to_string())
820 .with_data_file_format(data_file.file_format())
821 .with_schema(schema.clone())
822 .with_project_field_ids(project_field_ids.clone())
823 .with_case_sensitive(true)
824 .build()
825 })
826 .collect::<Vec<_>>();
827 let task_stream = Box::pin(futures::stream::iter(tasks)) as FileScanTaskStream;
828 let batches = table
829 .reader_builder()
830 .build()
831 .read(task_stream)?
832 .stream()
833 .try_collect::<Vec<_>>()
834 .await?;
835
836 let mut rows = Vec::new();
837 for batch in batches {
838 let ids = batch
839 .column_by_name("id")
840 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing id column"))?
841 .as_any()
842 .downcast_ref::<Int32Array>()
843 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "id must be Int32"))?;
844 let values = batch
845 .column_by_name("value")
846 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing value column"))?
847 .as_any()
848 .downcast_ref::<Int32Array>()
849 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "value must be Int32"))?;
850 rows.extend((0..ids.len()).map(|row| (ids.value(row), values.value(row))));
851 }
852 rows.sort_unstable_by_key(|(id, _)| *id);
853 Ok(rows)
854 }
855
856 #[tokio::test]
857 async fn cow_rewrite_keep_all_produces_no_changes() -> Result<()> {
858 let fixture = test_table_with_ids(vec![1, 2, 3]).await?;
859
860 let result = CowRewriteBuilder::new(&fixture.table)
861 .with_predicate(crate::expr::Predicate::AlwaysTrue)
862 .with_rewriter(Arc::new(KeepAll))
863 .rewrite()
864 .await?;
865
866 assert!(!result.has_changes());
867 assert_eq!(result.removed_data_files.len(), 0);
868 assert_eq!(result.added_data_files.len(), 0);
869 assert_eq!(result.unchanged_data_files.len(), 1);
870 assert_eq!(result.stats.candidate_files, 1);
871 assert_eq!(result.stats.unchanged_files, 1);
872 assert_eq!(result.stats.input_rows, 3);
873 assert_eq!(result.stats.output_rows, 0);
876
877 Ok(())
878 }
879
880 #[tokio::test]
881 async fn cow_rewrite_requires_rewriter() -> Result<()> {
882 let fixture = test_table_with_ids(vec![1]).await?;
883
884 let err = CowRewriteBuilder::new(&fixture.table)
885 .rewrite()
886 .await
887 .expect_err("missing rewriter should fail");
888
889 assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
890
891 Ok(())
892 }
893
894 #[tokio::test]
895 async fn cow_rewrite_delete_rows_removes_old_file_and_adds_replacement() -> Result<()> {
896 let fixture = test_table_with_ids(vec![1, 2, 3, 4]).await?;
897
898 let result = CowRewriteBuilder::new(&fixture.table)
899 .with_predicate(crate::expr::Predicate::AlwaysTrue)
900 .with_rewriter(Arc::new(DeleteEvenIds))
901 .rewrite()
902 .await?;
903
904 assert!(result.has_changes());
905 assert_eq!(result.removed_data_files.len(), 1);
906 assert_eq!(result.added_data_files.len(), 1);
907 assert_eq!(result.stats.input_rows, 4);
908 assert_eq!(result.stats.output_rows, 2);
909
910 let ids = read_ids(&fixture.table, &result.added_data_files).await?;
911 assert_eq!(ids, vec![1, 3]);
912
913 Ok(())
914 }
915
916 #[tokio::test]
917 async fn cow_rewrite_update_rows_rewrites_file_with_updated_values() -> Result<()> {
918 let fixture =
919 test_table_with_id_value_rows(vec![(1, 10), (2, 20), (3, 30), (4, 40)]).await?;
920
921 let result = CowRewriteBuilder::new(&fixture.table)
922 .with_predicate(crate::expr::Predicate::AlwaysTrue)
923 .with_rewriter(Arc::new(IncrementValueForEvenIds))
924 .rewrite()
925 .await?;
926
927 assert_eq!(result.removed_data_files.len(), 1);
928 assert_eq!(result.added_data_files.len(), 1);
929 assert_eq!(result.stats.input_rows, 4);
930 assert_eq!(result.stats.output_rows, 4);
931
932 let rows = read_ids_and_values(&fixture.table, &result.added_data_files).await?;
933 assert_eq!(rows, vec![(1, 10), (2, 30), (3, 30), (4, 50)]);
934
935 Ok(())
936 }
937
938 #[tokio::test]
939 async fn cow_rewrite_full_file_delete_removes_old_file_without_replacement() -> Result<()> {
940 let fixture = test_table_with_ids(vec![2, 4]).await?;
941
942 let result = CowRewriteBuilder::new(&fixture.table)
943 .with_predicate(crate::expr::Predicate::AlwaysTrue)
944 .with_rewriter(Arc::new(DeleteEvenIds))
945 .rewrite()
946 .await?;
947
948 assert!(result.has_changes());
949 assert_eq!(result.removed_data_files.len(), 1);
950 assert_eq!(result.added_data_files.len(), 0);
951 assert_eq!(result.stats.input_rows, 2);
952 assert_eq!(result.stats.output_rows, 0);
953
954 Ok(())
955 }
956
957 #[tokio::test]
958 async fn cow_rewrite_removes_file_fully_covered_by_position_deletes() -> Result<()> {
959 let mut fixture = TableTestFixture::new();
960 let positions = (0..300).collect::<Vec<i64>>();
961 fixture.setup_multi_row_group_manifest(&positions).await;
962
963 let result = CowRewriteBuilder::new(&fixture.table)
964 .with_predicate(crate::expr::Predicate::AlwaysTrue)
965 .with_rewriter(Arc::new(KeepAll))
966 .rewrite()
967 .await?;
968
969 assert_eq!(result.stats.candidate_files, 1);
970 assert_eq!(result.stats.rewritten_files, 1);
971 assert_eq!(result.stats.input_rows, 0);
972 assert_eq!(result.stats.output_rows, 0);
973 assert_eq!(result.removed_data_files.len(), 1);
974 assert!(result.added_data_files.is_empty());
975 assert!(result.unchanged_data_files.is_empty());
976
977 Ok(())
978 }
979
980 #[tokio::test]
981 async fn cow_rewrite_delete_no_matching_rows_keeps_old_file() -> Result<()> {
982 let fixture = test_table_with_ids(vec![1, 3]).await?;
983
984 let result = CowRewriteBuilder::new(&fixture.table)
985 .with_predicate(crate::expr::Predicate::AlwaysTrue)
986 .with_rewriter(Arc::new(DeleteEvenIds))
987 .rewrite()
988 .await?;
989
990 assert!(!result.has_changes());
991 assert_eq!(result.removed_data_files.len(), 0);
992 assert_eq!(result.added_data_files.len(), 0);
993 assert_eq!(result.unchanged_data_files.len(), 1);
994 assert_eq!(result.stats.input_rows, 2);
995 assert_eq!(result.stats.output_rows, 0);
996
997 Ok(())
998 }
999
1000 struct SilentFullDrop;
1005
1006 impl CowBatchRewriter for SilentFullDrop {
1007 fn rewrite_batch(&self, _batch: RecordBatch) -> Result<CowBatchRewrite> {
1008 Ok(CowBatchRewrite {
1009 output: None,
1010 changed: false,
1011 })
1012 }
1013 }
1014
1015 #[tokio::test]
1016 async fn cow_rewrite_none_output_implies_change() -> Result<()> {
1017 let fixture = test_table_with_ids(vec![1, 2]).await?;
1018
1019 let result = CowRewriteBuilder::new(&fixture.table)
1020 .with_predicate(crate::expr::Predicate::AlwaysTrue)
1021 .with_rewriter(Arc::new(SilentFullDrop))
1022 .rewrite()
1023 .await?;
1024
1025 assert!(result.has_changes());
1026 assert_eq!(result.removed_data_files.len(), 1);
1027 assert_eq!(result.added_data_files.len(), 0);
1028 assert!(result.unchanged_data_files.is_empty());
1029 assert_eq!(result.stats.rewritten_files, 1);
1030 assert_eq!(result.stats.input_rows, 2);
1031 assert_eq!(result.stats.output_rows, 0);
1032 assert_eq!(result.stats.changed_batches, 1);
1033
1034 Ok(())
1035 }
1036
1037 struct DropFirstBatchSilently {
1042 batches_seen: std::sync::atomic::AtomicUsize,
1043 }
1044
1045 impl CowBatchRewriter for DropFirstBatchSilently {
1046 fn rewrite_batch(&self, batch: RecordBatch) -> Result<CowBatchRewrite> {
1047 let seen = self
1048 .batches_seen
1049 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1050 if seen == 0 {
1051 Ok(CowBatchRewrite {
1052 output: None,
1053 changed: false,
1054 })
1055 } else {
1056 Ok(CowBatchRewrite {
1057 output: Some(batch),
1058 changed: true,
1059 })
1060 }
1061 }
1062 }
1063
1064 #[tokio::test]
1065 async fn cow_rewrite_silent_drop_before_change_loses_no_rows() -> Result<()> {
1066 let fixture = test_table_with_ids(vec![1, 2, 3, 4]).await?;
1067
1068 let result = CowRewriteBuilder::new(&fixture.table)
1069 .with_predicate(crate::expr::Predicate::AlwaysTrue)
1070 .with_batch_size(2)
1071 .with_rewriter(Arc::new(DropFirstBatchSilently {
1072 batches_seen: std::sync::atomic::AtomicUsize::new(0),
1073 }))
1074 .rewrite()
1075 .await?;
1076
1077 assert!(result.has_changes());
1078 assert_eq!(result.removed_data_files.len(), 1);
1079 assert_eq!(result.added_data_files.len(), 1);
1080 assert_eq!(result.stats.input_rows, 4);
1081 assert_eq!(result.stats.output_rows, 2);
1082
1083 let ids = read_ids(&fixture.table, &result.added_data_files).await?;
1084 assert_eq!(ids, vec![3, 4]);
1085
1086 Ok(())
1087 }
1088
1089 #[tokio::test]
1090 async fn cow_rewrite_kept_prefix_survives_later_full_batch_delete() -> Result<()> {
1091 let fixture = test_table_with_ids(vec![1, 3, 2, 4]).await?;
1092
1093 let result = CowRewriteBuilder::new(&fixture.table)
1094 .with_predicate(crate::expr::Predicate::AlwaysTrue)
1095 .with_batch_size(2)
1096 .with_rewriter(Arc::new(DeleteEvenIds))
1097 .rewrite()
1098 .await?;
1099
1100 assert_eq!(result.removed_data_files.len(), 1);
1101 assert_eq!(result.stats.input_rows, 4);
1102 assert_eq!(result.stats.output_rows, 2);
1103 assert_eq!(result.added_data_files.len(), 1);
1104 assert!(result.unchanged_data_files.is_empty());
1105 assert_eq!(
1106 read_ids(&fixture.table, &result.added_data_files).await?,
1107 vec![1, 3]
1108 );
1109
1110 Ok(())
1111 }
1112
1113 #[tokio::test]
1114 async fn cow_rewrite_uses_unique_replacement_paths_for_multiple_source_files() -> Result<()> {
1115 let fixture = test_table_with_id_batches(vec![vec![1, 2], vec![3, 4]]).await?;
1116
1117 let result = CowRewriteBuilder::new(&fixture.table)
1118 .with_predicate(crate::expr::Predicate::AlwaysTrue)
1119 .with_rewriter(Arc::new(DeleteEvenIds))
1120 .rewrite()
1121 .await?;
1122
1123 assert_eq!(result.stats.candidate_files, 2);
1124 assert_eq!(result.removed_data_files.len(), 2);
1125 assert_eq!(result.added_data_files.len(), 2);
1126
1127 let added_paths = result
1128 .added_data_files
1129 .iter()
1130 .map(|file| file.file_path().to_string())
1131 .collect::<HashSet<_>>();
1132 let removed_paths = result
1133 .removed_data_files
1134 .iter()
1135 .map(|file| file.file_path().to_string())
1136 .collect::<HashSet<_>>();
1137
1138 assert_eq!(added_paths.len(), result.added_data_files.len());
1139 assert!(added_paths.is_disjoint(&removed_paths));
1140
1141 let ids = read_ids(&fixture.table, &result.added_data_files).await?;
1142 assert_eq!(ids, vec![1, 3]);
1143
1144 Ok(())
1145 }
1146
1147 #[test]
1148 fn cow_batch_rewriter_is_object_safe() {
1149 let _rewriter: Arc<dyn CowBatchRewriter> = Arc::new(KeepAll);
1150 }
1151
1152 #[test]
1153 fn cow_rewrite_result_reports_no_changes() {
1154 let result = crate::cow_rewrite::CowRewriteResult {
1155 removed_data_files: vec![],
1156 added_data_files: vec![],
1157 unchanged_data_files: vec![],
1158 stats: crate::cow_rewrite::CowRewriteStats {
1159 candidate_files: 0,
1160 rewritten_files: 0,
1161 unchanged_files: 0,
1162 input_rows: 0,
1163 output_rows: 0,
1164 changed_batches: 0,
1165 },
1166 };
1167
1168 assert!(!result.has_changes());
1169 assert_eq!(result.stats.candidate_files, 0);
1170 }
1171
1172 struct DeleteIfModThree;
1174
1175 impl CowBatchRewriter for DeleteIfModThree {
1176 fn rewrite_batch(&self, batch: RecordBatch) -> Result<CowBatchRewrite> {
1177 let ids = batch
1178 .column_by_name("id")
1179 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing id column"))?
1180 .as_any()
1181 .downcast_ref::<Int32Array>()
1182 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "id must be Int32"))?;
1183
1184 let keep =
1185 BooleanArray::from_iter((0..ids.len()).map(|row| Some(ids.value(row) % 3 != 0)));
1186 let filtered = arrow_select::filter::filter_record_batch(&batch, &keep)
1187 .map_err(|err| Error::new(ErrorKind::Unexpected, err.to_string()))?;
1188
1189 Ok(CowBatchRewrite {
1190 changed: filtered.num_rows() != batch.num_rows(),
1191 output: (filtered.num_rows() > 0).then_some(filtered),
1192 })
1193 }
1194 }
1195
1196 #[tokio::test]
1202 async fn cow_rewrite_after_optional_column_add() -> Result<()> {
1203 let temp_dir = TempDir::new().unwrap();
1204 let warehouse = format!("file://{}", temp_dir.path().join("warehouse").display());
1205 let catalog: Arc<dyn Catalog> = Arc::new(
1206 MemoryCatalogBuilder::default()
1207 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1208 .load(
1209 "memory",
1210 HashMap::from([(MEMORY_CATALOG_WAREHOUSE.to_string(), warehouse)]),
1211 )
1212 .await?,
1213 );
1214 let namespace = NamespaceIdent::new("ns".to_string());
1215 catalog.create_namespace(&namespace, HashMap::new()).await?;
1216
1217 let schema = Schema::builder()
1218 .with_fields(vec![
1219 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1220 ])
1221 .build()?;
1222 let table = catalog
1223 .create_table(
1224 &namespace,
1225 TableCreation::builder()
1226 .name("evolved".to_string())
1227 .schema(schema)
1228 .build(),
1229 )
1230 .await?;
1231
1232 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1234 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
1235 PARQUET_FIELD_ID_META_KEY.to_string(),
1236 "1".to_string(),
1237 )])),
1238 ]));
1239 let batch = RecordBatch::try_new(arrow_schema, vec![Arc::new(Int32Array::from(vec![
1240 1, 2, 3, 4,
1241 ])) as ArrayRef])?;
1242 let data_files = super::writer::write_replacement_batches(
1243 &table,
1244 table.metadata().current_schema().clone(),
1245 None,
1246 futures::stream::iter(vec![Ok(batch)]),
1247 )
1248 .await?;
1249 let tx = Transaction::new(&table);
1250 let tx = tx.fast_append().add_data_files(data_files).apply(tx)?;
1251 let table = tx.commit(&*catalog).await?;
1252
1253 let tx = Transaction::new(&table);
1255 let tx = tx
1256 .update_schema()
1257 .add_column(AddColumn::optional(
1258 "value",
1259 Type::Primitive(PrimitiveType::Int),
1260 ))
1261 .apply(tx)?;
1262 let table = tx.commit(&*catalog).await?;
1263
1264 let current_snapshot = table.metadata().current_snapshot().unwrap();
1267 assert_ne!(
1268 table.metadata().current_schema_id(),
1269 current_snapshot.schema_id().unwrap()
1270 );
1271
1272 let result = CowRewriteBuilder::new(&table)
1275 .with_predicate(crate::expr::Predicate::AlwaysTrue)
1276 .with_rewriter(Arc::new(DeleteIfModThree))
1277 .rewrite()
1278 .await?;
1279
1280 assert!(result.has_changes());
1282 assert_eq!(result.removed_data_files.len(), 1);
1283 assert_eq!(result.added_data_files.len(), 1);
1284
1285 let ids = read_ids(&table, &result.added_data_files).await?;
1286 assert_eq!(ids, vec![1, 2, 4]);
1287
1288 Ok(())
1289 }
1290
1291 async fn two_file_table_disjoint_ids() -> Result<CowRewriteFixture> {
1294 let temp_dir = TempDir::new().unwrap();
1295 let warehouse = format!("file://{}", temp_dir.path().join("warehouse").display());
1296 let catalog = MemoryCatalogBuilder::default()
1297 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1298 .load(
1299 "memory",
1300 HashMap::from([(MEMORY_CATALOG_WAREHOUSE.to_string(), warehouse)]),
1301 )
1302 .await?;
1303 let namespace = NamespaceIdent::new("ns".to_string());
1304 catalog.create_namespace(&namespace, HashMap::new()).await?;
1305
1306 let schema = Schema::builder()
1307 .with_fields(vec![
1308 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1309 ])
1310 .build()?;
1311 let table = catalog
1312 .create_table(
1313 &namespace,
1314 TableCreation::builder()
1315 .name("disjoint".to_string())
1316 .schema(schema)
1317 .properties(HashMap::from([(
1318 TableProperties::PROPERTY_WRITE_TARGET_FILE_SIZE_BYTES.to_string(),
1319 "1".to_string(),
1320 )]))
1321 .build(),
1322 )
1323 .await?;
1324
1325 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1326 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
1327 PARQUET_FIELD_ID_META_KEY.to_string(),
1328 "1".to_string(),
1329 )])),
1330 ]));
1331 let input = vec![vec![1, 2, 3, 4], vec![100, 101, 102, 103]]
1334 .into_iter()
1335 .map(|ids| {
1336 Ok(RecordBatch::try_new(arrow_schema.clone(), vec![
1337 Arc::new(Int32Array::from(ids)) as ArrayRef,
1338 ])?)
1339 });
1340 let data_files = super::writer::write_replacement_batches(
1341 &table,
1342 table.metadata().current_schema().clone(),
1343 None,
1344 futures::stream::iter(input),
1345 )
1346 .await?;
1347
1348 let tx = Transaction::new(&table);
1349 let tx = tx.fast_append().add_data_files(data_files).apply(tx)?;
1350 let table = tx.commit(&catalog).await?;
1351
1352 Ok(CowRewriteFixture {
1353 _temp_dir: temp_dir,
1354 table,
1355 })
1356 }
1357
1358 #[tokio::test]
1362 async fn cow_rewrite_predicate_prunes_non_matching_files() -> Result<()> {
1363 let fixture = two_file_table_disjoint_ids().await?;
1364
1365 let predicate = crate::expr::Reference::new("id").greater_than(crate::spec::Datum::int(50));
1366
1367 let result = CowRewriteBuilder::new(&fixture.table)
1368 .with_predicate(predicate)
1369 .with_rewriter(Arc::new(KeepAll))
1370 .rewrite()
1371 .await?;
1372
1373 assert_eq!(result.stats.candidate_files, 1);
1375 assert_eq!(result.removed_data_files.len(), 0);
1377 assert_eq!(result.added_data_files.len(), 0);
1378 assert_eq!(result.unchanged_data_files.len(), 1);
1379
1380 let ids = read_ids(&fixture.table, &result.unchanged_data_files).await?;
1382 assert_eq!(ids, vec![100, 101, 102, 103]);
1383
1384 Ok(())
1385 }
1386
1387 struct TouchEveryBatch;
1390
1391 impl CowBatchRewriter for TouchEveryBatch {
1392 fn rewrite_batch(&self, batch: RecordBatch) -> Result<CowBatchRewrite> {
1393 Ok(CowBatchRewrite {
1394 output: Some(batch),
1395 changed: true,
1396 })
1397 }
1398 }
1399
1400 #[tokio::test]
1405 async fn cow_rewrite_preserves_partitions_end_to_end() -> Result<()> {
1406 let temp_dir = TempDir::new().unwrap();
1407 let warehouse = format!("file://{}", temp_dir.path().join("warehouse").display());
1408 let catalog = MemoryCatalogBuilder::default()
1409 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1410 .load(
1411 "memory",
1412 HashMap::from([(MEMORY_CATALOG_WAREHOUSE.to_string(), warehouse)]),
1413 )
1414 .await?;
1415 let namespace = NamespaceIdent::new("ns".to_string());
1416 catalog.create_namespace(&namespace, HashMap::new()).await?;
1417
1418 let schema = Schema::builder()
1419 .with_fields(vec![
1420 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1421 NestedField::optional(2, "value", Type::Primitive(PrimitiveType::Int)).into(),
1422 ])
1423 .build()?;
1424 let partition_spec = crate::spec::PartitionSpec::builder(Arc::new(schema.clone()))
1425 .add_partition_field("value", "value", Transform::Identity)?
1426 .build()?;
1427 let table = catalog
1428 .create_table(
1429 &namespace,
1430 TableCreation::builder()
1431 .name("partitioned".to_string())
1432 .schema(schema)
1433 .partition_spec(partition_spec.into_unbound())
1434 .build(),
1435 )
1436 .await?;
1437
1438 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1441 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
1442 PARQUET_FIELD_ID_META_KEY.to_string(),
1443 "1".to_string(),
1444 )])),
1445 Field::new("value", DataType::Int32, true).with_metadata(HashMap::from([(
1446 PARQUET_FIELD_ID_META_KEY.to_string(),
1447 "2".to_string(),
1448 )])),
1449 ]));
1450 let current_schema = table.metadata().current_schema().clone();
1451 let default_spec = table.metadata().default_partition_spec().as_ref().clone();
1452
1453 let batch_one = RecordBatch::try_new(arrow_schema.clone(), vec![
1454 Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef,
1455 Arc::new(Int32Array::from(vec![1, 1])),
1456 ])?;
1457 let one_partition = crate::spec::PartitionKey::new(
1458 default_spec.clone(),
1459 current_schema.clone(),
1460 Struct::from_iter([Some(Literal::int(1))]),
1461 );
1462 let files_one = super::writer::write_replacement_batches(
1463 &table,
1464 current_schema.clone(),
1465 Some(one_partition),
1466 futures::stream::iter(vec![Ok(batch_one)]),
1467 )
1468 .await?;
1469
1470 let batch_null = RecordBatch::try_new(arrow_schema.clone(), vec![
1471 Arc::new(Int32Array::from(vec![3, 4])) as ArrayRef,
1472 Arc::new(Int32Array::from(vec![None::<i32>, None])),
1473 ])?;
1474 let null_partition = crate::spec::PartitionKey::new(
1475 default_spec.clone(),
1476 current_schema.clone(),
1477 Struct::from_iter([None::<Literal>]),
1478 );
1479 let files_null = super::writer::write_replacement_batches(
1480 &table,
1481 current_schema.clone(),
1482 Some(null_partition),
1483 futures::stream::iter(vec![Ok(batch_null)]),
1484 )
1485 .await?;
1486
1487 let tx = Transaction::new(&table);
1488 let tx = tx
1489 .fast_append()
1490 .add_data_files([files_one, files_null].concat())
1491 .apply(tx)?;
1492 let table = tx.commit(&catalog).await?;
1493
1494 let result = CowRewriteBuilder::new(&table)
1495 .with_rewriter(Arc::new(TouchEveryBatch))
1496 .rewrite()
1497 .await?;
1498
1499 assert_eq!(result.stats.candidate_files, 2);
1500 assert_eq!(result.stats.rewritten_files, 2);
1501 assert_eq!(result.removed_data_files.len(), 2);
1502 assert_eq!(result.added_data_files.len(), 2);
1503 assert!(result.unchanged_data_files.is_empty());
1504
1505 let one_struct = Struct::from_iter([Some(Literal::int(1))]);
1507 let null_struct = Struct::from_iter([None::<Literal>]);
1508 let mut removed_partitions = result
1509 .removed_data_files
1510 .iter()
1511 .map(|file| file.partition().clone())
1512 .collect::<Vec<_>>();
1513 let mut added_partitions = result
1514 .added_data_files
1515 .iter()
1516 .map(|file| file.partition().clone())
1517 .collect::<Vec<_>>();
1518 removed_partitions.sort_by_key(|partition| partition.is_null_at_index(0));
1519 added_partitions.sort_by_key(|partition| partition.is_null_at_index(0));
1520 assert_eq!(removed_partitions, added_partitions);
1521 assert_eq!(added_partitions[0], one_struct);
1522 assert_eq!(added_partitions[1], null_struct);
1523
1524 for file in &result.added_data_files {
1525 assert_eq!(file.partition_spec_id, 0);
1526 if file.partition() == &one_struct {
1527 assert!(file.file_path().contains("value=1"), "{}", file.file_path());
1528 } else {
1529 assert!(
1530 file.file_path().contains("value=null"),
1531 "{}",
1532 file.file_path()
1533 );
1534 }
1535 }
1536
1537 let ids = read_ids(&table, &result.added_data_files).await?;
1539 assert_eq!(ids, vec![1, 2, 3, 4]);
1540
1541 Ok(())
1542 }
1543
1544 #[cfg(debug_assertions)]
1549 struct MutatePartitionColumn;
1550
1551 #[cfg(debug_assertions)]
1552 impl CowBatchRewriter for MutatePartitionColumn {
1553 fn rewrite_batch(&self, batch: RecordBatch) -> Result<CowBatchRewrite> {
1554 let values = batch
1555 .column_by_name("value")
1556 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "missing value column"))?
1557 .as_any()
1558 .downcast_ref::<Int32Array>()
1559 .ok_or_else(|| Error::new(ErrorKind::DataInvalid, "value must be Int32"))?;
1560 let updated =
1561 Int32Array::from_iter((0..values.len()).map(|row| Some(values.value(row) + 100)));
1562 let output = RecordBatch::try_new(batch.schema(), vec![
1563 batch.column(0).clone(),
1564 Arc::new(updated),
1565 ])
1566 .map_err(|err| Error::new(ErrorKind::Unexpected, err.to_string()))?;
1567
1568 Ok(CowBatchRewrite {
1569 output: Some(output),
1570 changed: true,
1571 })
1572 }
1573 }
1574
1575 #[cfg(debug_assertions)]
1576 #[tokio::test]
1577 #[should_panic(expected = "must preserve the source file's partition values")]
1578 async fn cow_rewrite_partition_column_mutation_trips_debug_guard() {
1579 let temp_dir = TempDir::new().unwrap();
1580 let warehouse = format!("file://{}", temp_dir.path().join("warehouse").display());
1581 let catalog = MemoryCatalogBuilder::default()
1582 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1583 .load(
1584 "memory",
1585 HashMap::from([(MEMORY_CATALOG_WAREHOUSE.to_string(), warehouse)]),
1586 )
1587 .await
1588 .unwrap();
1589 let namespace = NamespaceIdent::new("ns".to_string());
1590 catalog
1591 .create_namespace(&namespace, HashMap::new())
1592 .await
1593 .unwrap();
1594
1595 let schema = Schema::builder()
1596 .with_fields(vec![
1597 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1598 NestedField::required(2, "value", Type::Primitive(PrimitiveType::Int)).into(),
1599 ])
1600 .build()
1601 .unwrap();
1602 let partition_spec = crate::spec::PartitionSpec::builder(Arc::new(schema.clone()))
1603 .add_partition_field("value", "value", Transform::Identity)
1604 .unwrap()
1605 .build()
1606 .unwrap();
1607 let table = catalog
1608 .create_table(
1609 &namespace,
1610 TableCreation::builder()
1611 .name("partition_guard".to_string())
1612 .schema(schema)
1613 .partition_spec(partition_spec.into_unbound())
1614 .build(),
1615 )
1616 .await
1617 .unwrap();
1618
1619 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1621 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
1622 PARQUET_FIELD_ID_META_KEY.to_string(),
1623 "1".to_string(),
1624 )])),
1625 Field::new("value", DataType::Int32, false).with_metadata(HashMap::from([(
1626 PARQUET_FIELD_ID_META_KEY.to_string(),
1627 "2".to_string(),
1628 )])),
1629 ]));
1630 let batch = RecordBatch::try_new(arrow_schema, vec![
1631 Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef,
1632 Arc::new(Int32Array::from(vec![1, 1])),
1633 ])
1634 .unwrap();
1635 let partition_key = crate::spec::PartitionKey::new(
1636 table.metadata().default_partition_spec().as_ref().clone(),
1637 table.metadata().current_schema().clone(),
1638 Struct::from_iter([Some(Literal::int(1))]),
1639 );
1640 let data_files = super::writer::write_replacement_batches(
1641 &table,
1642 table.metadata().current_schema().clone(),
1643 Some(partition_key),
1644 futures::stream::iter(vec![Ok(batch)]),
1645 )
1646 .await
1647 .unwrap();
1648
1649 let tx = Transaction::new(&table);
1650 let tx = tx
1651 .fast_append()
1652 .add_data_files(data_files)
1653 .apply(tx)
1654 .unwrap();
1655 let table = tx.commit(&catalog).await.unwrap();
1656
1657 let _ = CowRewriteBuilder::new(&table)
1663 .with_rewriter(Arc::new(MutatePartitionColumn))
1664 .rewrite()
1665 .await;
1666 }
1667}