Skip to main content

mito2/sst/parquet/
writer.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Parquet writer.
16
17use 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
70/// Converts a flat RecordBatch for writing to parquet.
71enum FlatBatchConverter {
72    /// Write as-is in flat format.
73    Flat(FlatWriteFormat),
74    /// Convert flat batch to primary-key format by stripping tag columns.
75    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
92/// Result of splitting a batch at the next series boundary.
93enum SeriesBoundarySplit {
94    /// The whole batch belongs to the current series and stays in the current file.
95    Continue(RecordBatch),
96    /// The batch crosses a series boundary.
97    Split {
98        /// Remaining rows of the current series, appended to the current file.
99        current_file_tail: Option<RecordBatch>,
100        /// Rows of the following series that start the next file.
101        next_file_head: RecordBatch,
102    },
103}
104
105/// Returns the dictionary keys and values of the encoded `__primary_key` column.
106fn 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
130/// Splits `batch` at the first row whose encoded primary key differs from
131/// `current_primary_key`, the last primary key written to the current file.
132///
133/// The writer finishes an oversized file at such a series boundary so that the
134/// primary key ranges of output files never overlap, which allows pickers like
135/// TWCS to detect overlapping files by their time and primary key ranges. A
136/// series is never split in the middle: if no row starts a new series, the whole
137/// batch stays in the current file even if it already exceeds the size limit.
138fn 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
157/// Parquet SST writer.
158pub struct ParquetWriter<'a, F: WriterFactory, I: IndexerBuilder, P: FilePathProvider> {
159    /// Path provider that creates SST and index file paths according to file id.
160    path_provider: P,
161    writer: Option<AsyncArrowWriter<SizeAwareWriter<F::Writer>>>,
162    /// Current active file id.
163    current_file: FileId,
164    writer_factory: F,
165    /// Region metadata of the source and the target SST.
166    metadata: RegionMetadataRef,
167    /// Global index config.
168    index_config: IndexConfig,
169    /// Indexer build that can create indexer for multiple files.
170    indexer_builder: I,
171    /// Current active indexer.
172    current_indexer: Option<Indexer>,
173    bytes_written: Arc<AtomicUsize>,
174    /// Cleaner to remove temp files on failure.
175    file_cleaner: Option<TempFileCleaner>,
176    /// Write metrics
177    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    /// Creates a new parquet SST writer.
244    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    /// Finishes current SST file and index file.
273    async fn finish_current_file(
274        &mut self,
275        ssts: &mut SstInfoArray,
276        stats: &mut SourceStats,
277    ) -> Result<()> {
278        // maybe_init_writer will re-create a new file.
279        if let Some(mut current_writer) = mem::take(&mut self.writer) {
280            let mut stats = mem::take(stats);
281            // At least one row has been written.
282            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            // Finish indexer and writer.
292            // safety: writer and index can only be both present or not.
293            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            // Safety: num rows > 0 so we must have min/max.
311            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    /// Iterates FlatSource and writes all RecordBatch in flat format to Parquet file.
344    ///
345    /// The source must yield batches globally sorted by the encoded primary key.
346    /// `opts.max_file_size` is a soft limit for regions with a primary key: the
347    /// current file is finished at the next series boundary after exceeding the
348    /// limit (see [split_at_next_series]), so a series larger than the limit
349    /// stays in one file. Regions without a primary key are split at batch
350    /// boundaries.
351    ///
352    /// Returns the [SstInfo] if the SST is written.
353    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    /// Iterates FlatSource and writes all RecordBatch in primary-key format to Parquet file.
386    ///
387    /// The source must yield batches globally sorted by the encoded primary key.
388    /// See [Self::write_all_flat] for the `opts.max_file_size` semantics.
389    ///
390    /// Returns the [SstInfo] if the SST is written.
391    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        // object_store.write will make sure all bytes are written or an error is raised.
477        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                // safety: self.current_indexer must be set when first batch has been written.
501                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            // TODO(yingwen): Find and set proper column encoding for internal columns: op type and tsid.
541            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            // safety: self.writer is assigned above
583            Ok(self.writer.as_mut().unwrap())
584        }
585    }
586}
587
588#[derive(Default)]
589struct SourceStats {
590    /// Number of rows fetched.
591    num_rows: usize,
592    /// Time range of fetched batches.
593    time_range: Option<(Timestamp, Timestamp)>,
594    /// Last primary key written to the current file.
595    last_primary_key: Option<Bytes>,
596    /// Series estimator for computing num_series.
597    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        // Get the timestamp column by index
613        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
629/// Gets min and max timestamp from an timestamp array.
630fn 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    // If min timestamp exists, max timestamp should also exist.
682    Ok(min_ts.zip(max_ts))
683}
684
685/// Workaround for [AsyncArrowWriter] does not provide a method to
686/// get total bytes written after close.
687struct 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}