Skip to content

Commit 789d0e6

Browse files
committed
Parquet output: fix sink bugs, remove orphaned Python sink
From the multi-model review: - delete the now-dead Python sink (_parquet_rb_writer.py, rb_sink wiring, TestOutputSinkDirect) since file I/O is all C++ now - RecordBatchFileSink: guard mkdir(""), fix close-path exception safety, resolve compression via Arrow's Codec API (case-insensitive) - honor an explicit writeTimestampColumn - fail fast on single-file + dict basket - add regression tests Signed-off-by: Arham Chopra <arham.chopra@cubistsystematic.com>
1 parent eab7d98 commit 789d0e6

5 files changed

Lines changed: 283 additions & 381 deletions

File tree

cpp/csp/adapters/parquet/ParquetWriter.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,15 @@ PushInputAdapter *ParquetWriter::getStatusAdapter()
118118
void ParquetWriter::start()
119119
{
120120
std::vector<std::shared_ptr<::arrow::Field>> arrowFields;
121-
if( !m_writeTimestampColumn.has_value() && !m_adapterMgr.getTimestampColumnName().empty() )
121+
// Honor an explicitly-set writeTimestampColumn; otherwise auto-detect from whether
122+
// a timestamp column name was configured.
123+
bool wantTimestamp = m_writeTimestampColumn.has_value()
124+
? m_writeTimestampColumn.value()
125+
: !m_adapterMgr.getTimestampColumnName().empty();
126+
if( wantTimestamp )
122127
{
128+
CSP_TRUE_OR_THROW_RUNTIME( !m_adapterMgr.getTimestampColumnName().empty(),
129+
"writeTimestampColumn is true but no timestamp_column_name was provided" );
123130
m_writeTimestampColumn = true;
124131
auto tsBuilder = createArrowBackedArrayBuilder(
125132
m_adapterMgr.getTimestampColumnName(), getChunkSize(), CspType::DATETIME() );

cpp/csp/adapters/parquet/RecordBatchFileSink.cpp

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
#include <arrow/util/config.h>
1212
#include <parquet/arrow/writer.h>
1313

14+
#include <algorithm>
15+
#include <cctype>
16+
#include <exception>
1417
#include <memory>
1518
#include <optional>
1619
#include <unordered_map>
@@ -35,27 +38,32 @@ using FileWriterFactory =
3538

3639
::arrow::Compression::type resolveCompression( const std::string & name )
3740
{
38-
static const std::unordered_map<std::string, ::arrow::Compression::type> mapping{
39-
{ "", ::arrow::Compression::UNCOMPRESSED },
40-
{ "none", ::arrow::Compression::UNCOMPRESSED },
41-
{ "snappy", ::arrow::Compression::SNAPPY },
42-
{ "gzip", ::arrow::Compression::GZIP },
43-
{ "brotli", ::arrow::Compression::BROTLI },
44-
{ "zstd", ::arrow::Compression::ZSTD },
45-
{ "lz4", ::arrow::Compression::LZ4 },
46-
{ "lz4_frame", ::arrow::Compression::LZ4_FRAME },
47-
};
48-
auto it = mapping.find( name );
49-
CSP_TRUE_OR_THROW_RUNTIME( it != mapping.end(), "Unable to resolve compression: " << name );
50-
return it -> second;
41+
// csp treats "" / "none" as no compression; Arrow expects lower-case codec names.
42+
std::string lower( name );
43+
std::transform( lower.begin(), lower.end(), lower.begin(),
44+
[]( unsigned char c ) { return static_cast<char>( std::tolower( c ) ); } );
45+
if( lower.empty() || lower == "none" )
46+
return ::arrow::Compression::UNCOMPRESSED;
47+
48+
// Arrow is the source of truth for which compression names exist...
49+
auto compressionType = ::arrow::util::Codec::GetCompressionType( lower );
50+
CSP_TRUE_OR_THROW_RUNTIME( compressionType.ok(), "Unsupported compression '" << name << "'" );
51+
// ...and whether this Arrow build was actually compiled with support for it.
52+
CSP_TRUE_OR_THROW_RUNTIME( ::arrow::util::Codec::IsAvailable( compressionType.ValueUnsafe() ),
53+
"Compression '" << name << "' is not available in this Arrow build" );
54+
return compressionType.ValueUnsafe();
5155
}
5256

5357
// Enforce overwrite policy, create parent dirs, and open the output stream.
5458
std::shared_ptr<::arrow::io::OutputStream> openOutputStream( const std::string & path, bool allowOverwrite )
5559
{
5660
if( !allowOverwrite && utils::fileExists( path ) )
5761
CSP_THROW( FileExistsError, "Trying to overwrite existing file " << path << " while allow_overwrite is false" );
58-
utils::mkdir( utils::dirname( path ) );
62+
// Only create the parent directory when the path has one; a bare relative
63+
// filename (e.g. "out.parquet") has an empty dirname, and mkdir("") fails.
64+
auto parentDir = utils::dirname( path );
65+
if( !parentDir.empty() )
66+
utils::mkdir( parentDir );
5967

6068
auto result = ::arrow::io::FileOutputStream::Open( path );
6169
STATUS_OK_OR_THROW_RUNTIME( result.status(), "Failed to open output stream " << path );
@@ -94,8 +102,10 @@ FileWriter makeParquetFileWriter( const std::string & path, const std::shared_pt
94102
},
95103
[writer, stream]()
96104
{
97-
STATUS_OK_OR_THROW_RUNTIME( writer -> Close(), "Failed to close parquet writer" );
98-
STATUS_OK_OR_THROW_RUNTIME( stream -> Close(), "Failed to close parquet output stream" );
105+
auto writerStatus = writer -> Close();
106+
auto streamStatus = stream -> Close(); // always close the stream, even if the footer flush failed
107+
STATUS_OK_OR_THROW_RUNTIME( writerStatus, "Failed to close parquet writer" );
108+
STATUS_OK_OR_THROW_RUNTIME( streamStatus, "Failed to close parquet output stream" );
99109
} };
100110
}
101111

@@ -124,8 +134,10 @@ FileWriter makeIpcFileWriter( const std::string & path, const std::shared_ptr<::
124134
},
125135
[writer, stream]()
126136
{
127-
STATUS_OK_OR_THROW_RUNTIME( writer -> Close(), "Failed to close arrow IPC writer" );
128-
STATUS_OK_OR_THROW_RUNTIME( stream -> Close(), "Failed to close arrow output stream" );
137+
auto writerStatus = writer -> Close();
138+
auto streamStatus = stream -> Close(); // always close the stream, even if the writer close failed
139+
STATUS_OK_OR_THROW_RUNTIME( writerStatus, "Failed to close arrow IPC writer" );
140+
STATUS_OK_OR_THROW_RUNTIME( streamStatus, "Failed to close arrow output stream" );
129141
} };
130142
}
131143

@@ -134,7 +146,8 @@ FileWriter makeIpcFileWriter( const std::string & path, const std::shared_ptr<::
134146
FileWriter makeSplitWriter( const std::string & dir, const std::shared_ptr<::arrow::Schema> & schema,
135147
const FileWriterFactory & perColumn, const std::string & extension )
136148
{
137-
utils::mkdir( dir );
149+
if( !dir.empty() )
150+
utils::mkdir( dir );
138151

139152
auto subWriters = std::make_shared<std::vector<FileWriter>>();
140153
auto colSchemas = std::make_shared<std::vector<std::shared_ptr<::arrow::Schema>>>();
@@ -159,8 +172,14 @@ FileWriter makeSplitWriter( const std::string & dir, const std::shared_ptr<::arr
159172
},
160173
[subWriters]()
161174
{
175+
std::exception_ptr firstError;
162176
for( auto & w : *subWriters )
163-
w.close();
177+
{
178+
try { w.close(); }
179+
catch( ... ) { if( !firstError ) firstError = std::current_exception(); }
180+
}
181+
if( firstError )
182+
std::rethrow_exception( firstError );
164183
} };
165184
}
166185

@@ -197,10 +216,13 @@ RecordBatchSink makeFileSink( bool writeArrowBinary, bool splitColumns,
197216
{
198217
if( current -> has_value() )
199218
{
200-
( *current ) -> close();
219+
// Detach + reset BEFORE invoking the (user-supplied) visitor, so that a throwing
220+
// visitor cannot cause a later onStop()->closeCurrent() to close this writer twice.
221+
FileWriter w = std::move( current -> value() );
222+
current -> reset();
223+
w.close();
201224
if( fileVisitor )
202225
fileVisitor( *currentPath );
203-
current -> reset();
204226
}
205227
};
206228

csp/adapters/_parquet_rb_writer.py

Lines changed: 0 additions & 202 deletions
This file was deleted.

csp/adapters/output_adapters/parquet.py

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@ def _parquet_dict_basket_writer(
199199
return self._parquet_dict_basket_writer_node_def
200200

201201
def publish_dict_basket(self, column_name, value, key_type, value_type):
202+
if not self._split_columns_to_files:
203+
raise ValueError(
204+
f"Cannot publish dict basket '{column_name}': dict baskets require "
205+
"split_columns_to_files=True (single-file parquet output is not supported for baskets)"
206+
)
202207
if key_type is not str:
203208
raise NotImplementedError("Writing of baskets with non str key type is not supported")
204209
if self._filename_provider is not None:
@@ -224,38 +229,6 @@ def _create(self, engine, memo):
224229
if self._parquet_output_adapter_manager:
225230
return self._parquet_output_adapter_manager
226231

227-
from csp.adapters._parquet_rb_writer import create_sink, create_sink_factory
228-
229-
# Create main sink for the primary writer
230-
file_name = self._properties.get("file_name", "")
231-
compression = self._properties.get("compression", "snappy")
232-
allow_overwrite = self._properties.get("allow_overwrite", False)
233-
write_arrow_binary = self._properties.get("write_arrow_binary", False)
234-
split_columns = self._properties.get("split_columns_to_files", False)
235-
file_visitor = self._properties.get("file_visitor")
236-
file_metadata = self._properties.get("file_metadata")
237-
column_metadata = self._properties.get("column_metadata")
238-
239-
sink = create_sink(
240-
file_name=file_name,
241-
compression=compression,
242-
allow_overwrite=allow_overwrite,
243-
write_arrow_binary=write_arrow_binary,
244-
split_columns_to_files=split_columns,
245-
file_visitor=file_visitor,
246-
file_metadata=file_metadata,
247-
column_metadata=column_metadata,
248-
)
249-
self._properties["rb_sink"] = sink
250-
251-
# Sink factory for dict basket writers (no file_visitor — only main sink visits)
252-
sink_factory = create_sink_factory(
253-
compression=compression,
254-
allow_overwrite=allow_overwrite,
255-
write_arrow_binary=write_arrow_binary,
256-
)
257-
self._properties["rb_sink_factory"] = sink_factory
258-
259232
self._parquet_output_adapter_manager = _parquetadapterimpl._parquet_output_adapter_manager(
260233
engine, self._properties
261234
)

0 commit comments

Comments
 (0)