1use std::collections::HashMap;
18use std::future::Future;
19use std::mem;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::task::{Context, Poll};
24use std::time::Instant;
25
26use bytes::Bytes;
27use common_telemetry::debug;
28use common_time::Timestamp;
29use datatypes::arrow::array::{
30 ArrayRef, BinaryArray, TimestampMicrosecondArray, TimestampMillisecondArray,
31 TimestampNanosecondArray, TimestampSecondArray, UInt32Array,
32};
33use datatypes::arrow::compute::{max, min};
34use datatypes::arrow::datatypes::{DataType, SchemaRef, TimeUnit};
35use datatypes::arrow::record_batch::RecordBatch;
36use datatypes::extension::json::is_json2_extension_type;
37use object_store::{FuturesAsyncWriter, ObjectStore};
38use parquet::arrow::AsyncArrowWriter;
39use parquet::basic::{Compression, Encoding, ZstdLevel};
40use parquet::file::metadata::KeyValue;
41use parquet::file::properties::WriterProperties;
42use smallvec::smallvec;
43use snafu::{OptionExt, ResultExt};
44use store_api::metadata::RegionMetadataRef;
45use store_api::storage::{FileId, SequenceNumber};
46use tokio::io::AsyncWrite;
47use tokio_util::compat::{Compat, FuturesAsyncWriteCompatExt};
48
49use crate::access_layer::{FilePathProvider, Metrics, SstInfoArray, TempFileCleaner};
50use crate::config::{IndexBuildMode, IndexConfig};
51use crate::error::{
52 InvalidMetadataSnafu, InvalidRecordBatchSnafu, OpenDalSnafu, Result, UnexpectedSnafu,
53 WriteParquetSnafu,
54};
55use crate::read::FlatSource;
56use crate::sst::file::RegionFileId;
57use crate::sst::index::{IndexOutput, Indexer, IndexerBuilder};
58use crate::sst::parquet::flat_format::{
59 FlatWriteFormat, primary_key_column_index, time_index_column_index,
60};
61use crate::sst::parquet::format::{PrimaryKeyArray, PrimaryKeyWriteFormat};
62use crate::sst::parquet::{
63 COLUMN_INDEX_TRUNCATE_LENGTH, PARQUET_METADATA_KEY, SstInfo, WriteOptions,
64 apply_column_compression, apply_float_field_encoding, apply_internal_column_encoding,
65};
66use crate::sst::{
67 DEFAULT_WRITE_CONCURRENCY, FlatSchemaOptions, SeriesEstimator, maybe_wrap_schema,
68};
69
70enum FlatBatchConverter {
72 Flat(FlatWriteFormat),
74 PrimaryKey {
76 format: PrimaryKeyWriteFormat,
77 num_fields: usize,
78 },
79}
80
81impl FlatBatchConverter {
82 fn convert_batch(&self, batch: &RecordBatch) -> Result<RecordBatch> {
83 match self {
84 FlatBatchConverter::Flat(f) => f.convert_batch(batch),
85 FlatBatchConverter::PrimaryKey { format, num_fields } => {
86 format.convert_flat_batch(batch, *num_fields)
87 }
88 }
89 }
90}
91
92enum SeriesBoundarySplit {
94 Continue(RecordBatch),
96 Split {
98 current_file_tail: Option<RecordBatch>,
100 next_file_head: RecordBatch,
102 },
103}
104
105fn encoded_primary_keys(batch: &RecordBatch) -> Result<(&UInt32Array, &BinaryArray)> {
107 let column = batch.column(primary_key_column_index(batch.num_columns()));
108 let primary_keys = column
109 .as_any()
110 .downcast_ref::<PrimaryKeyArray>()
111 .with_context(|| InvalidRecordBatchSnafu {
112 reason: format!(
113 "expected dictionary primary key column, got {:?}",
114 column.data_type()
115 ),
116 })?;
117 let values = primary_keys
118 .values()
119 .as_any()
120 .downcast_ref::<BinaryArray>()
121 .with_context(|| InvalidRecordBatchSnafu {
122 reason: format!(
123 "expected binary primary key values, got {:?}",
124 primary_keys.values().data_type()
125 ),
126 })?;
127 Ok((primary_keys.keys(), values))
128}
129
130fn split_at_next_series(
139 batch: RecordBatch,
140 current_primary_key: &[u8],
141) -> Result<SeriesBoundarySplit> {
142 let (keys, values) = encoded_primary_keys(&batch)?;
143 let Some(offset) = (0..batch.num_rows())
144 .find(|&row| values.value(keys.value(row) as usize) != current_primary_key)
145 else {
146 return Ok(SeriesBoundarySplit::Continue(batch));
147 };
148
149 let next_file_head = batch.slice(offset, batch.num_rows() - offset);
150 let current_file_tail = (offset > 0).then(|| batch.slice(0, offset));
151 Ok(SeriesBoundarySplit::Split {
152 current_file_tail,
153 next_file_head,
154 })
155}
156
157pub struct ParquetWriter<'a, F: WriterFactory, I: IndexerBuilder, P: FilePathProvider> {
159 path_provider: P,
161 writer: Option<AsyncArrowWriter<SizeAwareWriter<F::Writer>>>,
162 current_file: FileId,
164 writer_factory: F,
165 metadata: RegionMetadataRef,
167 index_config: IndexConfig,
169 indexer_builder: I,
171 current_indexer: Option<Indexer>,
173 bytes_written: Arc<AtomicUsize>,
174 file_cleaner: Option<TempFileCleaner>,
176 metrics: &'a mut Metrics,
178}
179
180pub trait WriterFactory {
181 type Writer: AsyncWrite + Send + Unpin;
182 fn create(
183 &mut self,
184 file_path: &str,
185 write_buffer_size: usize,
186 ) -> impl Future<Output = Result<Self::Writer>>;
187}
188
189pub struct ObjectStoreWriterFactory {
190 object_store: ObjectStore,
191}
192
193impl WriterFactory for ObjectStoreWriterFactory {
194 type Writer = Compat<FuturesAsyncWriter>;
195
196 async fn create(&mut self, file_path: &str, write_buffer_size: usize) -> Result<Self::Writer> {
197 self.object_store
198 .writer_with(file_path)
199 .chunk(write_buffer_size)
200 .concurrent(DEFAULT_WRITE_CONCURRENCY)
201 .await
202 .map(|v| v.into_futures_async_write().compat_write())
203 .context(OpenDalSnafu)
204 }
205}
206
207impl<'a, I, P> ParquetWriter<'a, ObjectStoreWriterFactory, I, P>
208where
209 P: FilePathProvider,
210 I: IndexerBuilder,
211{
212 pub async fn new_with_object_store(
213 object_store: ObjectStore,
214 metadata: RegionMetadataRef,
215 index_config: IndexConfig,
216 indexer_builder: I,
217 path_provider: P,
218 metrics: &'a mut Metrics,
219 ) -> ParquetWriter<'a, ObjectStoreWriterFactory, I, P> {
220 ParquetWriter::new(
221 ObjectStoreWriterFactory { object_store },
222 metadata,
223 index_config,
224 indexer_builder,
225 path_provider,
226 metrics,
227 )
228 .await
229 }
230
231 pub(crate) fn with_file_cleaner(mut self, cleaner: TempFileCleaner) -> Self {
232 self.file_cleaner = Some(cleaner);
233 self
234 }
235}
236
237impl<'a, F, I, P> ParquetWriter<'a, F, I, P>
238where
239 F: WriterFactory,
240 I: IndexerBuilder,
241 P: FilePathProvider,
242{
243 pub async fn new(
245 factory: F,
246 metadata: RegionMetadataRef,
247 index_config: IndexConfig,
248 indexer_builder: I,
249 path_provider: P,
250 metrics: &'a mut Metrics,
251 ) -> ParquetWriter<'a, F, I, P> {
252 let init_file = FileId::random();
253 let indexer = indexer_builder
254 .build(RegionFileId::new(metadata.region_id, init_file), 0, None)
255 .await;
256
257 ParquetWriter {
258 path_provider,
259 writer: None,
260 current_file: init_file,
261 writer_factory: factory,
262 metadata,
263 index_config,
264 indexer_builder,
265 current_indexer: Some(indexer),
266 bytes_written: Arc::new(AtomicUsize::new(0)),
267 file_cleaner: None,
268 metrics,
269 }
270 }
271
272 async fn finish_current_file(
274 &mut self,
275 ssts: &mut SstInfoArray,
276 stats: &mut SourceStats,
277 ) -> Result<()> {
278 if let Some(mut current_writer) = mem::take(&mut self.writer) {
280 let mut stats = mem::take(stats);
281 assert!(stats.num_rows > 0);
283
284 debug!(
285 "Finishing current file {}, file size: {}, num rows: {}",
286 self.current_file,
287 self.bytes_written.load(Ordering::Relaxed),
288 stats.num_rows
289 );
290
291 let mut index_output = IndexOutput::default();
294 match self.index_config.build_mode {
295 IndexBuildMode::Sync => {
296 index_output = self.current_indexer.as_mut().unwrap().finish().await;
297 }
298 IndexBuildMode::Async => {
299 debug!(
300 "Index for file {} will be built asynchronously later",
301 self.current_file
302 );
303 }
304 }
305 current_writer.flush().await.context(WriteParquetSnafu)?;
306
307 let parquet_metadata = current_writer.close().await.context(WriteParquetSnafu)?;
308 let file_size = self.bytes_written.load(Ordering::Relaxed) as u64;
309
310 let time_range = stats.time_range.unwrap();
312
313 let max_row_group_uncompressed_size: u64 = parquet_metadata
314 .row_groups()
315 .iter()
316 .map(|rg| {
317 rg.columns()
318 .iter()
319 .map(|c| c.uncompressed_size() as u64)
320 .sum::<u64>()
321 })
322 .max()
323 .unwrap_or(0);
324 let num_series = stats.series_estimator.finish();
325 ssts.push(SstInfo {
326 file_id: self.current_file,
327 time_range,
328 file_size,
329 max_row_group_uncompressed_size,
330 num_rows: stats.num_rows,
331 num_row_groups: parquet_metadata.num_row_groups() as u64,
332 file_metadata: Some(Arc::new(parquet_metadata)),
333 index_metadata: index_output,
334 num_series,
335 });
336 self.current_file = FileId::random();
337 self.bytes_written.store(0, Ordering::Relaxed)
338 };
339
340 Ok(())
341 }
342
343 pub async fn write_all_flat(
354 &mut self,
355 source: FlatSource,
356 override_sequence: Option<SequenceNumber>,
357 opts: &WriteOptions,
358 ) -> Result<SstInfoArray> {
359 let mut options = FlatSchemaOptions::from_encoding(self.metadata.primary_key_encoding);
360
361 if source.schema().fields().iter().any(is_json2_extension_type) {
362 options.concretized_json_types = source
363 .schema()
364 .fields()
365 .iter()
366 .filter(|&field| is_json2_extension_type(field))
367 .map(|field| (field.name().clone(), field.data_type().clone()))
368 .collect::<HashMap<_, _>>();
369 }
370
371 let converter = FlatBatchConverter::Flat(
372 FlatWriteFormat::new(self.metadata.clone(), &options)
373 .with_override_sequence(override_sequence),
374 );
375 let res = self.write_all_flat_inner(source, &converter, opts).await;
376 if res.is_err() {
377 let file_id = self.current_file;
378 if let Some(cleaner) = &self.file_cleaner {
379 cleaner.clean_by_file_id(file_id).await;
380 }
381 }
382 res
383 }
384
385 pub async fn write_all_flat_as_primary_key(
392 &mut self,
393 source: FlatSource,
394 override_sequence: Option<SequenceNumber>,
395 opts: &WriteOptions,
396 ) -> Result<SstInfoArray> {
397 let num_fields = self.metadata.field_columns().count();
398 let converter = FlatBatchConverter::PrimaryKey {
399 format: PrimaryKeyWriteFormat::new(self.metadata.clone())
400 .with_override_sequence(override_sequence),
401 num_fields,
402 };
403 let res = self.write_all_flat_inner(source, &converter, opts).await;
404 if res.is_err() {
405 let file_id = self.current_file;
406 if let Some(cleaner) = &self.file_cleaner {
407 cleaner.clean_by_file_id(file_id).await;
408 }
409 }
410 res
411 }
412
413 async fn write_all_flat_inner(
414 &mut self,
415 mut source: FlatSource,
416 converter: &FlatBatchConverter,
417 opts: &WriteOptions,
418 ) -> Result<SstInfoArray> {
419 let mut results = smallvec![];
420 let mut stats = SourceStats::default();
421
422 loop {
423 let start = Instant::now();
424 let batch = match source.next_batch().await {
425 Ok(Some(batch)) => batch,
426 Ok(None) => break,
427 Err(e) => {
428 self.abort_current_indexer().await;
429 return Err(e);
430 }
431 };
432 self.metrics.iter_source += start.elapsed();
433
434 if self.metadata.primary_key.is_empty() {
435 self.append_flat_batch(&batch, converter, opts, &mut stats)
436 .await?;
437 if self.exceeds_max_file_size(opts) {
438 self.finish_current_file(&mut results, &mut stats).await?;
439 }
440 } else if self.exceeds_max_file_size(opts)
441 && let Some(current_primary_key) = stats.last_primary_key.as_deref()
442 {
443 let series_split = match split_at_next_series(batch, current_primary_key) {
444 Ok(series_split) => series_split,
445 Err(e) => {
446 self.abort_current_indexer().await;
447 return Err(e);
448 }
449 };
450 match series_split {
451 SeriesBoundarySplit::Continue(batch) => {
452 self.append_flat_batch(&batch, converter, opts, &mut stats)
453 .await?;
454 }
455 SeriesBoundarySplit::Split {
456 current_file_tail,
457 next_file_head,
458 } => {
459 if let Some(tail) = current_file_tail {
460 self.append_flat_batch(&tail, converter, opts, &mut stats)
461 .await?;
462 }
463 self.finish_current_file(&mut results, &mut stats).await?;
464 self.append_flat_batch(&next_file_head, converter, opts, &mut stats)
465 .await?;
466 }
467 }
468 } else {
469 self.append_flat_batch(&batch, converter, opts, &mut stats)
470 .await?;
471 }
472 }
473
474 self.finish_current_file(&mut results, &mut stats).await?;
475
476 Ok(results)
478 }
479
480 async fn append_flat_batch(
481 &mut self,
482 batch: &RecordBatch,
483 converter: &FlatBatchConverter,
484 opts: &WriteOptions,
485 stats: &mut SourceStats,
486 ) -> Result<()> {
487 let result = async {
488 let arrow_batch = converter.convert_batch(batch)?;
489 let start = Instant::now();
490 self.maybe_init_writer(arrow_batch.schema_ref(), opts)
491 .await?
492 .write(&arrow_batch)
493 .await
494 .context(WriteParquetSnafu)?;
495 self.metrics.write_batch += start.elapsed();
496
497 stats.update_flat(batch)?;
498 if matches!(self.index_config.build_mode, IndexBuildMode::Sync) {
499 let start = Instant::now();
500 self.current_indexer
502 .as_mut()
503 .unwrap()
504 .update_flat(batch)
505 .await;
506 self.metrics.update_index += start.elapsed();
507 }
508 Ok(())
509 }
510 .await;
511
512 if result.is_err() {
513 self.abort_current_indexer().await;
514 }
515 result
516 }
517
518 fn exceeds_max_file_size(&self, opts: &WriteOptions) -> bool {
519 opts.max_file_size
520 .is_some_and(|max_size| self.bytes_written.load(Ordering::Relaxed) >= max_size)
521 }
522
523 async fn abort_current_indexer(&mut self) {
524 if let Some(indexer) = &mut self.current_indexer {
525 indexer.abort().await;
526 }
527 }
528
529 async fn maybe_init_writer(
530 &mut self,
531 schema: &SchemaRef,
532 opts: &WriteOptions,
533 ) -> Result<&mut AsyncArrowWriter<SizeAwareWriter<F::Writer>>> {
534 if let Some(ref mut w) = self.writer {
535 Ok(w)
536 } else {
537 let json = self.metadata.to_json().context(InvalidMetadataSnafu)?;
538 let key_value_meta = KeyValue::new(PARQUET_METADATA_KEY.to_string(), json);
539
540 let props_builder = WriterProperties::builder()
542 .set_key_value_metadata(Some(vec![key_value_meta]))
543 .set_compression(Compression::ZSTD(ZstdLevel::default()))
544 .set_encoding(Encoding::PLAIN)
545 .set_max_row_group_row_count(Some(opts.row_group_size))
546 .set_column_index_truncate_length(COLUMN_INDEX_TRUNCATE_LENGTH)
547 .set_statistics_truncate_length(None);
548 let props_builder = apply_internal_column_encoding(props_builder, &self.metadata);
549 let props_builder = apply_float_field_encoding(
550 props_builder,
551 &self.metadata,
552 opts.float_field_encoding,
553 );
554 let props_builder = apply_column_compression(props_builder, &self.metadata)?;
555 let writer_props = props_builder.build();
556
557 let sst_file_path = self.path_provider.build_sst_file_path(RegionFileId::new(
558 self.metadata.region_id,
559 self.current_file,
560 ));
561 let writer = SizeAwareWriter::new(
562 self.writer_factory
563 .create(&sst_file_path, opts.write_buffer_size.as_bytes() as usize)
564 .await?,
565 self.bytes_written.clone(),
566 );
567 let arrow_writer =
568 AsyncArrowWriter::try_new(writer, maybe_wrap_schema(schema)?, Some(writer_props))
569 .context(WriteParquetSnafu)?;
570 self.writer = Some(arrow_writer);
571
572 let indexer = self
573 .indexer_builder
574 .build(
575 RegionFileId::new(self.metadata.region_id, self.current_file),
576 0,
577 Some(opts.row_group_size),
578 )
579 .await;
580 self.current_indexer = Some(indexer);
581
582 Ok(self.writer.as_mut().unwrap())
584 }
585 }
586}
587
588#[derive(Default)]
589struct SourceStats {
590 num_rows: usize,
592 time_range: Option<(Timestamp, Timestamp)>,
594 last_primary_key: Option<Bytes>,
596 series_estimator: SeriesEstimator,
598}
599
600impl SourceStats {
601 fn update_flat(&mut self, record_batch: &RecordBatch) -> Result<()> {
602 if record_batch.num_rows() == 0 {
603 return Ok(());
604 }
605
606 self.num_rows += record_batch.num_rows();
607 self.series_estimator.update_flat(record_batch);
608 let (keys, values) = encoded_primary_keys(record_batch)?;
609 let key = keys.value(record_batch.num_rows() - 1);
610 self.last_primary_key = Some(Bytes::copy_from_slice(values.value(key as usize)));
611
612 let time_index_col_idx = time_index_column_index(record_batch.num_columns());
614 let timestamp_array = record_batch.column(time_index_col_idx);
615
616 if let Some((min_in_batch, max_in_batch)) = timestamp_range_from_array(timestamp_array)? {
617 if let Some(time_range) = &mut self.time_range {
618 time_range.0 = time_range.0.min(min_in_batch);
619 time_range.1 = time_range.1.max(max_in_batch);
620 } else {
621 self.time_range = Some((min_in_batch, max_in_batch));
622 }
623 }
624
625 Ok(())
626 }
627}
628
629fn timestamp_range_from_array(
631 timestamp_array: &ArrayRef,
632) -> Result<Option<(Timestamp, Timestamp)>> {
633 let (min_ts, max_ts) = match timestamp_array.data_type() {
634 DataType::Timestamp(TimeUnit::Second, _) => {
635 let array = timestamp_array
636 .as_any()
637 .downcast_ref::<TimestampSecondArray>()
638 .unwrap();
639 let min_val = min(array).map(Timestamp::new_second);
640 let max_val = max(array).map(Timestamp::new_second);
641 (min_val, max_val)
642 }
643 DataType::Timestamp(TimeUnit::Millisecond, _) => {
644 let array = timestamp_array
645 .as_any()
646 .downcast_ref::<TimestampMillisecondArray>()
647 .unwrap();
648 let min_val = min(array).map(Timestamp::new_millisecond);
649 let max_val = max(array).map(Timestamp::new_millisecond);
650 (min_val, max_val)
651 }
652 DataType::Timestamp(TimeUnit::Microsecond, _) => {
653 let array = timestamp_array
654 .as_any()
655 .downcast_ref::<TimestampMicrosecondArray>()
656 .unwrap();
657 let min_val = min(array).map(Timestamp::new_microsecond);
658 let max_val = max(array).map(Timestamp::new_microsecond);
659 (min_val, max_val)
660 }
661 DataType::Timestamp(TimeUnit::Nanosecond, _) => {
662 let array = timestamp_array
663 .as_any()
664 .downcast_ref::<TimestampNanosecondArray>()
665 .unwrap();
666 let min_val = min(array).map(Timestamp::new_nanosecond);
667 let max_val = max(array).map(Timestamp::new_nanosecond);
668 (min_val, max_val)
669 }
670 _ => {
671 return UnexpectedSnafu {
672 reason: format!(
673 "Unexpected data type of time index: {:?}",
674 timestamp_array.data_type()
675 ),
676 }
677 .fail();
678 }
679 };
680
681 Ok(min_ts.zip(max_ts))
683}
684
685struct SizeAwareWriter<W> {
688 inner: W,
689 size: Arc<AtomicUsize>,
690}
691
692impl<W> SizeAwareWriter<W> {
693 fn new(inner: W, size: Arc<AtomicUsize>) -> Self {
694 Self {
695 inner,
696 size: size.clone(),
697 }
698 }
699}
700
701impl<W> AsyncWrite for SizeAwareWriter<W>
702where
703 W: AsyncWrite + Unpin,
704{
705 fn poll_write(
706 mut self: Pin<&mut Self>,
707 cx: &mut Context<'_>,
708 buf: &[u8],
709 ) -> Poll<std::result::Result<usize, std::io::Error>> {
710 let this = self.as_mut().get_mut();
711
712 match Pin::new(&mut this.inner).poll_write(cx, buf) {
713 Poll::Ready(Ok(bytes_written)) => {
714 this.size.fetch_add(bytes_written, Ordering::Relaxed);
715 Poll::Ready(Ok(bytes_written))
716 }
717 other => other,
718 }
719 }
720
721 fn poll_flush(
722 mut self: Pin<&mut Self>,
723 cx: &mut Context<'_>,
724 ) -> Poll<std::result::Result<(), std::io::Error>> {
725 Pin::new(&mut self.inner).poll_flush(cx)
726 }
727
728 fn poll_shutdown(
729 mut self: Pin<&mut Self>,
730 cx: &mut Context<'_>,
731 ) -> Poll<std::result::Result<(), std::io::Error>> {
732 Pin::new(&mut self.inner).poll_shutdown(cx)
733 }
734}