risingwave_connector/sink/iceberg/
position_delete.rs1use std::collections::HashMap;
18use std::sync::Arc;
19
20use anyhow::{Context, Result, anyhow, bail};
21use futures::StreamExt;
22use iceberg::arrow::schema_to_arrow_schema;
23use iceberg::delete_vector::DeleteVector;
24use iceberg::io::FileIO;
25use iceberg::puffin::{CompressionCodec, PuffinReader, PuffinWriter};
26use iceberg::spec::{DataContentType, DataFile, DataFileBuilder, DataFileFormat, PartitionKey};
27use iceberg::table::Table;
28use iceberg::writer::base_writer::position_delete_file_writer::POSITION_DELETE_SCHEMA;
29use iceberg::writer::file_writer::location_generator::{
30 DefaultFileNameGenerator, DefaultLocationGenerator, FileNameGenerator, LocationGenerator,
31};
32use iceberg::writer::file_writer::{
33 FileWriter, FileWriterBuilder, ParquetWriter, ParquetWriterBuilder,
34};
35use parquet::arrow::{ParquetRecordBatchStreamBuilder, ProjectionMask};
36use parquet::file::properties::WriterProperties;
37use risingwave_common::array::arrow::arrow_array_iceberg::{
38 Array, ArrayRef, Int64Array, RecordBatch, StringArray,
39};
40use risingwave_common::array::arrow::arrow_schema_iceberg::SchemaRef as ArrowSchemaRef;
41
42use crate::sink::iceberg::{IcebergConfig, PARQUET_CREATED_BY};
43use crate::source::iceberg::parquet_file_handler::ParquetFileReader;
44
45const DELETION_VECTOR_PROPERTY_CARDINALITY: &str = "cardinality";
47const DELETION_VECTOR_PROPERTY_REFERENCED_DATA_FILE: &str = "referenced-data-file";
49
50pub async fn read_dv_positions_from_data_file(
52 file_io: &FileIO,
53 data_file: &DataFile,
54) -> Result<DeleteVector> {
55 let blob_offset = data_file.content_offset().with_context(|| {
56 format!(
57 "DV file {} missing content_offset for referenced data file {:?}",
58 data_file.file_path(),
59 data_file.referenced_data_file()
60 )
61 })?;
62 let blob_length = data_file.content_size_in_bytes().with_context(|| {
63 format!(
64 "DV file {} missing content_size_in_bytes for referenced data file {:?}",
65 data_file.file_path(),
66 data_file.referenced_data_file()
67 )
68 })?;
69
70 let input_file = file_io.new_input(data_file.file_path())?;
71 let puffin_reader = PuffinReader::new(input_file);
72 let file_metadata = puffin_reader.file_metadata().await?;
73 let blob_metadata = file_metadata
74 .blobs()
75 .iter()
76 .find(|blob| blob.offset() == blob_offset as u64 && blob.length() == blob_length as u64)
77 .with_context(|| {
78 format!(
79 "DV blob metadata not found in {} at offset={} length={}",
80 data_file.file_path(),
81 blob_offset,
82 blob_length
83 )
84 })?;
85 let blob = puffin_reader.blob(blob_metadata).await?;
86
87 let delete_vector = DeleteVector::from_puffin_blob(blob)?;
88 Ok(delete_vector)
89}
90
91pub async fn read_parquet_position_deletes_from_file(
98 file_io: &FileIO,
99 delete_file: &DataFile,
100) -> Result<DeleteVector> {
101 let input_file = file_io.new_input(delete_file.file_path())?;
102 let metadata = input_file.metadata().await?;
103 let reader = input_file.reader().await?;
104 let parquet_reader = ParquetFileReader::new(metadata, reader);
105 let builder = ParquetRecordBatchStreamBuilder::new(parquet_reader).await?;
106 let projection = ProjectionMask::leaves(builder.parquet_schema(), [1]);
108 let mut stream = builder.with_projection(projection).build()?;
109
110 let mut delete_vector = DeleteVector::default();
111 while let Some(batch) = stream.next().await {
112 let batch = batch?;
113 let positions = batch.columns()[0]
115 .as_any()
116 .downcast_ref::<Int64Array>()
117 .context("position-delete pos column should be an Int64Array")?;
118 for pos in positions {
119 let pos = pos.with_context(|| {
120 format!(
121 "null value in position-delete file {}",
122 delete_file.file_path()
123 )
124 })?;
125 delete_vector.insert(pos as u64);
126 }
127 }
128
129 Ok(delete_vector)
130}
131
132pub async fn read_position_deletes_from_file(
134 file_io: &FileIO,
135 delete_file: &DataFile,
136) -> Result<DeleteVector> {
137 match delete_file.file_format() {
138 DataFileFormat::Puffin => read_dv_positions_from_data_file(file_io, delete_file).await,
139 DataFileFormat::Parquet => {
140 read_parquet_position_deletes_from_file(file_io, delete_file).await
141 }
142 other => bail!(
143 "position-delete file {} has unsupported format {:?}; expected Puffin or Parquet",
144 delete_file.file_path(),
145 other
146 ),
147 }
148}
149
150pub async fn write_dv_puffin_file(
154 table: &Table,
155 location_generator: &DefaultLocationGenerator,
156 file_name_generator: &DefaultFileNameGenerator,
157 data_file_path: String,
158 delete_vector: &DeleteVector,
159 partition_key: Option<&PartitionKey>,
160) -> Result<DataFile> {
161 let file_name = file_name_generator.generate_file_name();
162 let location = location_generator.generate_location(partition_key, &file_name);
163 let output_file = table.file_io().new_output(&location)?;
164 let mut writer = PuffinWriter::new(&output_file, HashMap::new(), false).await?;
165
166 let cardinality = delete_vector.len();
167 let properties = HashMap::from([
168 (
169 DELETION_VECTOR_PROPERTY_CARDINALITY.to_owned(),
170 cardinality.to_string(),
171 ),
172 (
173 DELETION_VECTOR_PROPERTY_REFERENCED_DATA_FILE.to_owned(),
174 data_file_path.clone(),
175 ),
176 ]);
177 let blob = delete_vector.to_puffin_blob(properties)?;
178 writer.add(blob, CompressionCodec::None).await?;
179
180 let result = writer.close_with_metadata().await?;
181 let blob_metadata = result
182 .blobs_metadata
183 .first()
184 .context("blob metadata should be present")?;
185
186 let mut builder = DataFileBuilder::default();
187 builder
188 .content(DataContentType::PositionDeletes)
189 .file_path(location)
190 .file_format(DataFileFormat::Puffin)
191 .record_count(cardinality)
192 .file_size_in_bytes(result.file_size_in_bytes)
193 .referenced_data_file(Some(data_file_path))
194 .content_offset(Some(blob_metadata.offset() as i64))
195 .content_size_in_bytes(Some(blob_metadata.length() as i64));
196 if let Some(partition_key) = partition_key {
197 builder
198 .partition(partition_key.data().clone())
199 .partition_spec_id(partition_key.spec().spec_id());
200 }
201 builder
202 .build()
203 .context("Failed to build deletion vector file metadata")
204}
205
206const POSITION_DELETE_WRITE_CHUNK_SIZE: usize = 1024;
208
209pub async fn write_parquet_position_delete_file(
213 table: &Table,
214 location_generator: &DefaultLocationGenerator,
215 file_name_generator: &DefaultFileNameGenerator,
216 config: &IcebergConfig,
217 data_file_path: String,
218 delete_vector: &DeleteVector,
219 partition_key: Option<&PartitionKey>,
220) -> Result<DataFile> {
221 let file_name = file_name_generator.generate_file_name();
222 let location = location_generator.generate_location(partition_key, &file_name);
223 let output_file = table.file_io().new_output(&location)?;
224
225 let parquet_writer_properties = WriterProperties::builder()
226 .set_compression(config.get_parquet_compression())
227 .set_max_row_group_bytes(config.write_parquet_max_row_group_bytes())
228 .set_created_by(PARQUET_CREATED_BY.to_owned())
229 .build();
230 let mut writer = ParquetWriterBuilder::new(
231 parquet_writer_properties,
232 POSITION_DELETE_SCHEMA.clone().into(),
233 )
234 .build(output_file)
235 .await?;
236
237 let arrow_schema: ArrowSchemaRef = Arc::new(schema_to_arrow_schema(&POSITION_DELETE_SCHEMA)?);
240
241 let mut positions: Vec<i64> = Vec::with_capacity(POSITION_DELETE_WRITE_CHUNK_SIZE);
242 for pos in delete_vector.iter() {
243 positions.push(pos as i64);
244 if positions.len() == POSITION_DELETE_WRITE_CHUNK_SIZE {
245 write_position_delete_chunk(
246 &mut writer,
247 &arrow_schema,
248 &data_file_path,
249 std::mem::take(&mut positions),
250 )
251 .await?;
252 positions.reserve(POSITION_DELETE_WRITE_CHUNK_SIZE);
253 }
254 }
255 if !positions.is_empty() {
256 write_position_delete_chunk(&mut writer, &arrow_schema, &data_file_path, positions).await?;
257 }
258
259 let data_files = writer.close().await?;
260 let [mut builder] = data_files.try_into().map_err(|_| {
262 anyhow!("position-delete writer produced invalid file count for {data_file_path}")
263 })?;
264
265 builder
268 .content(DataContentType::PositionDeletes)
269 .referenced_data_file(Some(data_file_path));
270 if let Some(partition_key) = partition_key {
271 builder
272 .partition(partition_key.data().clone())
273 .partition_spec_id(partition_key.spec().spec_id());
274 }
275 builder
276 .build()
277 .context("Failed to build position-delete file metadata")
278}
279
280async fn write_position_delete_chunk(
283 writer: &mut ParquetWriter,
284 arrow_schema: &ArrowSchemaRef,
285 data_file_path: &str,
286 positions: Vec<i64>,
287) -> Result<()> {
288 let path_column: ArrayRef = Arc::new(StringArray::from_iter_values(std::iter::repeat_n(
289 data_file_path,
290 positions.len(),
291 )));
292 let pos_column: ArrayRef = Arc::new(Int64Array::from(positions));
293 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![path_column, pos_column])
294 .map_err(|e| anyhow!(e))?;
295 writer.write(&batch).await?;
296 Ok(())
297}