Skip to content

Commit b22b3c4

Browse files
authored
feat(blob): support blob-write-null-on-missing-file and blob-write-null-on-fetch-failure options (#421)
1 parent b1ffcb0 commit b22b3c4

15 files changed

Lines changed: 820 additions & 87 deletions

cmake_modules/ThirdpartyToolchain.cmake

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1636,13 +1636,16 @@ macro(build_arrow)
16361636

16371637
target_link_libraries(arrow_dataset INTERFACE arrow_acero)
16381638

1639+
# libarrow.a calls dlsym; keep ${CMAKE_DL_LIBS} in the interface so -ldl is placed
1640+
# after libarrow.a on linkers that resolve symbols strictly left-to-right.
16391641
target_link_libraries(arrow
16401642
INTERFACE zstd
16411643
snappy
16421644
lz4
16431645
zlib
16441646
re2::re2
1645-
arrow_bundled_dependencies)
1647+
arrow_bundled_dependencies
1648+
${CMAKE_DL_LIBS})
16461649

16471650
target_link_libraries(parquet
16481651
INTERFACE zstd

include/paimon/defs.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,15 @@ struct PAIMON_EXPORT Options {
439439
/// Blob View is enabled, cpp paimon cannot automatically obtain the upstream table warehouse
440440
/// path and requires manual configuration by the user. No default value.
441441
static const char BLOB_VIEW_UPSTREAM_WAREHOUSE[];
442+
/// "blob-write-null-on-missing-file" - Whether to write NULL for a descriptor BLOB value when
443+
/// the referenced file does not exist at write time. When false, the write fails when the
444+
/// descriptor is read. Default value is "false".
445+
static const char BLOB_WRITE_NULL_ON_MISSING_FILE[];
446+
/// "blob-write-null-on-fetch-failure" - Whether to write NULL for a descriptor BLOB value when
447+
/// the referenced data cannot be fetched at write time (e.g. invalid descriptor or invalid
448+
/// offset). A missing file is handled by "blob-write-null-on-missing-file". When false, the
449+
/// write fails when the descriptor is read. Default value is "false".
450+
static const char BLOB_WRITE_NULL_ON_FETCH_FAILURE[];
442451
/// "global-index.enabled" - Whether to enable global index for scan. Default value is "true".
443452
static const char GLOBAL_INDEX_ENABLED[];
444453
/// "global-index.thread-num" - The maximum number of concurrent scanner for global index. No

src/paimon/CMakeLists.txt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,18 +791,29 @@ if(PAIMON_BUILD_TESTS)
791791
${TEST_STATIC_LINK_LIBS}
792792
${GTEST_LINK_TOOLCHAIN})
793793

794+
# jindo_utils_test only checks the in-memory status conversion and does not need an
795+
# OSS cluster, so it runs whenever jindo is built. The other jindo tests need real
796+
# OSS access and stay disabled.
797+
set(FS_TEST_JINDO_SOURCES)
798+
if(PAIMON_ENABLE_JINDO)
799+
list(APPEND FS_TEST_JINDO_SOURCES fs/jindo/jindo_utils_test.cpp)
800+
endif()
801+
794802
add_paimon_test(fs_test
795803
SOURCES
796804
common/fs/file_system_test.cpp
797805
common/fs/resolving_file_system_test.cpp
798806
fs/local/local_file_test.cpp
807+
${FS_TEST_JINDO_SOURCES}
799808
# fs/jindo/jindo_file_system_factory_test.cpp
800809
# fs/jindo/jindo_file_system_test.cpp
801810
STATIC_LINK_LIBS
802811
paimon_shared
803812
${PAIMON_LOCAL_FILE_SYSTEM_STATIC_LINK_LIBS}
804-
# ${PAIMON_JINDO_FILE_SYSTEM_STATIC_LINK_LIBS}
813+
${PAIMON_JINDO_FILE_SYSTEM_STATIC_LINK_LIBS}
805814
test_utils_static
806-
${GTEST_LINK_TOOLCHAIN})
815+
${GTEST_LINK_TOOLCHAIN}
816+
EXTRA_INCLUDES
817+
${JINDOSDK_INCLUDE_DIR})
807818

808819
endif()

src/paimon/common/defs.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ const char Options::FALLBACK_BLOB_DESCRIPTOR_FIELD[] = "blob.stored-descriptor-f
108108
const char Options::BLOB_VIEW_FIELD[] = "blob-view-field";
109109
const char Options::BLOB_VIEW_RESOLVE_ENABLED[] = "blob-view.resolve.enabled";
110110
const char Options::BLOB_VIEW_UPSTREAM_WAREHOUSE[] = "blob-view-upstream-warehouse";
111+
const char Options::BLOB_WRITE_NULL_ON_MISSING_FILE[] = "blob-write-null-on-missing-file";
112+
const char Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE[] = "blob-write-null-on-fetch-failure";
111113
const char Options::GLOBAL_INDEX_ENABLED[] = "global-index.enabled";
112114
const char Options::GLOBAL_INDEX_THREAD_NUM[] = "global-index.thread-num";
113115
const char Options::GLOBAL_INDEX_EXTERNAL_PATH[] = "global-index.external-path";

src/paimon/format/blob/blob_file_batch_reader_test.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,10 @@ TEST_P(BlobFileBatchReaderTest, EmptyFile) {
234234
file_system->Create(dir->Str() + "/file.blob", /*overwrite=*/true));
235235
std::shared_ptr<arrow::Field> blob_field = BlobUtils::ToArrowField("blob_col");
236236
auto struct_type = arrow::struct_({blob_field});
237-
ASSERT_OK_AND_ASSIGN(std::shared_ptr<BlobFormatWriter> writer,
238-
BlobFormatWriter::Create(output_stream, struct_type, file_system, pool_));
237+
ASSERT_OK_AND_ASSIGN(
238+
std::shared_ptr<BlobFormatWriter> writer,
239+
BlobFormatWriter::Create(output_stream, struct_type, /*write_null_on_missing_file=*/false,
240+
/*write_null_on_fetch_failure=*/false, file_system, pool_));
239241

240242
ASSERT_OK(writer->Flush());
241243
ASSERT_OK(writer->Finish());

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: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,32 @@
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,
40+
bool write_null_on_missing_file,
41+
bool write_null_on_fetch_failure,
3942
const std::shared_ptr<FileSystem>& fs,
4043
const std::shared_ptr<MemoryPool>& pool)
41-
: out_(out), uri_(uri), data_type_(data_type), fs_(fs), pool_(pool) {
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,
59+
bool write_null_on_missing_file, bool write_null_on_fetch_failure,
4960
const std::shared_ptr<FileSystem>& fs, const std::shared_ptr<MemoryPool>& pool) {
5061
if (out == nullptr) {
5162
return Status::Invalid("blob format writer create failed. out is nullptr");
@@ -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, write_null_on_missing_file, write_null_on_fetch_failure, fs, pool));
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: 15 additions & 0 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
@@ -49,8 +51,13 @@ namespace paimon::blob {
4951
// https://cwiki.apache.org/confluence/display/PAIMON/PIP-35%3A+Introduce+Blob+to+store+multimodal+data
5052
class BlobFormatWriter : public FormatWriter {
5153
public:
54+
/// When opening a descriptor input fails, `write_null_on_missing_file` converts a
55+
/// missing file (Status::NotExist) to a NULL element and `write_null_on_fetch_failure`
56+
/// converts any other open failure; failures during the streaming copy always fail the
57+
/// write. See Options::BLOB_WRITE_NULL_ON_MISSING_FILE / BLOB_WRITE_NULL_ON_FETCH_FAILURE.
5258
static Result<std::unique_ptr<BlobFormatWriter>> Create(
5359
const std::shared_ptr<OutputStream>& out, const std::shared_ptr<arrow::DataType>& data_type,
60+
bool write_null_on_missing_file, bool write_null_on_fetch_failure,
5461
const std::shared_ptr<FileSystem>& fs, const std::shared_ptr<MemoryPool>& pool);
5562

5663
Status AddBatch(ArrowArray* batch) override;
@@ -70,11 +77,16 @@ class BlobFormatWriter : public FormatWriter {
7077
private:
7178
BlobFormatWriter(const std::shared_ptr<OutputStream>& out, const std::string& uri,
7279
const std::shared_ptr<arrow::DataType>& data_type,
80+
bool write_null_on_missing_file, bool write_null_on_fetch_failure,
7381
const std::shared_ptr<FileSystem>& fs,
7482
const std::shared_ptr<MemoryPool>& pool);
7583

7684
Status WriteBlob(std::string_view blob_data);
7785

86+
/// Deserialize the descriptor and open an input stream on the referenced data.
87+
Result<std::unique_ptr<InputStream>> OpenDescriptorInputStream(
88+
std::string_view blob_data) const;
89+
7890
Status WriteBytes(const char* data, int64_t length);
7991
Status WriteWithCrc32(const char* data, int64_t length);
8092

@@ -95,6 +107,9 @@ class BlobFormatWriter : public FormatWriter {
95107
std::shared_ptr<FileSystem> fs_;
96108
std::shared_ptr<MemoryPool> pool_;
97109
std::shared_ptr<Metrics> metrics_;
110+
bool write_null_on_missing_file_ = false;
111+
bool write_null_on_fetch_failure_ = false;
112+
std::unique_ptr<Logger> logger_;
98113
};
99114

100115
} // namespace paimon::blob

0 commit comments

Comments
 (0)