Skip to content

Commit 9bf2603

Browse files
committed
Review fixes, and cleanup
Signed-off-by: Arham Chopra <arham.chopra@cubistsystematic.com>
1 parent 9f413e2 commit 9bf2603

12 files changed

Lines changed: 159 additions & 28 deletions

cpp/csp/adapters/arrow/ArrowFieldWriter.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,11 @@ CreatedFieldWriter createFieldWriter(
345345
std::vector<std::shared_ptr<::arrow::ArrayBuilder>> childBuilders;
346346
std::vector<std::unique_ptr<FieldWriter>> childWriters;
347347

348-
// Use fieldNames() for stable insertion order (fields() is sorted for memory layout)
348+
// Ordering convention: csp writes struct child columns in declaration order, using
349+
// fieldNames() rather than fields() (which is sorted by struct memory layout). This keeps
350+
// nested-struct child order consistent with top-level column order and stable against
351+
// internal field packing. The reader matches children by name, so either order round-trips,
352+
// but declaration order is the on-disk contract.
349353
for( auto & subFieldName : nestedMeta -> fieldNames() )
350354
{
351355
auto subField = nestedMeta -> field( subFieldName );

cpp/csp/adapters/parquet/ArrowBackedArrayBuilder.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ void ArrowBackedArrayBuilder::init( const std::string & columnName, const Struct
8888
{
8989
auto created = csp::adapters::arrow::createFieldWriter( columnName, field );
9090
m_writer = std::move( created.writer );
91+
// Reserve the first chunk up front so the per-row append path doesn't reallocate as rows accumulate.
92+
m_writer -> reserve( getChunkSize() );
93+
}
94+
95+
void ArrowBackedArrayBuilder::reserve( int64_t numRows )
96+
{
97+
m_writer -> reserve( numRows );
9198
}
9299

93100
std::shared_ptr<::arrow::DataType> ArrowBackedArrayBuilder::getDataType()
@@ -132,6 +139,9 @@ std::shared_ptr<::arrow::Array> ArrowBackedArrayBuilder::buildArray()
132139
auto arrays = m_writer -> finish();
133140
CSP_TRUE_OR_THROW_RUNTIME( arrays.size() == 1,
134141
"ArrowBackedArrayBuilder expected 1 array from FieldWriter, got " << arrays.size() );
142+
// finish() resets the underlying builder's capacity; re-reserve for the next chunk so the per-row
143+
// append path stays allocation-free (and keeps UnsafeAppend within reserved capacity).
144+
m_writer -> reserve( getChunkSize() );
135145
return arrays[0];
136146
}
137147

cpp/csp/adapters/parquet/ArrowBackedArrayBuilder.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ArrowBackedArrayBuilder : public ArrowSingleColumnArrayBuilder
4242
int64_t length() const override;
4343
void handleRowFinished() override;
4444
std::shared_ptr<::arrow::Array> buildArray() override;
45+
void reserve( int64_t numRows ) override;
4546

4647
// --- Scratch mode API ---
4748
// Get the scratch struct for value setting

cpp/csp/adapters/parquet/ArrowSingleColumnArrayBuilder.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ class ArrowSingleColumnArrayBuilder
3737
// Release the array from the currently built values
3838
virtual std::shared_ptr<::arrow::Array> buildArray() = 0;
3939

40+
// Pre-reserve builder capacity for numRows rows. Default no-op; fixed-width builders override to
41+
// reserve their Arrow builder so the per-row append path avoids repeated reallocation.
42+
virtual void reserve( int64_t /*numRows*/ ) {}
43+
4044
private:
4145
const std::string m_columnName;
4246
const std::uint32_t m_chunkSize;

cpp/csp/adapters/parquet/ParquetDictBasketOutputWriter.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,15 @@ ParquetStructDictBasketOutputWriter::ParquetStructDictBasketOutputWriter( Parque
151151
: ParquetDictBasketOutputWriter( outputAdapterManager, columnName )
152152
{
153153

154-
// We don't support fieldMap for now, only default field map
154+
// We don't support fieldMap for now, only default field map.
155+
// Iterate fieldNames() (declaration order), NOT fields() (sorted by memory layout): csp writes
156+
// struct columns -- both top-level and nested -- in declaration order, so the on-disk column order
157+
// matches the struct definition and stays stable regardless of internal field packing.
155158
auto structMetaPtr = std::static_pointer_cast<const CspStructType>( cspTypePtr ) -> meta().get();
156159
DictionaryPtr dict = std::make_shared<Dictionary>();
157-
for(auto&& field:structMetaPtr->fields())
160+
for( auto && fieldName : structMetaPtr -> fieldNames() )
158161
{
159-
dict->insert(field->fieldname(), columnName + "." + field->fieldname());
162+
dict->insert( fieldName, columnName + "." + fieldName );
160163
}
161164
m_valueOutputAdapter = getStructOutputHandler( cspTypePtr, dict );
162165
}

cpp/csp/adapters/parquet/ParquetOutputAdapter.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ StructParquetOutputHandler::StructParquetOutputHandler( Engine *engine, ParquetW
100100
CSP_THROW( ValueError, "Struct field '" << structFieldName << "' not found" );
101101

102102
if( structField -> type() -> type() == CspType::Type::DIALECT_GENERIC )
103-
continue;
103+
CSP_THROW( TypeError, "Writing of column " << structField -> fieldname()
104+
<< " of type DIALECT_GENERIC is not supported" );
104105

105106
auto builder = createArrowBackedArrayBuilderForField( columnName, getChunkSize(), structField );
106107
m_arrowBuilders.push_back( builder.get() );

cpp/csp/adapters/parquet/ParquetWriter.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,9 @@ void ParquetWriter::adoptMetadataFrom( ParquetWriter & source )
215215

216216
void ParquetWriter::onEndCycle()
217217
{
218-
// NOTE: when no file is open -- e.g. a filename_provider ticked "" to "pause" output -- this body is
219-
// skipped, so handleRowFinished() does NOT run and each scratch column's isSet bit is never cleared
220-
// for that cycle. If a column ticks *while paused* and then does NOT tick on the cycle the file
221-
// reopens, the stale paused-window value is emitted instead of null (the dropped tick "leaks" into
222-
// the first row after resume). This matches the previous writer's behaviour. If dropping ticks while
223-
// paused should instead null them out, clear each builder's scratch/isSet here in the else path.
218+
// When no file is open -- e.g. a filename_provider ticked "" to "pause" output -- this body is
219+
// skipped, so handleRowFinished() does not run for that cycle. As a result, a value that ticked
220+
// while paused is emitted on the first row after the file reopens, rather than being nulled.
224221
if( isFileOpen() ) [[likely]]
225222
{
226223
DateTime now;

cpp/csp/adapters/parquet/RecordBatchFileSink.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
namespace csp::adapters::parquet
2222
{
23+
2324
namespace
2425
{
2526

@@ -70,7 +71,7 @@ std::shared_ptr<::arrow::io::OutputStream> openOutputStream( const std::string &
7071
}
7172

7273
FileWriter makeParquetFileWriter( const std::string & path, const std::shared_ptr<::arrow::Schema> & schema,
73-
const std::string & compression, bool allowOverwrite )
74+
const std::string & compression, bool allowOverwrite, bool useDictionary )
7475
{
7576
auto stream = openOutputStream( path, allowOverwrite );
7677

@@ -80,6 +81,11 @@ FileWriter makeParquetFileWriter( const std::string & path, const std::shared_pt
8081
// Arrow 6.0) is always present, so there is no need to gate on the Arrow version or fall back to
8182
// the deprecated PARQUET_2_0.
8283
props.version( ::parquet::ParquetVersion::PARQUET_2_6 );
84+
// Dictionary encoding is on by default. For high-cardinality numeric columns it is a large write-time
85+
// cost (hash table build) and can even grow files; allow it to be turned off (trading file size for
86+
// write throughput). Low-cardinality columns still benefit from leaving it on (the default).
87+
if( !useDictionary )
88+
props.disable_dictionary();
8389
::parquet::ArrowWriterProperties::Builder arrowProps;
8490
arrowProps.store_schema(); // preserve arrow (file/column) schema metadata in the parquet file
8591

@@ -204,16 +210,17 @@ FileWriter makeSplitWriter( const std::string & dir, const std::shared_ptr<::arr
204210

205211
RecordBatchSink makeFileSink( bool writeArrowBinary, bool splitColumns,
206212
const std::string & compression, bool allowOverwrite,
207-
std::function<void( const std::string & )> fileVisitor )
213+
std::function<void( const std::string & )> fileVisitor,
214+
bool useDictionary )
208215
{
209216
const std::string extension = writeArrowBinary ? ".arrow" : ".parquet";
210217

211218
// Per-file factory: format choice is the only difference.
212219
FileWriterFactory perFile =
213-
[writeArrowBinary, compression, allowOverwrite]( const std::string & path, const std::shared_ptr<::arrow::Schema> & schema )
220+
[writeArrowBinary, compression, allowOverwrite, useDictionary]( const std::string & path, const std::shared_ptr<::arrow::Schema> & schema )
214221
{
215222
return writeArrowBinary ? makeIpcFileWriter( path, schema, compression, allowOverwrite )
216-
: makeParquetFileWriter( path, schema, compression, allowOverwrite );
223+
: makeParquetFileWriter( path, schema, compression, allowOverwrite, useDictionary );
217224
};
218225

219226
// Layout choice: single file vs. one file per column.

cpp/csp/adapters/parquet/RecordBatchFileSink.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ RecordBatchSink makeFileSink( bool writeArrowBinary,
2222
bool splitColumns,
2323
const std::string & compression,
2424
bool allowOverwrite,
25-
std::function<void( const std::string & )> fileVisitor = {} );
25+
std::function<void( const std::string & )> fileVisitor = {},
26+
bool useDictionary = true );
2627

2728
} // namespace csp::adapters::parquet
2829

cpp/csp/python/adapters/parquetadapterimpl.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ csp::AdapterManager *create_parquet_output_adapter_manager( PyEngine *engine, co
685685
bool splitColumns = properties.get<bool>( "split_columns_to_files" );
686686
bool allowOverwrite = properties.get<bool>( "allow_overwrite" );
687687
std::string compression = properties.get<std::string>( "compression" );
688+
bool useDictionary = properties.get( "write_dictionary", true );
688689

689690
// Optional Python file_visitor: wrapped as a plain C++ callback, invoked synchronously (with the
690691
// GIL held during the run) after each file is closed, so a slow visitor blocks the engine. Only
@@ -707,13 +708,13 @@ csp::AdapterManager *create_parquet_output_adapter_manager( PyEngine *engine, co
707708
}
708709

709710
auto * mgr = engine -> engine() -> createOwnedObject<ParquetOutputAdapterManager>( properties );
710-
mgr -> setSink( makeFileSink( writeArrowBinary, splitColumns, compression, allowOverwrite, fileVisitor ) );
711+
mgr -> setSink( makeFileSink( writeArrowBinary, splitColumns, compression, allowOverwrite, fileVisitor, useDictionary ) );
711712

712713
// Dict-basket writers always write split-column files (one per value/symbol/index column),
713714
// without a file visitor.
714-
mgr -> setSinkFactory( [writeArrowBinary, compression, allowOverwrite]( const std::string & ) -> RecordBatchSink
715+
mgr -> setSinkFactory( [writeArrowBinary, compression, allowOverwrite, useDictionary]( const std::string & ) -> RecordBatchSink
715716
{
716-
return makeFileSink( writeArrowBinary, /*splitColumns=*/true, compression, allowOverwrite, {} );
717+
return makeFileSink( writeArrowBinary, /*splitColumns=*/true, compression, allowOverwrite, {}, useDictionary );
717718
} );
718719

719720
return mgr;

0 commit comments

Comments
 (0)