Skip to content

Commit bbd270a

Browse files
SteNicholasclaude
andcommitted
feat(blob view): support blob-view.resolve.enabled to preserve blob view references at read time
Align with CoreOptions.BLOB_VIEW_RESOLVE_ENABLED in apache/paimon: the option defaults to true; when set to false the read passes the serialized BlobViewStruct bytes through without resolving them from upstream tables, skips the prescan/resolver entirely, and no longer requires blob-view-upstream-warehouse, so blob view values can be forwarded to another blob-view table. Closes alibaba#413 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e7306f8 commit bbd270a

7 files changed

Lines changed: 117 additions & 0 deletions

File tree

include/paimon/defs.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ struct PAIMON_EXPORT Options {
408408
/// serialized BlobViewStruct bytes inline in data files and resolve from upstream tables at
409409
/// read time. No default value.
410410
static const char BLOB_VIEW_FIELD[];
411+
/// "blob-view.resolve.enabled" - Whether to resolve blob-view-field values from upstream
412+
/// tables at read time. Set to false to preserve serialized BlobViewStruct bytes when
413+
/// forwarding blob view values to another blob-view table. Default value is "true".
414+
static const char BLOB_VIEW_RESOLVE_ENABLED[];
411415
/// "blob-view-upstream-warehouse" - Since the catalog capabilities are partially missing, when
412416
/// Blob View is enabled, cpp paimon cannot automatically obtain the upstream table warehouse
413417
/// path and requires manual configuration by the user. No default value.

src/paimon/common/defs.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const char Options::BLOB_FIELD[] = "blob-field";
102102
const char Options::BLOB_DESCRIPTOR_FIELD[] = "blob-descriptor-field";
103103
const char Options::FALLBACK_BLOB_DESCRIPTOR_FIELD[] = "blob.stored-descriptor-fields";
104104
const char Options::BLOB_VIEW_FIELD[] = "blob-view-field";
105+
const char Options::BLOB_VIEW_RESOLVE_ENABLED[] = "blob-view.resolve.enabled";
105106
const char Options::BLOB_VIEW_UPSTREAM_WAREHOUSE[] = "blob-view-upstream-warehouse";
106107
const char Options::GLOBAL_INDEX_ENABLED[] = "global-index.enabled";
107108
const char Options::GLOBAL_INDEX_THREAD_NUM[] = "global-index.thread-num";

src/paimon/core/core_options.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ struct CoreOptions::Impl {
442442
bool row_tracking_enabled = false;
443443
bool row_tracking_partition_group_on_commit = true;
444444
bool data_evolution_enabled = false;
445+
bool blob_view_resolve_enabled = true;
445446
bool legacy_partition_name_enabled = true;
446447
bool global_index_enabled = true;
447448
std::optional<int32_t> global_index_thread_num;
@@ -563,6 +564,9 @@ struct CoreOptions::Impl {
563564
// Parse blob-view-upstream-warehouse - warehouse path for configured blob view fields
564565
PAIMON_RETURN_NOT_OK(
565566
parser.Parse(Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, &blob_view_upstream_warehouse));
567+
// Parse blob-view.resolve.enabled - whether to resolve blob view fields at read time
568+
PAIMON_RETURN_NOT_OK(
569+
parser.Parse<bool>(Options::BLOB_VIEW_RESOLVE_ENABLED, &blob_view_resolve_enabled));
566570
return Status::OK();
567571
}
568572

@@ -1489,6 +1493,10 @@ std::optional<std::string> CoreOptions::GetBlobViewUpstreamWarehouse() const {
14891493
return impl_->blob_view_upstream_warehouse;
14901494
}
14911495

1496+
bool CoreOptions::BlobViewResolveEnabled() const {
1497+
return impl_->blob_view_resolve_enabled;
1498+
}
1499+
14921500
std::vector<std::string> CoreOptions::GetBlobInlineFields() const {
14931501
std::vector<std::string> blob_inline_fields = impl_->blob_descriptor_fields;
14941502
blob_inline_fields.insert(blob_inline_fields.end(), impl_->blob_view_fields.begin(),

src/paimon/core/core_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ class PAIMON_EXPORT CoreOptions {
197197
const std::vector<std::string>& GetBlobDescriptorFields() const;
198198
const std::vector<std::string>& GetBlobViewFields() const;
199199
std::optional<std::string> GetBlobViewUpstreamWarehouse() const;
200+
bool BlobViewResolveEnabled() const;
200201
std::vector<std::string> GetBlobInlineFields() const;
201202

202203
const std::map<std::string, std::string>& ToMap() const;

src/paimon/core/core_options_test.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ TEST(CoreOptionsTest, TestDefaultValue) {
124124
ASSERT_TRUE(core_options.GetBlobViewFields().empty());
125125
ASSERT_TRUE(core_options.GetBlobInlineFields().empty());
126126
ASSERT_EQ(std::nullopt, core_options.GetBlobViewUpstreamWarehouse());
127+
ASSERT_TRUE(core_options.BlobViewResolveEnabled());
127128
ASSERT_TRUE(core_options.LegacyPartitionNameEnabled());
128129
ASSERT_TRUE(core_options.GlobalIndexEnabled());
129130
ASSERT_EQ(std::nullopt, core_options.GetGlobalIndexExternalPath());
@@ -228,6 +229,7 @@ TEST(CoreOptionsTest, TestFromMap) {
228229
{Options::BLOB_DESCRIPTOR_FIELD, "blob3,blob4"},
229230
{Options::BLOB_VIEW_FIELD, "blob5"},
230231
{Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, "FILE:///tmp/blob_view_upstream_warehouse/"},
232+
{Options::BLOB_VIEW_RESOLVE_ENABLED, "false"},
231233
{Options::PARTITION_GENERATE_LEGACY_NAME, "false"},
232234
{Options::GLOBAL_INDEX_ENABLED, "false"},
233235
{Options::GLOBAL_INDEX_THREAD_NUM, "4"},
@@ -368,6 +370,7 @@ TEST(CoreOptionsTest, TestFromMap) {
368370
std::vector<std::string>({"blob3", "blob4", "blob5"}));
369371
ASSERT_EQ(core_options.GetBlobViewUpstreamWarehouse(),
370372
std::optional<std::string>("FILE:///tmp/blob_view_upstream_warehouse/"));
373+
ASSERT_FALSE(core_options.BlobViewResolveEnabled());
371374
ASSERT_FALSE(core_options.LegacyPartitionNameEnabled());
372375
ASSERT_FALSE(core_options.GlobalIndexEnabled());
373376
ASSERT_EQ(core_options.GetGlobalIndexThreadNum(), 4);

src/paimon/core/operation/data_evolution_split_read.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ Result<std::unique_ptr<BatchReader>> DataEvolutionSplitRead::CreateReader(
167167
Result<std::unique_ptr<BatchReader>> DataEvolutionSplitRead::WrapWithBlobViewResolverIfNeeded(
168168
const std::shared_ptr<DataSplit>& data_split,
169169
std::unique_ptr<BatchReader>&& inner_reader) const {
170+
if (!options_.BlobViewResolveEnabled()) {
171+
// preserve serialized BlobViewStruct bytes, e.g. for forwarding blob view values to
172+
// another blob-view table
173+
return std::move(inner_reader);
174+
}
170175
std::vector<std::string> read_blob_view_fields = HasBlobViewField(options_, raw_read_schema_);
171176
if (read_blob_view_fields.empty()) {
172177
return std::move(inner_reader);

test/inte/blob_table_inte_test.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,6 +2500,101 @@ TEST_P(BlobTableInteTest, TestBlobViewFieldWithUpstreamTable) {
25002500
}
25012501
}
25022502

2503+
TEST_P(BlobTableInteTest, TestBlobViewFieldResolveDisabled) {
2504+
auto file_format = GetParam();
2505+
if (file_format != "orc" && file_format != "parquet") {
2506+
return;
2507+
}
2508+
2509+
// With blob-view.resolve.enabled=false the read must pass the serialized BlobViewStruct
2510+
// bytes through without touching the upstream side: the referenced upstream table
2511+
// deliberately does not exist and no upstream warehouse is configured.
2512+
arrow::FieldVector fields = {arrow::field("f0", arrow::int32()),
2513+
BlobUtils::ToArrowField("view", true)};
2514+
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
2515+
{Options::FILE_FORMAT, file_format},
2516+
{Options::BUCKET, "-1"},
2517+
{Options::ROW_TRACKING_ENABLED, "true"},
2518+
{Options::DATA_EVOLUTION_ENABLED, "true"},
2519+
{Options::BLOB_VIEW_FIELD, "view"},
2520+
{Options::BLOB_VIEW_RESOLVE_ENABLED, "false"},
2521+
{Options::FILE_SYSTEM, "local"}};
2522+
CreateTable(fields, /*partition_keys=*/{}, options);
2523+
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
2524+
2525+
Identifier upstream_identifier("upstream_db", "upstream_table");
2526+
arrow::LargeBinaryBuilder view_builder;
2527+
for (int32_t i = 0; i < 4; ++i) {
2528+
if (i == 2) {
2529+
ASSERT_TRUE(view_builder.AppendNull().ok());
2530+
continue;
2531+
}
2532+
BlobViewStruct view_struct(upstream_identifier, /*field_id=*/6,
2533+
/*row_id=*/static_cast<int64_t>(i));
2534+
auto serialized = view_struct.Serialize(pool_);
2535+
ASSERT_TRUE(
2536+
view_builder
2537+
.Append(reinterpret_cast<const uint8_t*>(serialized->data()), serialized->size())
2538+
.ok());
2539+
}
2540+
std::shared_ptr<arrow::Array> write_view_array;
2541+
ASSERT_TRUE(view_builder.Finish(&write_view_array).ok());
2542+
auto write_f0_array =
2543+
arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([100,101,102,103])")
2544+
.ValueOrDie();
2545+
auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>(
2546+
arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, write_view_array}),
2547+
std::vector<std::string>({"f0", "view"}))
2548+
.ValueOrDie());
2549+
2550+
// write & commit
2551+
auto schema = arrow::schema(fields);
2552+
ASSERT_OK_AND_ASSIGN(auto commit_msgs,
2553+
WriteArray(table_path, {}, schema->field_names(), {write_struct}));
2554+
ASSERT_OK(Commit(table_path, commit_msgs));
2555+
2556+
// scan & read
2557+
ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path));
2558+
ASSERT_OK_AND_ASSIGN(auto result,
2559+
ReadTable(table_path, schema->field_names(), plan, /*predicate=*/nullptr));
2560+
ASSERT_TRUE(result.chunked_array);
2561+
auto read_concat = arrow::Concatenate(result.chunked_array->chunks()).ValueOrDie();
2562+
auto read_struct = std::dynamic_pointer_cast<arrow::StructArray>(read_concat);
2563+
ASSERT_EQ(read_struct->length(), 4);
2564+
2565+
auto read_f0_array = read_struct->GetFieldByName("f0");
2566+
ASSERT_TRUE(read_f0_array);
2567+
ASSERT_TRUE(read_f0_array->Equals(write_f0_array))
2568+
<< "read f0:" << read_f0_array->ToString() << std::endl
2569+
<< "written f0:" << write_f0_array->ToString();
2570+
auto read_view_array = read_struct->GetFieldByName("view");
2571+
ASSERT_TRUE(read_view_array);
2572+
ASSERT_TRUE(read_view_array->Equals(write_view_array))
2573+
<< "read view:" << read_view_array->ToString() << std::endl
2574+
<< "written view:" << write_view_array->ToString();
2575+
2576+
// The pass-through bytes still deserialize to the original view struct, so they can be
2577+
// forwarded to another blob-view table.
2578+
auto typed_view_array = std::dynamic_pointer_cast<arrow::LargeBinaryArray>(read_view_array);
2579+
ASSERT_TRUE(typed_view_array);
2580+
std::string_view first_view = typed_view_array->GetView(0);
2581+
ASSERT_OK_AND_ASSIGN(bool is_view_struct,
2582+
BlobViewStruct::IsBlobViewStruct(first_view.data(), first_view.size()));
2583+
ASSERT_TRUE(is_view_struct);
2584+
ASSERT_OK_AND_ASSIGN(auto view_struct,
2585+
BlobViewStruct::Deserialize(first_view.data(), first_view.size()));
2586+
ASSERT_TRUE(view_struct->GetIdentifier() == upstream_identifier);
2587+
ASSERT_EQ(view_struct->FieldId(), 6);
2588+
ASSERT_EQ(view_struct->RowId(), 0);
2589+
2590+
// Re-enabling resolve makes the same read fail on the missing upstream warehouse.
2591+
// Without this check the assertions above would also pass if resolution were never
2592+
// triggered at all.
2593+
ASSERT_NOK_WITH_MSG(ReadTable(table_path, schema->field_names(), plan, /*predicate=*/nullptr,
2594+
{{Options::BLOB_VIEW_RESOLVE_ENABLED, "true"}}),
2595+
"BLOB_VIEW_UPSTREAM_WAREHOUSE");
2596+
}
2597+
25032598
TEST_P(BlobTableInteTest, TestBlobViewFieldWithUpstreamDescriptorBlob) {
25042599
auto file_format = GetParam();
25052600
if (GetParam() == "lance") {

0 commit comments

Comments
 (0)