@@ -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+
25032663TEST_P (BlobTableInteTest, TestBlobViewFieldWithUpstreamDescriptorBlob) {
25042664 auto file_format = GetParam ();
25052665 if (GetParam () == " lance" ) {
0 commit comments