Skip to content

Commit 3d886ba

Browse files
authored
feat(blob view): support blob-view.resolve.enabled to preserve blob view references at read time (alibaba#414)
1 parent e7306f8 commit 3d886ba

7 files changed

Lines changed: 182 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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,6 +2500,166 @@ TEST_P(BlobTableInteTest, TestBlobViewFieldWithUpstreamTable) {
25002500
}
25012501
}
25022502

2503+
TEST_P(BlobTableInteTest, TestForwardBlobViewReference) {
2504+
auto file_format = GetParam();
2505+
if (file_format != "orc" && file_format != "parquet") {
2506+
return;
2507+
}
2508+
2509+
// Forward blob view references between two blob-view tables: read the source table with
2510+
// resolve dynamically disabled, write the preserved BlobViewStruct bytes into the target
2511+
// table, then verify the target still stores the original upstream references and a
2512+
// default read resolves them to the actual upstream blob values.
2513+
const std::string upstream_db_name = "append_table_with_multi_blob";
2514+
const std::string upstream_table_name = "append_table_with_multi_blob";
2515+
std::string src_db_path = paimon::test::GetDataDir() + file_format + "/" + upstream_db_name +
2516+
".db/" + upstream_table_name;
2517+
std::string dst_db_path =
2518+
PathUtil::JoinPath(dir_->Str(), upstream_db_name + ".db/" + upstream_table_name);
2519+
ASSERT_TRUE(TestUtil::CopyDirectory(src_db_path, dst_db_path));
2520+
2521+
// The source table has no upstream warehouse configured: only a read with resolve
2522+
// dynamically disabled can succeed on it.
2523+
arrow::FieldVector fields = {arrow::field("f0", arrow::int32()),
2524+
BlobUtils::ToArrowField("view", true)};
2525+
std::map<std::string, std::string> source_options = {{Options::MANIFEST_FORMAT, "orc"},
2526+
{Options::FILE_FORMAT, file_format},
2527+
{Options::BUCKET, "-1"},
2528+
{Options::ROW_TRACKING_ENABLED, "true"},
2529+
{Options::DATA_EVOLUTION_ENABLED, "true"},
2530+
{Options::BLOB_VIEW_FIELD, "view"},
2531+
{Options::FILE_SYSTEM, "local"}};
2532+
CreateTable(fields, /*partition_keys=*/{}, source_options);
2533+
std::string source_table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
2534+
2535+
// The target table configures the upstream warehouse for resolving forwarded references.
2536+
std::map<std::string, std::string> target_options = source_options;
2537+
target_options[Options::BLOB_VIEW_UPSTREAM_WAREHOUSE] = dir_->Str();
2538+
auto schema = arrow::schema(fields);
2539+
::ArrowSchema c_target_schema;
2540+
ASSERT_TRUE(arrow::ExportSchema(*schema, &c_target_schema).ok());
2541+
ASSERT_OK_AND_ASSIGN(auto catalog, Catalog::Create(dir_->Str(), {}));
2542+
ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar_forward"), &c_target_schema,
2543+
/*partition_keys=*/{}, /*primary_keys=*/{}, target_options,
2544+
/*ignore_if_exists=*/false));
2545+
std::string target_table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar_forward");
2546+
2547+
// src array
2548+
Identifier upstream_identifier(upstream_db_name, upstream_table_name);
2549+
arrow::LargeBinaryBuilder view_builder;
2550+
for (int32_t i = 0; i < 8; ++i) {
2551+
if (i < 6) {
2552+
BlobViewStruct view_struct(upstream_identifier, /*field_id=*/6,
2553+
/*row_id=*/static_cast<int64_t>(i));
2554+
auto serialized = view_struct.Serialize(pool_);
2555+
ASSERT_TRUE(view_builder
2556+
.Append(reinterpret_cast<const uint8_t*>(serialized->data()),
2557+
serialized->size())
2558+
.ok());
2559+
} else {
2560+
ASSERT_TRUE(view_builder.AppendNull().ok());
2561+
}
2562+
}
2563+
std::shared_ptr<arrow::Array> write_view_array;
2564+
ASSERT_TRUE(view_builder.Finish(&write_view_array).ok());
2565+
auto write_f0_array = arrow::ipc::internal::json::ArrayFromJSON(
2566+
arrow::int32(), R"([100,101,102,103,104,105,106,107])")
2567+
.ValueOrDie();
2568+
auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>(
2569+
arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, write_view_array}),
2570+
std::vector<std::string>({"f0", "view"}))
2571+
.ValueOrDie());
2572+
2573+
// write & commit into the source table
2574+
ASSERT_OK_AND_ASSIGN(auto commit_msgs,
2575+
WriteArray(source_table_path, {}, schema->field_names(), {write_struct}));
2576+
ASSERT_OK(Commit(source_table_path, commit_msgs));
2577+
2578+
// A default read fails on the missing upstream warehouse: the pass-through below is
2579+
// enabled by the dynamic option alone.
2580+
ASSERT_OK_AND_ASSIGN(auto source_plan, ScanTable(source_table_path));
2581+
ASSERT_NOK_WITH_MSG(
2582+
ReadTable(source_table_path, schema->field_names(), source_plan, /*predicate=*/nullptr),
2583+
"BLOB_VIEW_UPSTREAM_WAREHOUSE");
2584+
2585+
ASSERT_OK_AND_ASSIGN(
2586+
auto source_result,
2587+
ReadTable(source_table_path, schema->field_names(), source_plan, /*predicate=*/nullptr,
2588+
{{Options::BLOB_VIEW_RESOLVE_ENABLED, "false"}}));
2589+
ASSERT_TRUE(source_result.chunked_array);
2590+
auto source_concat = arrow::Concatenate(source_result.chunked_array->chunks()).ValueOrDie();
2591+
auto source_struct = std::dynamic_pointer_cast<arrow::StructArray>(source_concat);
2592+
ASSERT_EQ(source_struct->length(), 8);
2593+
auto forward_f0_array = source_struct->GetFieldByName("f0");
2594+
ASSERT_TRUE(forward_f0_array);
2595+
auto forward_view_array = source_struct->GetFieldByName("view");
2596+
ASSERT_TRUE(forward_view_array);
2597+
ASSERT_TRUE(forward_view_array->Equals(write_view_array))
2598+
<< "source view:" << forward_view_array->ToString() << std::endl
2599+
<< "written view:" << write_view_array->ToString();
2600+
2601+
// Forward the preserved references into the target blob-view table.
2602+
auto forward_struct = std::dynamic_pointer_cast<arrow::StructArray>(
2603+
arrow::StructArray::Make(arrow::ArrayVector({forward_f0_array, forward_view_array}),
2604+
std::vector<std::string>({"f0", "view"}))
2605+
.ValueOrDie());
2606+
ASSERT_OK_AND_ASSIGN(
2607+
auto forward_commit_msgs,
2608+
WriteArray(target_table_path, {}, schema->field_names(), {forward_struct}));
2609+
ASSERT_OK(Commit(target_table_path, forward_commit_msgs));
2610+
2611+
// The target table stores the original upstream references byte-identically.
2612+
ASSERT_OK_AND_ASSIGN(auto target_plan, ScanTable(target_table_path));
2613+
ASSERT_OK_AND_ASSIGN(
2614+
auto raw_target_result,
2615+
ReadTable(target_table_path, schema->field_names(), target_plan, /*predicate=*/nullptr,
2616+
{{Options::BLOB_VIEW_RESOLVE_ENABLED, "false"}}));
2617+
ASSERT_TRUE(raw_target_result.chunked_array);
2618+
auto raw_target_concat =
2619+
arrow::Concatenate(raw_target_result.chunked_array->chunks()).ValueOrDie();
2620+
auto raw_target_struct = std::dynamic_pointer_cast<arrow::StructArray>(raw_target_concat);
2621+
ASSERT_EQ(raw_target_struct->length(), 8);
2622+
auto raw_target_view_array = raw_target_struct->GetFieldByName("view");
2623+
ASSERT_TRUE(raw_target_view_array);
2624+
ASSERT_TRUE(raw_target_view_array->Equals(write_view_array))
2625+
<< "target view:" << raw_target_view_array->ToString() << std::endl
2626+
<< "written view:" << write_view_array->ToString();
2627+
2628+
// A default read of the target table resolves to the actual upstream blob values.
2629+
ASSERT_OK_AND_ASSIGN(auto resolved_result,
2630+
ReadTable(target_table_path, schema->field_names(), target_plan,
2631+
/*predicate=*/nullptr));
2632+
ASSERT_TRUE(resolved_result.chunked_array);
2633+
auto resolved_concat = arrow::Concatenate(resolved_result.chunked_array->chunks()).ValueOrDie();
2634+
auto resolved_struct = std::dynamic_pointer_cast<arrow::StructArray>(resolved_concat);
2635+
ASSERT_EQ(resolved_struct->length(), 8);
2636+
ASSERT_OK_AND_ASSIGN(auto resolved, ConvertDescriptorToRawBlob(resolved_struct, {"view"}));
2637+
2638+
std::string padding_b(2048, 'b');
2639+
std::string padding_d(2048, 'd');
2640+
std::string padding_e(2048, 'e');
2641+
std::string padding_f(2048, 'f');
2642+
// clang-format off
2643+
std::string expected_json = R"([
2644+
[100, null],
2645+
[101, ")" + padding_b + R"("],
2646+
[102, null],
2647+
[103, ")" + padding_d + R"("],
2648+
[104, ")" + padding_e + R"("],
2649+
[105, ")" + padding_f + R"("],
2650+
[106, null],
2651+
[107, null]
2652+
])";
2653+
// clang-format on
2654+
auto expected_struct = std::dynamic_pointer_cast<arrow::StructArray>(
2655+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), expected_json)
2656+
.ValueOrDie());
2657+
ASSERT_OK_AND_ASSIGN(auto expected_with_rk, PrependRowKindColumn(expected_struct));
2658+
ASSERT_TRUE(resolved->Equals(expected_with_rk))
2659+
<< "resolved:" << resolved->ToString() << std::endl
2660+
<< "expected:" << expected_with_rk->ToString();
2661+
}
2662+
25032663
TEST_P(BlobTableInteTest, TestBlobViewFieldWithUpstreamDescriptorBlob) {
25042664
auto file_format = GetParam();
25052665
if (GetParam() == "lance") {

0 commit comments

Comments
 (0)