Skip to content

Commit aa195ce

Browse files
committed
feat: use buffered path for WriteTable if max row group size is set
1 parent 4acdeee commit aa195ce

3 files changed

Lines changed: 133 additions & 18 deletions

File tree

cpp/src/parquet/arrow/arrow_reader_writer_test.cc

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5894,13 +5894,12 @@ TEST(TestArrowReadWrite, WriteRecordBatchRespectsMaxRowGroupSize) {
58945894
auto pool = ::arrow::default_memory_pool();
58955895
auto sink = CreateOutputStream();
58965896
// Use a small byte size limit with the default (large) row count limit so
5897-
// that only the byte size limit takes effect. Use a small data page size
5898-
// so that buffered values are flushed to pages (and thus counted by the
5899-
// size check) between batches.
5897+
// that only the byte size limit takes effect. The default data page size is
5898+
// kept, so this also covers values that are still buffered by the column
5899+
// encoders rather than flushed into pages.
59005900
auto writer_properties = WriterProperties::Builder()
59015901
.max_row_group_size(4 * 1024)
59025902
->disable_dictionary()
5903-
->data_pagesize(1024)
59045903
->build();
59055904
auto arrow_writer_properties = default_arrow_writer_properties();
59065905

@@ -5938,6 +5937,87 @@ TEST(TestArrowReadWrite, WriteRecordBatchRespectsMaxRowGroupSize) {
59385937
}
59395938
}
59405939

5940+
TEST(TestArrowReadWrite, WriteTableRespectsMaxRowGroupSize) {
5941+
// When a byte size limit is set, WriteTable switches to buffered row groups
5942+
// and feeds the table in small batches, so a chunk_size covering the whole
5943+
// table is still split into row groups bounded by the limit.
5944+
constexpr int64_t kMaxRowGroupSize = 16 * 1024;
5945+
constexpr int kNumRows = 8000;
5946+
5947+
std::shared_ptr<Table> table;
5948+
ASSERT_NO_FATAL_FAILURE(
5949+
MakeDoubleTable(/*num_columns=*/1, kNumRows, /*nchunks=*/1, &table));
5950+
5951+
auto sink = CreateOutputStream();
5952+
// The default data page size (1MB) is much larger than the limit here, so
5953+
// this only works if values still buffered by the column encoders count
5954+
// towards the row group size.
5955+
auto writer_properties = WriterProperties::Builder()
5956+
.max_row_group_size(kMaxRowGroupSize)
5957+
->write_batch_size(256)
5958+
->disable_dictionary()
5959+
->build();
5960+
ASSERT_OK_NO_THROW(WriteTable(*table, ::arrow::default_memory_pool(), sink,
5961+
/*chunk_size=*/kNumRows, writer_properties));
5962+
ASSERT_OK_AND_ASSIGN(auto buffer, sink->Finish());
5963+
5964+
auto reader = ParquetFileReader::Open(std::make_shared<BufferReader>(buffer));
5965+
auto file_metadata = reader->metadata();
5966+
// The limit must have split the single chunk into multiple row groups.
5967+
ASSERT_GT(file_metadata->num_row_groups(), 1);
5968+
5969+
int64_t total_rows = 0;
5970+
for (int i = 0; i < file_metadata->num_row_groups(); ++i) {
5971+
auto row_group_metadata = file_metadata->RowGroup(i);
5972+
total_rows += row_group_metadata->num_rows();
5973+
// The size estimate is conservative, so row groups stay under the limit.
5974+
EXPECT_LE(row_group_metadata->total_compressed_size(), kMaxRowGroupSize);
5975+
}
5976+
// All rows are written exactly once.
5977+
EXPECT_EQ(kNumRows, total_rows);
5978+
}
5979+
5980+
TEST(TestArrowReadWrite, WriteTableUnlimitedRowGroupSize) {
5981+
// Without an explicit byte size limit, chunk_size alone decides the row group
5982+
// boundaries and row groups are not buffered.
5983+
constexpr int kNumRows = 2000;
5984+
std::shared_ptr<Table> table;
5985+
ASSERT_NO_FATAL_FAILURE(
5986+
MakeDoubleTable(/*num_columns=*/1, kNumRows, /*nchunks=*/1, &table));
5987+
5988+
auto sink = CreateOutputStream();
5989+
ASSERT_OK_NO_THROW(WriteTable(*table, ::arrow::default_memory_pool(), sink,
5990+
/*chunk_size=*/1000, default_writer_properties()));
5991+
ASSERT_OK_AND_ASSIGN(auto buffer, sink->Finish());
5992+
5993+
auto reader = ParquetFileReader::Open(std::make_shared<BufferReader>(buffer));
5994+
ASSERT_EQ(2, reader->metadata()->num_row_groups());
5995+
EXPECT_EQ(1000, reader->metadata()->RowGroup(0)->num_rows());
5996+
EXPECT_EQ(1000, reader->metadata()->RowGroup(1)->num_rows());
5997+
}
5998+
5999+
TEST(TestArrowReadWrite, WriteTableMaxRowGroupSizeRoundTrip) {
6000+
// The data must survive the switch to the buffered write path unchanged.
6001+
constexpr int kNumRows = 3000;
6002+
std::shared_ptr<Table> table;
6003+
ASSERT_NO_FATAL_FAILURE(
6004+
MakeDoubleTable(/*num_columns=*/3, kNumRows, /*nchunks=*/1, &table));
6005+
6006+
auto sink = CreateOutputStream();
6007+
auto writer_properties =
6008+
WriterProperties::Builder().max_row_group_size(16 * 1024)->build();
6009+
ASSERT_OK_NO_THROW(WriteTable(*table, ::arrow::default_memory_pool(), sink,
6010+
/*chunk_size=*/kNumRows, writer_properties));
6011+
ASSERT_OK_AND_ASSIGN(auto buffer, sink->Finish());
6012+
6013+
ASSERT_OK_AND_ASSIGN(auto reader, OpenFile(std::make_shared<BufferReader>(buffer),
6014+
::arrow::default_memory_pool()));
6015+
std::shared_ptr<Table> result;
6016+
ASSERT_OK_NO_THROW(reader->ReadTable(&result));
6017+
ASSERT_OK(result->ValidateFull());
6018+
AssertTablesEqual(*table, *result, /*same_chunk_layout=*/false);
6019+
}
6020+
59416021
TEST(TestArrowReadWrite, MultithreadedWrite) {
59426022
const int num_columns = 20;
59436023
const int num_rows = 1000;

cpp/src/parquet/arrow/writer.cc

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,23 @@ class FileWriterImpl : public FileWriter {
432432
WriteRowGroup(0, 0).OrElse([&](auto&&) { PARQUET_IGNORE_NOT_OK(Close()); }));
433433
}
434434

435+
// If max_row_group_size is set, use buffered path to write row groups.
436+
if (this->properties().max_row_group_size() != std::numeric_limits<int64_t>::max()) {
437+
::arrow::TableBatchReader reader(table);
438+
reader.set_chunksize(std::min(chunk_size, this->properties().write_batch_size()));
439+
while (true) {
440+
std::shared_ptr<RecordBatch> batch;
441+
RETURN_NOT_OK(reader.ReadNext(&batch));
442+
if (batch == nullptr) {
443+
break;
444+
}
445+
RETURN_NOT_OK(WriteRecordBatchBuffered(*batch, chunk_size).OrElse([&](auto&&) {
446+
PARQUET_IGNORE_NOT_OK(Close());
447+
}));
448+
}
449+
return Status::OK();
450+
}
451+
435452
for (int chunk = 0; chunk * chunk_size < table.num_rows(); chunk++) {
436453
int64_t offset = chunk * chunk_size;
437454
RETURN_NOT_OK(WriteRowGroup(offset, std::min(chunk_size, table.num_rows() - offset))
@@ -450,26 +467,38 @@ class FileWriterImpl : public FileWriter {
450467
}
451468

452469
Status WriteRecordBatch(const RecordBatch& batch) override {
470+
// Checked up front because reading the properties of a closed file throws.
471+
RETURN_NOT_OK(CheckClosed());
472+
return WriteRecordBatchBuffered(batch, this->properties().max_row_group_length());
473+
}
474+
475+
// Writes `batch` into buffered row groups, starting a new row group whenever
476+
// the current one reaches `max_rows_per_row_group` rows or the max row group
477+
// size in bytes configured on the writer properties.
478+
Status WriteRecordBatchBuffered(const RecordBatch& batch,
479+
int64_t max_rows_per_row_group) {
453480
RETURN_NOT_OK(CheckClosed());
454481
if (batch.num_rows() == 0) {
455482
return Status::OK();
456483
}
457484

458-
// Max number of rows allowed in a row group.
459-
const int64_t max_row_group_length = this->properties().max_row_group_length();
460485
// Max compressed byte size allowed in a row group.
461486
const int64_t max_row_group_size = this->properties().max_row_group_size();
462487
const bool row_group_size_limited =
463488
max_row_group_size != std::numeric_limits<int64_t>::max();
464489

490+
// Estimated size of the data accumulated in the current row group.
491+
auto estimated_row_group_size = [&]() {
492+
const auto buffered = row_group_writer_->estimated_buffered_stats();
493+
return row_group_writer_->total_compressed_bytes() +
494+
row_group_writer_->total_compressed_bytes_written() + buffered.value_bytes +
495+
buffered.def_level_bytes + buffered.rep_level_bytes + buffered.dict_bytes;
496+
};
497+
465498
// Whether the current row group reached the row count or byte size limit.
466499
auto row_group_full = [&]() {
467-
return row_group_writer_->num_rows() >= max_row_group_length ||
468-
(row_group_size_limited &&
469-
row_group_writer_->total_compressed_bytes() +
470-
row_group_writer_->total_compressed_bytes_written() +
471-
row_group_writer_->estimated_buffered_stats().dict_bytes >=
472-
max_row_group_size);
500+
return row_group_writer_->num_rows() >= max_rows_per_row_group ||
501+
(row_group_size_limited && estimated_row_group_size() >= max_row_group_size);
473502
};
474503

475504
// Initialize a new buffered row group writer if necessary.
@@ -510,7 +539,7 @@ class FileWriterImpl : public FileWriter {
510539
int64_t offset = 0;
511540
while (offset < batch.num_rows()) {
512541
const int64_t batch_size =
513-
std::min(max_row_group_length - row_group_writer_->num_rows(),
542+
std::min(max_rows_per_row_group - row_group_writer_->num_rows(),
514543
batch.num_rows() - offset);
515544
RETURN_NOT_OK(WriteBatch(offset, batch_size));
516545
offset += batch_size;

cpp/src/parquet/properties.h

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -496,13 +496,19 @@ class PARQUET_EXPORT WriterProperties {
496496
return this;
497497
}
498498

499-
/// Specify the max row group size in compressed bytes.
499+
/// Specify the max row group size in bytes.
500500
/// Default unlimited.
501501
///
502-
/// The limit is checked against the compressed pages accumulated in the
503-
/// current row group, so the actual row group size may slightly exceed it.
504-
/// Only effective for buffered row groups (
505-
/// parquet::arrow::FileWriter::WriteRecordBatch).
502+
/// The limit is checked between writes against an estimate of the data
503+
/// accumulated in the current row group, which combines the size of the
504+
/// compressed pages with an uncompressed estimate of the values still
505+
/// buffered by the column encoders. The estimate is therefore approximate
506+
/// and errs on the conservative side.
507+
/// Row groups are buffered in memory while the limit is enforced, so
508+
/// enabling this option increases the memory footprint of the writer.
509+
/// It is honoured by parquet::arrow::FileWriter::WriteRecordBatch and
510+
/// parquet::arrow::FileWriter::WriteTable; row groups created explicitly
511+
/// through NewRowGroup()/WriteColumnChunk() are not affected.
506512
Builder* max_row_group_size(int64_t max_row_group_size) {
507513
max_row_group_size_ = max_row_group_size;
508514
return this;

0 commit comments

Comments
 (0)