Skip to content

Commit 0425348

Browse files
SteNicholasclaude
andcommitted
feat(blob): support blob-write-null-on-missing-file and blob-write-null-on-fetch-failure options
Introduce two table options for descriptor BLOB writes and thread them from the table options map through BlobFileFormat/BlobWriterBuilder into BlobFormatWriter: - blob-write-null-on-missing-file: write NULL instead of failing the write when the file referenced by a blob descriptor does not exist. - blob-write-null-on-fetch-failure: write NULL when the referenced data cannot be fetched for other reasons, e.g. an invalid offset. BlobFormatWriter::WriteBlob now opens the descriptor input stream before writing any bytes, so a rejected row leaves no partial data in the output stream, while failures during the streaming copy still fail the write. JindoFileSystem maps JDO_FILE_NOT_FOUND_ERROR to Status::NotExist in Open() and JindoInputStream::Length() so that a missing OSS object is classified as a missing file rather than a generic IO error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f24a61b commit 0425348

13 files changed

Lines changed: 550 additions & 43 deletions

include/paimon/defs.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,15 @@ struct PAIMON_EXPORT Options {
416416
/// Blob View is enabled, cpp paimon cannot automatically obtain the upstream table warehouse
417417
/// path and requires manual configuration by the user. No default value.
418418
static const char BLOB_VIEW_UPSTREAM_WAREHOUSE[];
419+
/// "blob-write-null-on-missing-file" - Whether to write NULL for a descriptor BLOB value when
420+
/// the referenced file does not exist at write time. When false, the write fails when the
421+
/// descriptor is read. Default value is "false".
422+
static const char BLOB_WRITE_NULL_ON_MISSING_FILE[];
423+
/// "blob-write-null-on-fetch-failure" - Whether to write NULL for a descriptor BLOB value when
424+
/// the referenced data cannot be fetched at write time (e.g. invalid descriptor or invalid
425+
/// offset). A missing file is handled by "blob-write-null-on-missing-file". When false, the
426+
/// write fails when the descriptor is read. Default value is "false".
427+
static const char BLOB_WRITE_NULL_ON_FETCH_FAILURE[];
419428
/// "global-index.enabled" - Whether to enable global index for scan. Default value is "true".
420429
static const char GLOBAL_INDEX_ENABLED[];
421430
/// "global-index.thread-num" - The maximum number of concurrent scanner for global index. No

src/paimon/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,7 @@ if(PAIMON_BUILD_TESTS)
773773
fs/local/local_file_test.cpp
774774
# fs/jindo/jindo_file_system_factory_test.cpp
775775
# fs/jindo/jindo_file_system_test.cpp
776+
# fs/jindo/jindo_utils_test.cpp
776777
STATIC_LINK_LIBS
777778
paimon_shared
778779
${PAIMON_LOCAL_FILE_SYSTEM_STATIC_LINK_LIBS}

src/paimon/common/defs.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ const char Options::FALLBACK_BLOB_DESCRIPTOR_FIELD[] = "blob.stored-descriptor-f
104104
const char Options::BLOB_VIEW_FIELD[] = "blob-view-field";
105105
const char Options::BLOB_VIEW_RESOLVE_ENABLED[] = "blob-view.resolve.enabled";
106106
const char Options::BLOB_VIEW_UPSTREAM_WAREHOUSE[] = "blob-view-upstream-warehouse";
107+
const char Options::BLOB_WRITE_NULL_ON_MISSING_FILE[] = "blob-write-null-on-missing-file";
108+
const char Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE[] = "blob-write-null-on-fetch-failure";
107109
const char Options::GLOBAL_INDEX_ENABLED[] = "global-index.enabled";
108110
const char Options::GLOBAL_INDEX_THREAD_NUM[] = "global-index.thread-num";
109111
const char Options::GLOBAL_INDEX_EXTERNAL_PATH[] = "global-index.external-path";

src/paimon/format/blob/blob_file_format_factory_test.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,24 @@
1616

1717
#include "paimon/format/blob/blob_file_format_factory.h"
1818

19+
#include <map>
20+
#include <memory>
21+
#include <string>
22+
23+
#include "arrow/api.h"
24+
#include "arrow/c/bridge.h"
1925
#include "gtest/gtest.h"
26+
#include "paimon/common/data/blob_utils.h"
27+
#include "paimon/common/utils/arrow/status_utils.h"
28+
#include "paimon/data/blob.h"
29+
#include "paimon/defs.h"
30+
#include "paimon/format/file_format.h"
31+
#include "paimon/format/file_format_factory.h"
32+
#include "paimon/format/format_writer.h"
33+
#include "paimon/format/writer_builder.h"
34+
#include "paimon/fs/local/local_file_system.h"
2035
#include "paimon/status.h"
36+
#include "paimon/testing/utils/test_helper.h"
2137
#include "paimon/testing/utils/testharness.h"
2238

2339
namespace paimon::blob::test {
@@ -29,4 +45,46 @@ TEST(BlobFileFormatFactoryTest, TestIdentifier) {
2945
ASSERT_EQ(file_format->Identifier(), "blob");
3046
}
3147

48+
TEST(BlobFileFormatFactoryTest, TestWriteNullOptionPropagation) {
49+
// Verifies the option flows through the production path
50+
// FileFormatFactory::Get -> BlobFileFormat -> BlobWriterBuilder -> BlobFormatWriter.
51+
std::unique_ptr<paimon::test::UniqueTestDirectory> dir =
52+
paimon::test::UniqueTestDirectory::Create();
53+
ASSERT_TRUE(dir);
54+
std::shared_ptr<FileSystem> fs = std::make_shared<LocalFileSystem>();
55+
auto struct_type = arrow::struct_({BlobUtils::ToArrowField("blob_col", true)});
56+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Blob> missing_blob,
57+
Blob::FromPath(dir->Str() + "/not_exist_file", /*offset=*/0,
58+
/*length=*/10));
59+
60+
auto write_once = [&](const std::map<std::string, std::string>& options,
61+
const std::string& file_name) -> Status {
62+
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileFormat> format,
63+
FileFormatFactory::Get("blob", options));
64+
auto schema = arrow::schema(struct_type->fields());
65+
::ArrowSchema c_schema;
66+
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema, &c_schema));
67+
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriterBuilder> writer_builder,
68+
format->CreateWriterBuilder(&c_schema, /*batch_size=*/1024));
69+
// The blob writer builder is a SpecificFSWriterBuilder by construction.
70+
static_cast<SpecificFSWriterBuilder*>(writer_builder.get())->WithFileSystem(fs);
71+
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<OutputStream> out,
72+
fs->Create(dir->Str() + "/" + file_name, /*overwrite=*/true));
73+
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FormatWriter> writer,
74+
writer_builder->Build(out, "none"));
75+
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> array,
76+
paimon::test::TestHelper::MakeBlobDescriptorArray(
77+
struct_type, missing_blob, GetDefaultPool()));
78+
ArrowArray c_array;
79+
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
80+
PAIMON_RETURN_NOT_OK(writer->AddBatch(&c_array));
81+
return writer->Finish();
82+
};
83+
84+
// Without the option, writing the missing descriptor fails.
85+
ASSERT_NOK_WITH_MSG(write_once({}, "no_option.blob"), "not exists");
86+
// The option set in the format options map reaches the writer.
87+
ASSERT_OK(write_once({{Options::BLOB_WRITE_NULL_ON_MISSING_FILE, "true"}}, "with_option.blob"));
88+
}
89+
3290
} // namespace paimon::blob::test

src/paimon/format/blob/blob_format_writer.cpp

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,33 @@
3131
#include "paimon/common/utils/delta_varint_compressor.h"
3232
#include "paimon/data/blob.h"
3333
#include "paimon/io/byte_array_input_stream.h"
34+
#include "paimon/logging.h"
3435

3536
namespace paimon::blob {
3637

3738
BlobFormatWriter::BlobFormatWriter(const std::shared_ptr<OutputStream>& out, const std::string& uri,
3839
const std::shared_ptr<arrow::DataType>& data_type,
3940
const std::shared_ptr<FileSystem>& fs,
40-
const std::shared_ptr<MemoryPool>& pool)
41-
: out_(out), uri_(uri), data_type_(data_type), fs_(fs), pool_(pool) {
41+
const std::shared_ptr<MemoryPool>& pool,
42+
bool write_null_on_missing_file,
43+
bool write_null_on_fetch_failure)
44+
: out_(out),
45+
uri_(uri),
46+
data_type_(data_type),
47+
fs_(fs),
48+
pool_(pool),
49+
write_null_on_missing_file_(write_null_on_missing_file),
50+
write_null_on_fetch_failure_(write_null_on_fetch_failure) {
4251
metrics_ = std::make_shared<MetricsImpl>();
4352
tmp_buffer_ = Bytes::AllocateBytes(kTmpBufferSize, pool_.get());
4453
magic_number_bytes_ = IntegerToLittleEndian<int32_t>(BlobDefs::kMagicNumber, pool_);
54+
logger_ = Logger::GetLogger("BlobFormatWriter");
4555
}
4656

4757
Result<std::unique_ptr<BlobFormatWriter>> BlobFormatWriter::Create(
4858
const std::shared_ptr<OutputStream>& out, const std::shared_ptr<arrow::DataType>& data_type,
49-
const std::shared_ptr<FileSystem>& fs, const std::shared_ptr<MemoryPool>& pool) {
59+
const std::shared_ptr<FileSystem>& fs, const std::shared_ptr<MemoryPool>& pool,
60+
bool write_null_on_missing_file, bool write_null_on_fetch_failure) {
5061
if (out == nullptr) {
5162
return Status::Invalid("blob format writer create failed. out is nullptr");
5263
}
@@ -65,7 +76,8 @@ Result<std::unique_ptr<BlobFormatWriter>> BlobFormatWriter::Create(
6576
fmt::format("field {} is not BLOB", data_type->field(0)->ToString()));
6677
}
6778
PAIMON_ASSIGN_OR_RAISE(std::string uri, out->GetUri());
68-
return std::unique_ptr<BlobFormatWriter>(new BlobFormatWriter(out, uri, data_type, fs, pool));
79+
return std::unique_ptr<BlobFormatWriter>(new BlobFormatWriter(
80+
out, uri, data_type, fs, pool, write_null_on_missing_file, write_null_on_fetch_failure));
6981
}
7082

7183
Status BlobFormatWriter::AddBatch(ArrowArray* batch) {
@@ -126,27 +138,41 @@ Status BlobFormatWriter::Finish() {
126138
}
127139

128140
Status BlobFormatWriter::WriteBlob(std::string_view blob_data) {
129-
crc32_ = 0;
130-
PAIMON_ASSIGN_OR_RAISE(int64_t previous_pos, out_->GetPos());
131-
132-
// write magic number
133-
PAIMON_RETURN_NOT_OK(WriteWithCrc32(magic_number_bytes_->data(), magic_number_bytes_->size()));
134-
135-
// write blob content
141+
// Open the blob input stream before writing any bytes, so that a failed fetch can be
142+
// converted to a NULL element without leaving partial data in the output stream.
136143
// Dynamically check whether blob_data is a serialized BlobDescriptor (by magic header)
137144
// rather than relying on blob_as_descriptor_ config. This is consistent with Java behavior:
138145
// at write time, the input bytes are auto-detected as descriptor or raw data.
139146
std::unique_ptr<InputStream> in;
140147
PAIMON_ASSIGN_OR_RAISE(bool is_descriptor,
141148
BlobDescriptor::IsBlobDescriptor(blob_data.data(), blob_data.size()));
142149
if (is_descriptor) {
143-
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<Blob> blob,
144-
Blob::FromDescriptor(blob_data.data(), blob_data.size()));
145-
PAIMON_ASSIGN_OR_RAISE(in, blob->NewInputStream(fs_));
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;
164+
}
165+
in = std::move(opened).value();
146166
} else {
147167
in = std::make_unique<ByteArrayInputStream>(blob_data.data(), blob_data.size());
148168
}
149169
PAIMON_ASSIGN_OR_RAISE(int64_t file_length, in->Length());
170+
171+
crc32_ = 0;
172+
PAIMON_ASSIGN_OR_RAISE(int64_t previous_pos, out_->GetPos());
173+
174+
// write magic number
175+
PAIMON_RETURN_NOT_OK(WriteWithCrc32(magic_number_bytes_->data(), magic_number_bytes_->size()));
150176
int64_t total_read_length = 0;
151177
int64_t read_len = std::min(file_length, static_cast<int64_t>(tmp_buffer_->size()));
152178
while (read_len > 0) {
@@ -179,6 +205,13 @@ Status BlobFormatWriter::WriteBlob(std::string_view blob_data) {
179205
return Status::OK();
180206
}
181207

208+
Result<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_);
213+
}
214+
182215
Status BlobFormatWriter::WriteBytes(const char* data, int64_t length) {
183216
PAIMON_ASSIGN_OR_RAISE(int64_t actual, out_->Write(data, length));
184217
if (actual != length) {

src/paimon/format/blob/blob_format_writer.h

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "arrow/api.h"
2727
#include "arrow/util/crc32.h"
2828
#include "paimon/format/format_writer.h"
29+
#include "paimon/logging.h"
2930
#include "paimon/memory/bytes.h"
3031
#include "paimon/memory/memory_pool.h"
3132
#include "paimon/result.h"
@@ -39,6 +40,7 @@ struct ArrowArray;
3940
namespace paimon {
4041
class Blob;
4142
class FileSystem;
43+
class InputStream;
4244
class Metrics;
4345
class OutputStream;
4446
} // namespace paimon
@@ -51,7 +53,8 @@ class BlobFormatWriter : public FormatWriter {
5153
public:
5254
static Result<std::unique_ptr<BlobFormatWriter>> Create(
5355
const std::shared_ptr<OutputStream>& out, const std::shared_ptr<arrow::DataType>& data_type,
54-
const std::shared_ptr<FileSystem>& fs, const std::shared_ptr<MemoryPool>& pool);
56+
const std::shared_ptr<FileSystem>& fs, const std::shared_ptr<MemoryPool>& pool,
57+
bool write_null_on_missing_file = false, bool write_null_on_fetch_failure = false);
5558

5659
Status AddBatch(ArrowArray* batch) override;
5760

@@ -70,11 +73,15 @@ class BlobFormatWriter : public FormatWriter {
7073
private:
7174
BlobFormatWriter(const std::shared_ptr<OutputStream>& out, const std::string& uri,
7275
const std::shared_ptr<arrow::DataType>& data_type,
73-
const std::shared_ptr<FileSystem>& fs,
74-
const std::shared_ptr<MemoryPool>& pool);
76+
const std::shared_ptr<FileSystem>& fs, const std::shared_ptr<MemoryPool>& pool,
77+
bool write_null_on_missing_file, bool write_null_on_fetch_failure);
7578

7679
Status WriteBlob(std::string_view blob_data);
7780

81+
/// Deserialize the descriptor and open an input stream on the referenced data.
82+
Result<std::unique_ptr<InputStream>> OpenDescriptorInputStream(
83+
std::string_view blob_data) const;
84+
7885
Status WriteBytes(const char* data, int64_t length);
7986
Status WriteWithCrc32(const char* data, int64_t length);
8087

@@ -95,6 +102,9 @@ class BlobFormatWriter : public FormatWriter {
95102
std::shared_ptr<FileSystem> fs_;
96103
std::shared_ptr<MemoryPool> pool_;
97104
std::shared_ptr<Metrics> metrics_;
105+
bool write_null_on_missing_file_ = false;
106+
bool write_null_on_fetch_failure_ = false;
107+
std::unique_ptr<Logger> logger_;
98108
};
99109

100110
} // namespace paimon::blob

0 commit comments

Comments
 (0)