3030#include " paimon/common/utils/arrow/status_utils.h"
3131#include " paimon/common/utils/delta_varint_compressor.h"
3232#include " paimon/data/blob.h"
33+ #include " paimon/fs/file_system.h"
3334#include " paimon/io/byte_array_input_stream.h"
3435#include " paimon/logging.h"
3536
@@ -48,6 +49,8 @@ BlobFormatWriter::BlobFormatWriter(const std::shared_ptr<OutputStream>& out, con
4849 pool_(pool),
4950 write_null_on_missing_file_(write_null_on_missing_file),
5051 write_null_on_fetch_failure_(write_null_on_fetch_failure) {
52+ // Create() has already checked that data_type has exactly one BLOB field.
53+ blob_field_name_ = data_type_->field (0 )->name ();
5154 metrics_ = std::make_shared<MetricsImpl>();
5255 tmp_buffer_ = Bytes::AllocateBytes (kTmpBufferSize , pool_.get ());
5356 magic_number_bytes_ = IntegerToLittleEndian<int32_t >(BlobDefs::kMagicNumber , pool_);
@@ -67,6 +70,9 @@ Result<std::unique_ptr<BlobFormatWriter>> BlobFormatWriter::Create(
6770 if (pool == nullptr ) {
6871 return Status::Invalid (" blob format writer create failed. pool is nullptr" );
6972 }
73+ if (fs == nullptr ) {
74+ return Status::Invalid (" blob format writer create failed. fs is nullptr" );
75+ }
7076 if (data_type->num_fields () != 1 ) {
7177 return Status::Invalid (
7278 fmt::format (" blob data type field number {} is not 1" , data_type->num_fields ()));
@@ -117,6 +123,10 @@ Status BlobFormatWriter::AddBatch(ArrowArray* batch) {
117123}
118124
119125Status BlobFormatWriter::Flush () {
126+ metrics_->SetCounter (BlobMetrics::WRITE_NULL_ON_MISSING_FILE_COUNT ,
127+ null_on_missing_file_count_);
128+ metrics_->SetCounter (BlobMetrics::WRITE_NULL_ON_FETCH_FAILURE_COUNT ,
129+ null_on_fetch_failure_count_);
120130 return out_->Flush ();
121131}
122132
@@ -140,29 +150,20 @@ Status BlobFormatWriter::Finish() {
140150Status BlobFormatWriter::WriteBlob (std::string_view blob_data) {
141151 // Open the blob input stream before writing any bytes, so that a failed fetch can be
142152 // converted to a NULL element without leaving partial data in the output stream.
143- // Dynamically check whether blob_data is a serialized BlobDescriptor (by magic header)
144- // rather than relying on blob_as_descriptor_ config. This is consistent with Java behavior:
145- // at write time, the input bytes are auto-detected as descriptor or raw data.
153+ // Whether blob_data is a serialized BlobDescriptor is detected by its magic header rather
154+ // than taken from a blob_as_descriptor option, so each row may hold either form.
146155 std::unique_ptr<InputStream> in;
147156 PAIMON_ASSIGN_OR_RAISE (bool is_descriptor,
148157 BlobDescriptor::IsBlobDescriptor (blob_data.data (), blob_data.size ()));
149158 if (is_descriptor) {
150- Result<std::unique_ptr<InputStream>> opened = OpenDescriptorInputStream (blob_data);
151- if (!opened.ok ()) {
152- const Status& status = opened.status ();
153- // A missing file is only handled by 'blob-write-null-on-missing-file'; other fetch
154- // failures are only handled by 'blob-write-null-on-fetch-failure' (aligned with Java).
155- bool write_null =
156- status.IsNotExist () ? write_null_on_missing_file_ : write_null_on_fetch_failure_;
157- if (write_null) {
158- PAIMON_LOG_WARN (logger_, " Failed to open blob, writing NULL for BLOB field: %s" ,
159- status.ToString ().c_str ());
160- bin_lengths_.push_back (BlobDefs::kNullBinLength );
161- return Status::OK ();
162- }
163- return status;
159+ PAIMON_ASSIGN_OR_RAISE (std::unique_ptr<InputStream> descriptor_in,
160+ OpenDescriptorInputStream (blob_data));
161+ // A null stream means a write-null option already converted the failure.
162+ if (descriptor_in == nullptr ) {
163+ bin_lengths_.push_back (BlobDefs::kNullBinLength );
164+ return Status::OK ();
164165 }
165- in = std::move (opened). value ( );
166+ in = std::move (descriptor_in );
166167 } else {
167168 in = std::make_unique<ByteArrayInputStream>(blob_data.data (), blob_data.size ());
168169 }
@@ -206,10 +207,73 @@ Status BlobFormatWriter::WriteBlob(std::string_view blob_data) {
206207}
207208
208209Result<std::unique_ptr<InputStream>> BlobFormatWriter::OpenDescriptorInputStream (
209- std::string_view blob_data) const {
210- PAIMON_ASSIGN_OR_RAISE (std::unique_ptr<Blob> blob,
211- Blob::FromDescriptor (blob_data.data (), blob_data.size ()));
212- return blob->NewInputStream (fs_);
210+ std::string_view blob_data) {
211+ // A descriptor that cannot be deserialized is a fetch failure: the referenced data cannot be
212+ // reached. Its URI is inside the unreadable bytes, hence the placeholder; the underlying
213+ // status comes from the byte reader and never mentions blobs, hence the added context.
214+ Result<std::unique_ptr<Blob>> blob_result =
215+ Blob::FromDescriptor (blob_data.data (), blob_data.size ());
216+ if (!blob_result.ok ()) {
217+ const Status& status = blob_result.status ();
218+ return HandleFetchFailure (
219+ " <unknown>" , status.WithMessage (" invalid blob descriptor: " , status.message ()));
220+ }
221+ std::unique_ptr<Blob> blob = std::move (blob_result).value ();
222+
223+ // A missing file is identified by FileSystem::Exists rather than by the status of a failed
224+ // open, since file system implementations disagree on which status a missing file maps to.
225+ // The check runs only when `write_null_on_missing_file_` needs the classification; otherwise
226+ // a missing file gets the same treatment as any other failed open.
227+ if (write_null_on_missing_file_) {
228+ Result<bool > exists = fs_->Exists (blob->Uri ());
229+ if (exists.ok ()) {
230+ if (!exists.value ()) {
231+ return HandleMissingFile (blob->Uri ());
232+ }
233+ } else if (!write_null_on_fetch_failure_) {
234+ // The check cannot answer whether the file is there; with no fetch-failure handling
235+ // to defer to, fail rather than assume either answer.
236+ const Status& status = exists.status ();
237+ return status.WithMessage (" failed to check existence of blob file '" , blob->Uri (),
238+ " ': " , status.message ());
239+ }
240+ // A failed check is otherwise deferred to the open below, which can still succeed.
241+ }
242+
243+ Result<std::unique_ptr<InputStream>> opened = blob->NewInputStream (fs_);
244+ if (!opened.ok ()) {
245+ // The file can be deleted between the check above and this open. Classifying that from
246+ // `opened.status()` would reintroduce the plugin-specific status codes this writer avoids,
247+ // so ask FileSystem::Exists once more. This narrows the window rather than closing it; a
248+ // check that cannot answer falls through to the open failure.
249+ if (write_null_on_missing_file_) {
250+ Result<bool > exists = fs_->Exists (blob->Uri ());
251+ if (exists.ok () && !exists.value ()) {
252+ return HandleMissingFile (blob->Uri ());
253+ }
254+ }
255+ return HandleFetchFailure (blob->Uri (), opened.status ());
256+ }
257+ return std::move (opened).value ();
258+ }
259+
260+ std::unique_ptr<InputStream> BlobFormatWriter::HandleMissingFile (const std::string& blob_uri) {
261+ PAIMON_LOG_WARN (logger_, " Blob file %s does not exist, writing NULL for BLOB field %s into %s" ,
262+ blob_uri.c_str (), blob_field_name_.c_str (), uri_.c_str ());
263+ ++null_on_missing_file_count_;
264+ return std::unique_ptr<InputStream>();
265+ }
266+
267+ Result<std::unique_ptr<InputStream>> BlobFormatWriter::HandleFetchFailure (
268+ const std::string& blob_uri, const Status& status) {
269+ if (!write_null_on_fetch_failure_) {
270+ return status;
271+ }
272+ PAIMON_LOG_WARN (logger_, " Failed to fetch blob %s, writing NULL for BLOB field %s into %s: %s" ,
273+ blob_uri.c_str (), blob_field_name_.c_str (), uri_.c_str (),
274+ status.ToString ().c_str ());
275+ ++null_on_fetch_failure_count_;
276+ return std::unique_ptr<InputStream>();
213277}
214278
215279Status BlobFormatWriter::WriteBytes (const char * data, int64_t length) {
0 commit comments