This is an automated email from the ASF dual-hosted git repository.

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 7c0af00e feat(blob): read blob-view dependency tables through a 
catalog (#427)
7c0af00e is described below

commit 7c0af00eeac8ce5e543e1ccd6a6d08cfa6374ca7
Author: Yonghao Fang <[email protected]>
AuthorDate: Tue Oct 13 15:36:20 2026 +0800

    feat(blob): read blob-view dependency tables through a catalog (#427)
---
 include/paimon/defs.h                              |   8 +-
 src/paimon/common/catalog/catalog_context.h        |  21 +++-
 .../core/operation/data_evolution_split_read.cpp   |  15 ++-
 src/paimon/core/operation/internal_read_context.h  |   5 +
 .../core/operation/internal_read_context_test.cpp  |  40 ++++++
 src/paimon/core/operation/read_context.cpp         |  74 ++++++++++-
 src/paimon/core/operation/read_context_test.cpp    | 113 +++++++++++++++++
 src/paimon/core/utils/blob_view_lookup.cpp         |  40 ++----
 src/paimon/core/utils/blob_view_lookup.h           |   4 +-
 src/paimon/rest/rest_api.h                         |   5 +
 src/paimon/rest/rest_catalog.h                     |   2 +
 src/paimon/rest/rest_credential_provider.cpp       |   9 +-
 src/paimon/rest/rest_credential_provider.h         |   7 +-
 src/paimon/testing/mock/mock_catalog.h             |   5 +
 test/inte/blob_table_inte_test.cpp                 | 140 +++------------------
 15 files changed, 312 insertions(+), 176 deletions(-)

diff --git a/include/paimon/defs.h b/include/paimon/defs.h
index 7b473240..8c88c3de 100644
--- a/include/paimon/defs.h
+++ b/include/paimon/defs.h
@@ -568,9 +568,11 @@ struct PAIMON_EXPORT Options {
     /// tables at read time. Set to false to preserve serialized 
BlobViewStruct bytes when
     /// forwarding blob view values to another blob-view table. Default value 
is "true".
     static const char BLOB_VIEW_RESOLVE_ENABLED[];
-    /// "blob-view-upstream-warehouse" - Since the catalog capabilities are 
partially missing, when
-    /// Blob View is enabled, cpp paimon cannot automatically obtain the 
upstream table warehouse
-    /// path and requires manual configuration by the user. No default value.
+    /// "blob-view-upstream-warehouse" - Warehouse root the upstream table a 
blob-view field points
+    /// at is resolved from. Since the catalog capabilities are partially 
missing, cpp paimon cannot
+    /// always obtain it automatically, so it may be configured explicitly. 
When it is not set, the
+    /// read falls back to the warehouse the downstream table itself lives in, 
which resolves a
+    /// co-located upstream table. No default value.
     static const char BLOB_VIEW_UPSTREAM_WAREHOUSE[];
     /// "blob-write-null-on-missing-file" - Whether to write NULL for a 
descriptor BLOB value,
     /// including an ARRAY<BLOB> element or a MAP<..., BLOB> value, when the 
referenced file does
diff --git a/src/paimon/common/catalog/catalog_context.h 
b/src/paimon/common/catalog/catalog_context.h
index fecc3e01..7a9f8b87 100644
--- a/src/paimon/common/catalog/catalog_context.h
+++ b/src/paimon/common/catalog/catalog_context.h
@@ -21,18 +21,29 @@
 #include <map>
 #include <string>
 
-#include "paimon/fs/file_system.h"
-
 namespace paimon {
+
+/// What a dependency table - the blob table behind a BlobView - is read 
through. Like the Java
+/// `CatalogContext` that `CatalogEnvironment.dependencyReadContext()` 
returns, it carries no file
+/// system. The catalog built from these options issues credentials for the 
table about to be read,
+/// whereas a file system handed down from the read above authenticates as a 
different table - and
+/// the `X-Paimon-Read-Via` header travelling with it cannot excuse that, 
being request context a
+/// server must not treat as authorization proof. A caller supplying its own 
file system therefore
+/// gets no blob view delegation.
 struct CatalogContext {
     CatalogContext(const std::string& _root_path,
                    const std::map<std::string, std::string>& _options,
-                   const std::shared_ptr<FileSystem>& _file_system)
-        : root_path(_root_path), options(_options), file_system(_file_system) 
{}
+                   const std::map<std::string, std::string>& 
_fs_scheme_to_identifier_map)
+        : root_path(_root_path),
+          options(_options),
+          fs_scheme_to_identifier_map(_fs_scheme_to_identifier_map) {}
 
     std::string root_path;
     std::map<std::string, std::string> options;
-    std::shared_ptr<FileSystem> file_system;
+    /// Maps a URI scheme to the registered file system serving it, the map 
the file system of the
+    /// read this descends from was resolved with. The catalog builds its own 
file system out of
+    /// these options and needs the map to route a scheme that is not their 
default.
+    std::map<std::string, std::string> fs_scheme_to_identifier_map;
 };
 
 }  // namespace paimon
diff --git a/src/paimon/core/operation/data_evolution_split_read.cpp 
b/src/paimon/core/operation/data_evolution_split_read.cpp
index 5c26d341..7491ad3e 100644
--- a/src/paimon/core/operation/data_evolution_split_read.cpp
+++ b/src/paimon/core/operation/data_evolution_split_read.cpp
@@ -280,17 +280,26 @@ Result<std::unique_ptr<BatchReader>> 
DataEvolutionSplitRead::WrapWithBlobViewRes
     if (read_blob_view_fields.empty()) {
         return std::move(inner_reader);
     }
+    // The explicit upstream warehouse is optional. Absent it, fall back to 
the warehouse this
+    // (downstream) table itself lives in, which is where a co-located 
upstream table is resolved
+    // from; a metastore-backed catalog resolves the upstream location by 
identifier anyway, so the
+    // root is only a hint there. Stripping the trailing 
`<database>.db/<table>` (or
+    // `<database>/<table>`) components off this table's path yields that 
warehouse.
     std::optional<std::string> warehouse_path = 
options_.GetBlobViewUpstreamWarehouse();
     if (!warehouse_path) {
-        return Status::Invalid(
-            "invalid config for blob view, supposed to set 
BLOB_VIEW_UPSTREAM_WAREHOUSE");
+        // TODO(yonghao.fyh): refactor warehouse path in the future
+        warehouse_path =
+            
PathUtil::GetParentDirPath(PathUtil::GetParentDirPath(context_->GetPath()));
     }
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<BatchReader> pre_reader,
                            CreateBlobViewReader(data_split, 
read_blob_view_fields, row_ranges));
     PAIMON_ASSIGN_OR_RAISE(std::unordered_set<BlobViewStruct> 
blob_view_structs,
                            ExtractBlobViewStructs(pre_reader.get()));
+    // The dependency table is read through a catalog of its own, told how to 
resolve a scheme but
+    // not handed this read's file system: that one authenticates as this 
table, and the table about
+    // to be read is a different one whose credentials the metastore issues 
only for it.
     auto catalog_context = std::make_shared<CatalogContext>(
-        warehouse_path.value(), options_.ToMap(), options_.GetFileSystem());
+        warehouse_path.value(), options_.ToMap(), 
context_->GetFileSystemSchemeToIdentifierMap());
     // use global thread number
     uint32_t cpu_count = std::thread::hardware_concurrency();
     uint32_t thread_num = cpu_count > 0 ? cpu_count : 1;
diff --git a/src/paimon/core/operation/internal_read_context.h 
b/src/paimon/core/operation/internal_read_context.h
index e3c2326f..8e34a85b 100644
--- a/src/paimon/core/operation/internal_read_context.h
+++ b/src/paimon/core/operation/internal_read_context.h
@@ -111,6 +111,11 @@ class InternalReadContext {
     std::shared_ptr<RealtimeContext> GetRealtimeContext() const {
         return read_context_->GetRealtimeContext();
     }
+    /// The scheme map the file system of this read was resolved with, which a 
catalog reading a
+    /// dependency table on its behalf needs to resolve that table's location 
the same way.
+    const std::map<std::string, std::string>& 
GetFileSystemSchemeToIdentifierMap() const {
+        return read_context_->GetFileSystemSchemeToIdentifierMap();
+    }
 
     bool ReadAheadCacheEnabled() const {
         return read_context_->ReadAheadCacheEnabled();
diff --git a/src/paimon/core/operation/internal_read_context_test.cpp 
b/src/paimon/core/operation/internal_read_context_test.cpp
index 9e0dc5fb..086d56d1 100644
--- a/src/paimon/core/operation/internal_read_context_test.cpp
+++ b/src/paimon/core/operation/internal_read_context_test.cpp
@@ -75,6 +75,46 @@ TEST(InternalReadContext, TestReadWithSpecifiedSchema) {
     ASSERT_TRUE(internal_context->GetReadSchema()->Equals(expected_schema));
 }
 
+TEST(InternalReadContext, TestHeaderOptionSurvivesIntoCoreOptions) {
+    // The `header.` option a read through a REST catalog reports its starting 
table with has to
+    // still be among the options a dependency table is read through: 
`DataEvolutionSplitRead`
+    // builds that catalog context out of `CoreOptions::ToMap()`, so an option 
dropped on the way
+    // here would quietly drop the `X-Paimon-Read-Via` delegation header along 
with it.
+    std::string path = paimon::test::GetDataDir() + 
"/orc/append_09.db/append_09";
+    ReadContextBuilder context_builder(path);
+    ASSERT_OK_AND_ASSIGN(auto read_context, context_builder.Finish());
+    SchemaManager schema_manager(std::make_shared<LocalFileSystem>(), 
read_context->GetPath());
+    ASSERT_OK_AND_ASSIGN(auto table_schema, schema_manager.ReadSchema(0));
+
+    // The merge `TableRead::Create` performs: the table's own options first, 
then the ones the read
+    // context carries over them, which is where a header option arrives from.
+    const std::string read_via = 
"%7B%22database%22%3A%22db1%22%2C%22object%22%3A%22t1%22%7D";
+    std::map<std::string, std::string> options = table_schema->Options();
+    options["header.X-Paimon-Read-Via"] = read_via;
+    ASSERT_OK_AND_ASSIGN(auto internal_context, 
InternalReadContext::Create(std::move(read_context),
+                                                                            
table_schema, options));
+    
ASSERT_EQ(internal_context->GetCoreOptions().ToMap().at("header.X-Paimon-Read-Via"),
 read_via);
+}
+
+TEST(InternalReadContext, TestFileSystemSchemeMapReachesTheDependencyRead) {
+    // A blob view reads its dependency table through a catalog of its own, 
which builds a file
+    // system out of the options and this map rather than borrowing the one of 
the read it descends
+    // from. Nothing reading a local path would notice the map going missing: 
a location is
+    // resolved through it only when its scheme is not the default of the 
options, so dropping it
+    // surfaces as a `dfs://` dependency table that cannot be opened, far away 
from the cause.
+    std::string path = paimon::test::GetDataDir() + 
"/orc/append_09.db/append_09";
+    const std::map<std::string, std::string> scheme_map = {{"file", "local"}};
+    ReadContextBuilder context_builder(path);
+    context_builder.WithFileSystemSchemeToIdentifierMap(scheme_map);
+    ASSERT_OK_AND_ASSIGN(auto read_context, context_builder.Finish());
+    SchemaManager schema_manager(std::make_shared<LocalFileSystem>(), 
read_context->GetPath());
+    ASSERT_OK_AND_ASSIGN(auto table_schema, schema_manager.ReadSchema(0));
+    ASSERT_OK_AND_ASSIGN(auto internal_context,
+                         InternalReadContext::Create(std::move(read_context), 
table_schema,
+                                                     table_schema->Options()));
+    ASSERT_EQ(internal_context->GetFileSystemSchemeToIdentifierMap(), 
scheme_map);
+}
+
 TEST(InternalReadContext, TestReadWithSpecifiedFieldId) {
     std::string path = paimon::test::GetDataDir() + 
"/orc/append_09.db/append_09";
     ReadContextBuilder context_builder(path);
diff --git a/src/paimon/core/operation/read_context.cpp 
b/src/paimon/core/operation/read_context.cpp
index 5cbbb5db..1779d730 100644
--- a/src/paimon/core/operation/read_context.cpp
+++ b/src/paimon/core/operation/read_context.cpp
@@ -18,23 +18,73 @@
 
 #include "paimon/read_context.h"
 
+#include <map>
+#include <string>
 #include <utility>
 
 #include "arrow/c/abi.h"
 #include "arrow/c/bridge.h"
 #include "paimon/catalog/catalog.h"
+#include "paimon/catalog_options.h"
 #include "paimon/common/utils/path_util.h"
+#include "paimon/common/utils/string_utils.h"
+#include "paimon/common/utils/url_utils.h"
 #include "paimon/core/utils/branch_manager.h"
 #include "paimon/executor.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/predicate/full_text_search.h"
 #include "paimon/predicate/vector_search.h"
+#include "paimon/rest/rest_api.h"
 #include "paimon/status.h"
 #include "paimon/table/format/format_table.h"
+#include "rapidjson/document.h"
+#include "rapidjson/stringbuffer.h"
+#include "rapidjson/writer.h"
 
 namespace paimon {
 class Predicate;
 
+namespace {
+
+/// True when the catalog-level options - the ones the catalog was created 
with, not the read's own
+/// table options - select the REST protocol, the only kind turning a 
`header.`-prefixed option into
+/// a request header. `Catalog::Create` dispatches on this same `metastore` 
key, so a catalog that
+/// speaks REST is exactly one whose `GetOptions()` names it. The Java 
counterpart answers this from
+/// the catalog loader / `catalogContext.options()` it was built with.
+bool IsRestMetastore(const std::map<std::string, std::string>& 
catalog_options) {
+    auto metastore = catalog_options.find(CatalogOptions::METASTORE);
+    if (metastore == catalog_options.end()) {
+        return false;
+    }
+    // Matched leniently in lower case, the way `Catalog::Create` matches it.
+    const std::string name = StringUtils::ToLowerCase(metastore->second);
+    return name == "rest";
+}
+
+/// `{"database":"db1","object":"t1"}`, the flat JSON the metastore parses a 
header value back into.
+/// The field names are the Java `Identifier.FIELD_DATABASE_NAME` and 
`FIELD_OBJECT_NAME`, and the
+/// object name keeps any branch and system-table suffix, as the 
`getObjectName()` the Java
+/// serializer reads does, so a read of a branch reports that branch.
+std::string IdentifierToFlatJson(const Identifier& identifier) {
+    rapidjson::Document document(rapidjson::kObjectType);
+    auto& allocator = document.GetAllocator();
+    document.AddMember(rapidjson::StringRef("database"),
+                       rapidjson::Value(identifier.GetDatabaseName(), 
allocator), allocator);
+    // C++ `GetTableName()` returns the stored object name - branch and 
system-table suffix
+    // included - which is the Java `getObjectName()` this field mirrors, not 
Java's stripped
+    // `getTableName()` (that one is `GetDataTableName()` here). So a branch 
read reports the
+    // branch.
+    document.AddMember(rapidjson::StringRef("object"),
+                       rapidjson::Value(identifier.GetTableName(), allocator), 
allocator);
+    // Compact, not the pretty form `RapidJsonUtil::ToJson` writes: the value 
goes into a header.
+    rapidjson::StringBuffer buffer;
+    rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+    document.Accept(writer);
+    return buffer.GetString();
+}
+
+}  // namespace
+
 ReadContext::ReadContext(
     const std::string& path, const std::string& branch,
     const std::vector<std::string>& read_field_names, const 
std::vector<int32_t>& read_field_ids,
@@ -377,9 +427,9 @@ Result<std::unique_ptr<ReadContext>> 
ReadContextBuilder::Finish() {
         PAIMON_ASSIGN_OR_RAISE(impl_->specific_file_system_,
                                
impl_->catalog_->GetTableFileSystem(impl_->identifier_.value()));
     }
-    PAIMON_ASSIGN_OR_RAISE(std::string branch,
-                           
BranchManager::ResolveBranch(/*identifier=*/std::nullopt,
-                                                        impl_->options_, 
impl_->branch_, "read"));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::string branch,
+        BranchManager::ResolveBranch(impl_->identifier_, impl_->options_, 
impl_->branch_, "read"));
     if (impl_->enable_prefetch_ && impl_->prefetch_max_parallel_num_ == 0) {
         return Status::Invalid("prefetch max parallel num should be greater 
than 0");
     }
@@ -404,6 +454,24 @@ Result<std::unique_ptr<ReadContext>> 
ReadContextBuilder::Finish() {
     if (impl_->enable_multi_thread_row_to_batch_ && 
impl_->row_to_batch_thread_number_ <= 0) {
         return Status::Invalid("row to batch thread number should be greater 
than 0");
     }
+
+    const std::string read_via_option =
+        std::string(RestApi::kHeaderOptionPrefix) + RestApi::kReadViaHeader;
+    if (impl_->catalog_ != nullptr && impl_->identifier_ &&
+        IsRestMetastore(impl_->catalog_->GetOptions())) {
+        // A blob-view dependency read (BlobViewLookup) rebuilds its own 
catalog from a
+        // CatalogContext whose options come from these read options, so they 
carry none of the
+        // catalog-level connection settings. Hand over the ones 
RestApi::Create needs to come back
+        // as the same REST catalog - metastore, uri and the auth options - 
instead of letting it
+        // silently degrade to a filesystem catalog; a key the caller already 
set wins.
+        for (const auto& [key, value] : impl_->catalog_->GetOptions()) {
+            impl_->options_.emplace(key, value);
+        }
+        // Reports the outermost starting table; an existing value, set by an 
outer read, is kept.
+        impl_->options_.emplace(
+            read_via_option,
+            
UrlUtils::EncodeString(IdentifierToFlatJson(impl_->identifier_.value())));
+    }
     auto ctx = std::make_unique<ReadContext>(
         impl_->path_, branch, impl_->read_field_names_, 
impl_->read_field_ids_, impl_->predicate_,
         impl_->vector_search_, impl_->full_text_search_, 
impl_->enable_predicate_filter_,
diff --git a/src/paimon/core/operation/read_context_test.cpp 
b/src/paimon/core/operation/read_context_test.cpp
index b820d245..176df8b0 100644
--- a/src/paimon/core/operation/read_context_test.cpp
+++ b/src/paimon/core/operation/read_context_test.cpp
@@ -23,6 +23,7 @@
 #include "arrow/c/bridge.h"
 #include "arrow/type.h"
 #include "gtest/gtest.h"
+#include "paimon/catalog_options.h"
 #include "paimon/common/io/cache/lru_cache.h"
 #include "paimon/core/utils/branch_manager.h"
 #include "paimon/defs.h"
@@ -77,6 +78,93 @@ TEST(ReadContextTest, TestWithCatalogNullRejected) {
                         "cannot read through a null catalog");
 }
 
+TEST(ReadContextTest, TestReadViaHeaderReportsTheStartingTable) {
+    // A read through a REST catalog carries the table it started from, so 
that the metastore can
+    // authorize a dependency table - the blob table behind a BlobView - 
through that table's owner.
+    // The value is the identifier's flat JSON in the form-urlencoded flavor 
the REST server
+    // expects, byte for byte what the Java client sends.
+    const std::string expected = 
"%7B%22database%22%3A%22db1%22%2C%22object%22%3A%22t1%22%7D";
+    for (const std::string metastore : {"rest"}) {
+        auto catalog = std::make_shared<MockVersionManagedCatalog>();
+        catalog->SetTableFileSystem(std::make_shared<MockFileSystem>());
+        catalog->SetOptions({{CatalogOptions::METASTORE, metastore}});
+
+        ReadContextBuilder builder("table_root_path");
+        ASSERT_OK_AND_ASSIGN(auto ctx,
+                             builder.WithCatalog(catalog, Identifier("db1", 
"t1")).Finish());
+
+        const std::map<std::string, std::string>& options = ctx->GetOptions();
+        ASSERT_EQ(options.count("header.X-Paimon-Read-Via"), 1u) << metastore;
+        ASSERT_EQ(options.at("header.X-Paimon-Read-Via"), expected) << 
metastore;
+        // The catalog-level connection options are handed to the read options 
so the blob-view
+        // dependency catalog can be rebuilt as the same REST catalog; here 
that is the metastore.
+        ASSERT_EQ(options.count(CatalogOptions::METASTORE), 1u) << metastore;
+        ASSERT_EQ(options.at(CatalogOptions::METASTORE), metastore) << 
metastore;
+        ASSERT_EQ(options.size(), 2u) << metastore;
+    }
+}
+
+TEST(ReadContextTest, TestReadViaHeaderKeepsTheOutermostView) {
+    // A view read on behalf of another view arrives with the header already 
set, and the outermost
+    // view is the one the metastore hears about: the option in hand wins, as 
it does in the Java
+    // `CatalogEnvironment.dependencyReadContext()`.
+    auto catalog = std::make_shared<MockVersionManagedCatalog>();
+    catalog->SetTableFileSystem(std::make_shared<MockFileSystem>());
+    catalog->SetOptions({{CatalogOptions::METASTORE, "rest"}});
+
+    ReadContextBuilder builder("table_root_path");
+    builder.AddOption("header.X-Paimon-Read-Via", "outer-view");
+    ASSERT_OK_AND_ASSIGN(auto ctx, builder.WithCatalog(catalog, 
Identifier("db1", "t1")).Finish());
+    ASSERT_EQ(ctx->GetOptions().at("header.X-Paimon-Read-Via"), "outer-view");
+}
+
+TEST(ReadContextTest, TestReadViaHeaderNeedsARestCatalogAndATable) {
+    // Only a REST metastore turns a `header.` option into a request header, 
and only a read naming
+    // a table has one to report: the two guards the Java counterpart checks 
before copying the
+    // options over. A catalog that is not one is told nothing.
+    for (const std::string metastore : {"filesystem", "hive", "alake"}) {
+        auto catalog = std::make_shared<MockVersionManagedCatalog>();
+        catalog->SetTableFileSystem(std::make_shared<MockFileSystem>());
+        catalog->SetOptions({{CatalogOptions::METASTORE, metastore}});
+
+        ReadContextBuilder builder("table_root_path");
+        ASSERT_OK_AND_ASSIGN(auto ctx,
+                             builder.WithCatalog(catalog, Identifier("db1", 
"t1")).Finish());
+        ASSERT_EQ(ctx->GetOptions().count("header.X-Paimon-Read-Via"), 0u) << 
metastore;
+    }
+
+    // Nor is one added when the options leave the metastore out, which reads 
as a filesystem
+    // catalog, or when the read goes through no catalog and so names no table 
- however the
+    // metastore is spelled, since `Catalog::Create` matches it in lower case.
+    auto catalog = std::make_shared<MockVersionManagedCatalog>();
+    catalog->SetTableFileSystem(std::make_shared<MockFileSystem>());
+    ReadContextBuilder no_metastore_builder("table_root_path");
+    ASSERT_OK_AND_ASSIGN(
+        auto no_metastore_ctx,
+        no_metastore_builder.WithCatalog(catalog, Identifier("db1", 
"t1")).Finish());
+    
ASSERT_EQ(no_metastore_ctx->GetOptions().count("header.X-Paimon-Read-Via"), 0u);
+
+    ReadContextBuilder no_catalog_builder("table_root_path");
+    no_catalog_builder.AddOption(CatalogOptions::METASTORE, "REST");
+    ASSERT_OK_AND_ASSIGN(auto no_catalog_ctx, no_catalog_builder.Finish());
+    ASSERT_EQ(no_catalog_ctx->GetOptions().count("header.X-Paimon-Read-Via"), 
0u);
+}
+
+TEST(ReadContextTest, TestReadViaHeaderReportsTheBranchOfTheStartingTable) {
+    // What is reported is the object name, the branch suffix included, 
because that is what the
+    // Java `getObjectName()` its serializer reads returns: a read of a branch 
says so instead of
+    // naming the data table. The metastore is matched in upper case here as 
well.
+    auto catalog = std::make_shared<MockVersionManagedCatalog>();
+    catalog->SetTableFileSystem(std::make_shared<MockFileSystem>());
+    catalog->SetOptions({{CatalogOptions::METASTORE, "REST"}});
+
+    ReadContextBuilder builder("table_root_path");
+    ASSERT_OK_AND_ASSIGN(auto ctx,
+                         builder.WithCatalog(catalog, Identifier("db1", "t1", 
"dev")).Finish());
+    ASSERT_EQ(ctx->GetOptions().at("header.X-Paimon-Read-Via"),
+              
"%7B%22database%22%3A%22db1%22%2C%22object%22%3A%22t1%24branch_dev%22%7D");
+}
+
 TEST(ReadContextTest, TestDefaultValue) {
     ReadContextBuilder builder("table_root_path");
     ASSERT_OK_AND_ASSIGN(auto ctx, builder.Finish());
@@ -225,6 +313,31 @@ TEST(ReadContextTest, TestBranch) {
     ASSERT_NOK_WITH_MSG(escaping_builder.Finish(), "branch name cannot contain 
path separators");
 }
 
+TEST(ReadContextTest, TestBranchAgreesWithIdentifier) {
+    // A read through a catalog names its table with an Identifier that can 
itself carry a branch,
+    // so that branch and the one WithBranch() names must agree, exactly as 
the write and commit
+    // builders already require: one read cannot be aimed at two branches at 
once.
+    auto catalog = std::make_shared<MockVersionManagedCatalog>();
+    catalog->SetTableFileSystem(std::make_shared<MockFileSystem>());
+
+    // The identifier's branch is honored when WithBranch() stays silent.
+    ReadContextBuilder identifier_builder("table_root_path");
+    identifier_builder.WithCatalog(catalog, Identifier("db1", "t1", "dev"));
+    ASSERT_OK_AND_ASSIGN(auto identifier_ctx, identifier_builder.Finish());
+    ASSERT_EQ("dev", identifier_ctx->GetBranch());
+
+    // An agreeing WithBranch() is accepted.
+    ReadContextBuilder agreeing_builder("table_root_path");
+    agreeing_builder.WithCatalog(catalog, Identifier("db1", "t1", 
"dev")).WithBranch("dev");
+    ASSERT_OK_AND_ASSIGN(auto agreeing_ctx, agreeing_builder.Finish());
+    ASSERT_EQ("dev", agreeing_ctx->GetBranch());
+
+    // A disagreeing WithBranch() is refused, and both branches are named.
+    ReadContextBuilder mixed_builder("table_root_path");
+    mixed_builder.WithCatalog(catalog, Identifier("db1", "t1", 
"dev")).WithBranch("rt");
+    ASSERT_NOK_WITH_MSG(mixed_builder.Finish(), "but both 'dev' and 'rt' were 
named");
+}
+
 TEST(ReadContextTest, TestFileSystemAndSchemeMapConflict) {
     ReadContextBuilder builder("table_root_path");
     auto fs = std::make_shared<MockFileSystem>();
diff --git a/src/paimon/core/utils/blob_view_lookup.cpp 
b/src/paimon/core/utils/blob_view_lookup.cpp
index 99e7dc83..30999fe6 100644
--- a/src/paimon/core/utils/blob_view_lookup.cpp
+++ b/src/paimon/core/utils/blob_view_lookup.cpp
@@ -32,7 +32,6 @@
 #include "paimon/common/table/special_fields.h"
 #include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/common/utils/checked_cast.h"
-#include "paimon/common/utils/path_util.h"
 #include "paimon/data/blob_descriptor.h"
 #include "paimon/defs.h"
 #include "paimon/executor.h"
@@ -150,13 +149,16 @@ Result<BlobViewLookup::DescriptorMapping> 
BlobViewLookup::LoadTableDescriptorChu
     if (branch) {
         return Status::Invalid("do not support upstream table with branch");
     }
-    auto file_system = catalog_context->file_system;
-    PAIMON_ASSIGN_OR_RAISE(std::string table_path, 
GetTableLocation(catalog_context, identifier));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::shared_ptr<Catalog> catalog,
+        Catalog::Create(catalog_context->root_path, catalog_context->options,
+                        /*file_system=*/nullptr, 
catalog_context->fs_scheme_to_identifier_map));
+    PAIMON_ASSIGN_OR_RAISE(std::string table_path, 
catalog->GetTableLocation(identifier));
     ScanContextBuilder scan_builder(table_path);
     auto global_index_result = BitmapGlobalIndexResult::FromRanges(row_ranges);
     scan_builder.SetGlobalIndexResult(global_index_result)
         .WithMemoryPool(pool)
-        .WithFileSystem(file_system);
+        .WithCatalog(catalog, identifier);
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ScanContext> scan_context, 
scan_builder.Finish());
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableScan> table_scan,
                            TableScan::Create(std::move(scan_context)));
@@ -169,7 +171,7 @@ Result<BlobViewLookup::DescriptorMapping> 
BlobViewLookup::LoadTableDescriptorChu
         .AddOption(Options::BLOB_AS_DESCRIPTOR, "true")
         .EnablePrefetch(true)
         .WithMemoryPool(pool)
-        .WithFileSystem(file_system);
+        .WithCatalog(catalog, identifier);
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, 
read_builder.Finish());
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableRead> table_read,
                            TableRead::Create(std::move(read_context)));
@@ -182,34 +184,6 @@ Result<BlobViewLookup::DescriptorMapping> 
BlobViewLookup::LoadTableDescriptorChu
     return mapping;
 }
 
-Result<std::string> BlobViewLookup::GetTableLocation(
-    const std::shared_ptr<CatalogContext>& catalog_context, const Identifier& 
identifier) {
-    auto file_system = catalog_context->file_system;
-    PAIMON_ASSIGN_OR_RAISE(
-        std::unique_ptr<Catalog> catalog,
-        Catalog::Create(catalog_context->root_path, catalog_context->options, 
file_system));
-    // The table path may be either xxx/test_database/test_table or
-    // xxx/test_database.db/test_table. If neither path exists or both paths 
exist, it means we
-    // cannot infer the table path, and an error will be reported. If only one 
of the paths
-    // exists, we will use that path.
-    PAIMON_ASSIGN_OR_RAISE(std::string source_table_path, 
catalog->GetTableLocation(identifier));
-    std::string database_name = identifier.GetDatabaseName();
-    PAIMON_ASSIGN_OR_RAISE(std::string data_table_name, 
identifier.GetDataTableName());
-    std::string fallback_source_table_path = PathUtil::JoinPath(
-        PathUtil::JoinPath(catalog_context->root_path, database_name), 
data_table_name);
-
-    PAIMON_ASSIGN_OR_RAISE(bool exist, 
catalog_context->file_system->Exists(source_table_path));
-    PAIMON_ASSIGN_OR_RAISE(bool fallback_exist, 
file_system->Exists(fallback_source_table_path));
-    if (exist == fallback_exist) {
-        return Status::Invalid(
-            fmt::format("Ambiguous table path: both table path {} and fallback 
table path {} are "
-                        "present or absent",
-                        source_table_path, fallback_source_table_path));
-    }
-    std::string final_table_path = exist ? source_table_path : 
fallback_source_table_path;
-    return final_table_path;
-}
-
 Status BlobViewLookup::ExtractBlobDescriptors(const Identifier& identifier,
                                               const std::vector<int32_t>& 
field_ids,
                                               const 
std::shared_ptr<MemoryPool>& pool,
diff --git a/src/paimon/core/utils/blob_view_lookup.h 
b/src/paimon/core/utils/blob_view_lookup.h
index 169d9f83..336863db 100644
--- a/src/paimon/core/utils/blob_view_lookup.h
+++ b/src/paimon/core/utils/blob_view_lookup.h
@@ -29,6 +29,7 @@
 #include "paimon/catalog/identifier.h"
 #include "paimon/common/catalog/catalog_context.h"
 #include "paimon/common/data/blob_view_struct.h"
+#include "paimon/fs/file_system.h"
 #include "paimon/memory/bytes.h"
 #include "paimon/result.h"
 #include "paimon/utils/range.h"
@@ -93,9 +94,6 @@ class BlobViewLookup {
 
     static std::vector<std::vector<Range>> SplitRowRanges(const 
std::vector<Range>& row_ranges,
                                                           int64_t 
target_rows_per_task);
-
-    static Result<std::string> GetTableLocation(
-        const std::shared_ptr<CatalogContext>& catalog_context, const 
Identifier& identifier);
 };
 
 }  // namespace paimon
diff --git a/src/paimon/rest/rest_api.h b/src/paimon/rest/rest_api.h
index 6efdc180..722fc525 100644
--- a/src/paimon/rest/rest_api.h
+++ b/src/paimon/rest/rest_api.h
@@ -80,6 +80,11 @@ class RestApi {
     static constexpr const char* kOptionUrlPrefix = "prefix";
     /// Options with this prefix are sent as http headers (with the prefix 
stripped).
     static constexpr const char* kHeaderOptionPrefix = "header.";
+    /// Optional header carrying the URL-encoded `Identifier` JSON of the 
table which initiated a
+    /// dependency read. It only provides request context; servers must not 
treat it as
+    /// authorization proof. Mirrors the Java `RESTApi.READ_VIA_HEADER`, and a 
REST catalog turns
+    /// the `kHeaderOptionPrefix`-prefixed option into this header.
+    static constexpr const char* kReadViaHeader = "X-Paimon-Read-Via";
     /// Credentials are refreshed once they expire in less than this, so that 
a request
     /// signed with them cannot expire while it is in flight.
     static constexpr int64_t kTokenExpirationSafeTimeMillis = 3600000;
diff --git a/src/paimon/rest/rest_catalog.h b/src/paimon/rest/rest_catalog.h
index c9089d43..2659b67c 100644
--- a/src/paimon/rest/rest_catalog.h
+++ b/src/paimon/rest/rest_catalog.h
@@ -131,6 +131,8 @@ class RestCatalog : public Catalog, public 
VersionManagedCatalog {
         const Identifier& data_identifier, const std::optional<std::string>& 
branch,
         std::string* table_path, std::string* table_id) const;
 
+    /// Converts a `GetTableResponse` into a `TableSchema`, applying the table 
path, audit
+    /// and branch options.
     static Result<std::unique_ptr<TableSchema>> ToTableSchema(
         const GetTableResponse& response, const std::optional<std::string>& 
branch);
 
diff --git a/src/paimon/rest/rest_credential_provider.cpp 
b/src/paimon/rest/rest_credential_provider.cpp
index fda2de9c..5e93b84d 100644
--- a/src/paimon/rest/rest_credential_provider.cpp
+++ b/src/paimon/rest/rest_credential_provider.cpp
@@ -65,7 +65,12 @@ size_t RestToken::Hash::operator()(const RestToken& 
rest_token) const {
 
 RestCredentialProvider::RestCredentialProvider(const std::shared_ptr<RestApi>& 
api,
                                                const Identifier& identifier, 
Clock clock)
-    : api_(api),
+    : RestCredentialProvider([api, identifier]() { return 
api->LoadTableToken(identifier); },
+                             identifier, std::move(clock)) {}
+
+RestCredentialProvider::RestCredentialProvider(TokenLoader token_loader,
+                                               const Identifier& identifier, 
Clock clock)
+    : token_loader_(std::move(token_loader)),
       identifier_(identifier),
       clock_(std::move(clock)),
       logger_(Logger::GetLogger("RestCredentialProvider")) {}
@@ -139,7 +144,7 @@ std::map<std::string, std::string> 
RestCredentialProvider::MergeOptionsWithCrede
 Status RestCredentialProvider::RefreshToken() const {
     PAIMON_LOG_INFO(logger_, "begin refresh data token for identifier [%s]",
                     identifier_.ToString().c_str());
-    PAIMON_ASSIGN_OR_RAISE(GetTableTokenResponse response, 
api_->LoadTableToken(identifier_));
+    PAIMON_ASSIGN_OR_RAISE(GetTableTokenResponse response, token_loader_());
     PAIMON_LOG_INFO(logger_, "end refresh data token for identifier [%s] 
expiresAtMillis [%ld]",
                     identifier_.ToString().c_str(),
                     static_cast<int64_t>(response.GetExpiresAtMillis()));
diff --git a/src/paimon/rest/rest_credential_provider.h 
b/src/paimon/rest/rest_credential_provider.h
index 3860ee67..c46f38d8 100644
--- a/src/paimon/rest/rest_credential_provider.h
+++ b/src/paimon/rest/rest_credential_provider.h
@@ -65,6 +65,7 @@ struct RestToken {
 class RestCredentialProvider : public CredentialProvider {
  public:
     using Clock = std::function<std::chrono::system_clock::time_point()>;
+    using TokenLoader = std::function<Result<GetTableTokenResponse>()>;
 
     /// @param api Client of the catalog that issues the credentials. Shared 
because a
     ///            provider commonly outlives the catalog it was obtained from.
@@ -73,6 +74,10 @@ class RestCredentialProvider : public CredentialProvider {
     RestCredentialProvider(const std::shared_ptr<RestApi>& api, const 
Identifier& identifier,
                            Clock clock = std::chrono::system_clock::now);
 
+    /// Creates a provider backed by an arbitrary table-token loader. This 
keeps the refresh and
+    /// file-system cache semantics shared by REST-compatible catalog 
implementations.
+    RestCredentialProvider(TokenLoader token_loader, const Identifier& 
identifier, Clock clock);
+
     ~RestCredentialProvider() override = default;
 
     /// Returns credentials that are not about to expire, reloading them when 
needed. The
@@ -103,7 +108,7 @@ class RestCredentialProvider : public CredentialProvider {
     /// Whether `token_` is absent or expires within the safe time.
     bool ShouldRefresh() const;
 
-    std::shared_ptr<RestApi> api_;
+    TokenLoader token_loader_;
     Identifier identifier_;
     Clock clock_;
     std::shared_ptr<Logger> logger_;
diff --git a/src/paimon/testing/mock/mock_catalog.h 
b/src/paimon/testing/mock/mock_catalog.h
index 88b9b914..b9954e31 100644
--- a/src/paimon/testing/mock/mock_catalog.h
+++ b/src/paimon/testing/mock/mock_catalog.h
@@ -251,6 +251,11 @@ class MockVersionManagedCatalog : public Catalog, public 
VersionManagedCatalog {
     const std::map<std::string, std::string>& GetOptions() const override {
         return options_;
     }
+
+    /// Sets the catalog-level options `GetOptions()` reports, e.g. 
`metastore`.
+    void SetOptions(const std::map<std::string, std::string>& options) {
+        options_ = options;
+    }
     Result<std::vector<SnapshotInfo>> ListSnapshots(const Identifier&,
                                                     const std::string&) const 
override {
         return Unsupported();
diff --git a/test/inte/blob_table_inte_test.cpp 
b/test/inte/blob_table_inte_test.cpp
index 0d566595..80ea97e5 100644
--- a/test/inte/blob_table_inte_test.cpp
+++ b/test/inte/blob_table_inte_test.cpp
@@ -4156,8 +4156,8 @@ TEST_P(BlobTableInteTest, TestForwardBlobViewReference) {
         PathUtil::JoinPath(dir_->Str(), upstream_db_name + ".db/" + 
upstream_table_name);
     ASSERT_TRUE(TestUtil::CopyDirectory(src_db_path, dst_db_path));
 
-    // The source table has no upstream warehouse configured: only a read with 
resolve
-    // dynamically disabled can succeed on it.
+    // The source table configures no explicit upstream warehouse; a default 
read falls back to the
+    // warehouse it lives in (dir_), where the copied upstream table sits.
     arrow::FieldVector fields = {arrow::field("f0", arrow::int32()),
                                  BlobUtils::ToArrowField("view", true)};
     std::map<std::string, std::string> source_options = {
@@ -4210,12 +4210,15 @@ TEST_P(BlobTableInteTest, TestForwardBlobViewReference) 
{
                          WriteArray(source_table_path, {}, 
schema->field_names(), {write_struct}));
     ASSERT_OK(Commit(source_table_path, commit_msgs));
 
-    // A default read fails on the missing upstream warehouse: the 
pass-through below is
-    // enabled by the dynamic option alone.
+    // With no explicit upstream warehouse, a default read now falls back to 
the source table's own
+    // warehouse (dir_) and resolves the copied upstream table instead of 
failing. The pass-through
+    // read below still uses the dynamic option to preserve the raw 
BlobViewStruct bytes.
     ASSERT_OK_AND_ASSIGN(auto source_plan, ScanTable(source_table_path));
-    ASSERT_NOK_WITH_MSG(
-        ReadTable(source_table_path, schema->field_names(), source_plan, 
/*predicate=*/nullptr),
-        "BLOB_VIEW_UPSTREAM_WAREHOUSE");
+    ASSERT_OK_AND_ASSIGN(
+        auto source_default_result,
+        ReadTable(source_table_path, schema->field_names(), source_plan, 
/*predicate=*/nullptr));
+    ASSERT_TRUE(source_default_result);
+    ASSERT_EQ(source_default_result->length(), 8);
 
     ASSERT_OK_AND_ASSIGN(
         auto source_result,
@@ -4577,7 +4580,7 @@ TEST_P(BlobTableInteTest, 
TestBlobViewFieldWithMultipleUpstreamTables) {
         << "expected:" << expected_with_rk->ToString();
 }
 
-TEST_P(BlobTableInteTest, TestBlobViewFailsWhenBothPathsAbsent) {
+TEST_P(BlobTableInteTest, TestBlobViewFailsWhenUpstreamTableAbsent) {
     if (GetParam() == "mosaic") {
         return;
     }
@@ -4589,10 +4592,10 @@ TEST_P(BlobTableInteTest, 
TestBlobViewFailsWhenBothPathsAbsent) {
 
     ASSERT_OK(WriteDanglingBlobViewRows(table_path));
 
-    // reading fails because neither the table path nor the fallback one 
exists upstream
+    // reading fails because the upstream table does not exist at the 
catalog-issued location
     ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path));
     ASSERT_NOK_WITH_MSG(ReadTable(table_path, schema->field_names(), plan, 
/*predicate=*/nullptr),
-                        "Ambiguous table path");
+                        "not found latest schema");
 }
 
 TEST_P(BlobTableInteTest, TestBlobViewSkipsDanglingReferenceOfDeletedRow) {
@@ -4613,7 +4616,7 @@ TEST_P(BlobTableInteTest, 
TestBlobViewSkipsDanglingReferenceOfDeletedRow) {
     // without the deletion vector the dangling reference fails the read
     ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path));
     ASSERT_NOK_WITH_MSG(ReadTable(table_path, schema->field_names(), plan, 
/*predicate=*/nullptr),
-                        "Ambiguous table path");
+                        "not found latest schema");
 
     ASSERT_OK_AND_ASSIGN(std::string anchor_file_name,
                          
DeletionVectorTestHelper::RetrieveAnchorFileName(commit_msgs));
@@ -4659,7 +4662,7 @@ TEST_P(BlobTableInteTest, 
TestBlobViewSkipsDanglingReferenceInEveryRowRangeGroup
     // without deletion vectors both groups' dangling references fail the read
     ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path));
     ASSERT_NOK_WITH_MSG(ReadTable(table_path, schema->field_names(), plan, 
/*predicate=*/nullptr),
-                        "Ambiguous table path");
+                        "not found latest schema");
 
     // each group's anchor gets a deletion vector dropping its row 0, the 
dangling one
     ASSERT_OK_AND_ASSIGN(std::string anchor_file_name0,
@@ -4698,7 +4701,7 @@ TEST_P(BlobTableInteTest, 
TestBlobViewPreReadHonorsRowRangesNotPredicate) {
     // reading the whole split fails on the dangling reference of row 0
     ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path));
     ASSERT_NOK_WITH_MSG(ReadTable(table_path, schema->field_names(), plan, 
/*predicate=*/nullptr),
-                        "Ambiguous table path");
+                        "not found latest schema");
 
     // a predicate keeping only row 1 does not rescue the query: it is 
evaluated above the split
     // read, while the pre-read takes none, so row 0's reference is still 
collected and resolved
@@ -4706,7 +4709,7 @@ TEST_P(BlobTableInteTest, 
TestBlobViewPreReadHonorsRowRangesNotPredicate) {
                                               FieldType::INT, Literal(101));
     ASSERT_OK_AND_ASSIGN(auto filtered_plan, ScanTable(table_path, 
keep_row_1));
     ASSERT_NOK_WITH_MSG(ReadTable(table_path, schema->field_names(), 
filtered_plan, keep_row_1),
-                        "Ambiguous table path");
+                        "not found latest schema");
 
     // dropping the same row through the row ranges does: the pre-read honors 
them the same way
     // the main read does, so row 0 is never resolved
@@ -4717,115 +4720,6 @@ TEST_P(BlobTableInteTest, 
TestBlobViewPreReadHonorsRowRangesNotPredicate) {
                           /*predicate=*/nullptr, /*row_ranges=*/{Range(1, 
1)}));
 }
 
-TEST_P(BlobTableInteTest, TestBlobViewWithFallbackPath) {
-    auto file_format = GetParam();
-    if (file_format == "mosaic") {
-        return;
-    }
-    const std::string upstream_db_name = "fallback_db";
-    const std::string upstream_table_name = "fallback_table";
-    arrow::FieldVector upstream_fields = {arrow::field("f0", arrow::int32()),
-                                          BlobUtils::ToArrowField("blob", 
true)};
-    auto upstream_schema = arrow::schema(upstream_fields);
-    std::map<std::string, std::string> upstream_options = {
-        {Options::FILE_FORMAT, file_format},     {Options::BUCKET, "-1"},
-        {Options::ROW_TRACKING_ENABLED, "true"}, 
{Options::DATA_EVOLUTION_ENABLED, "true"},
-        {Options::BLOB_AS_DESCRIPTOR, "true"},   {Options::FILE_SYSTEM, 
"local"}};
-
-    // Create the upstream table at the fallback path: <warehouse>/db/table 
(no .db).
-    auto upstream_dir = UniqueTestDirectory::Create("local");
-    std::string fallback_table_path =
-        PathUtil::JoinPath(upstream_dir->Str(), upstream_db_name + "/" + 
upstream_table_name);
-
-    // Manually create schema at fallback path so it can be read as a valid 
paimon table.
-    {
-        // Use a temporary warehouse with Catalog to build the table data, 
then copy to fallback.
-        auto temp_dir = UniqueTestDirectory::Create("local");
-        ::ArrowSchema c_schema;
-        ASSERT_TRUE(arrow::ExportSchema(*upstream_schema, &c_schema).ok());
-        ASSERT_OK_AND_ASSIGN(auto catalog,
-                             Catalog::Create(temp_dir->Str(), 
{{Options::FILE_SYSTEM, "local"}}));
-        ASSERT_OK(catalog->CreateDatabase(upstream_db_name, {}, 
/*ignore_if_exists=*/true));
-        ASSERT_OK(catalog->CreateTable(Identifier(upstream_db_name, 
upstream_table_name), &c_schema,
-                                       /*partition_keys=*/{}, 
/*primary_keys=*/{}, upstream_options,
-                                       /*ignore_if_exists=*/false));
-        std::string temp_table_path =
-            PathUtil::JoinPath(temp_dir->Str(), upstream_db_name + ".db/" + 
upstream_table_name);
-
-        // Write data to the temp table.
-        std::string raw_json = R"([[0, "hello"], [1, "world"]])";
-        auto raw_array = std::dynamic_pointer_cast<arrow::StructArray>(
-            
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(upstream_fields), 
raw_json)
-                .ValueOrDie());
-        ASSERT_OK_AND_ASSIGN(auto desc_array, 
ConvertRawBlobToDescriptor(raw_array, {"blob"}));
-        ASSERT_OK_AND_ASSIGN(
-            auto upstream_commit_msgs,
-            WriteArray(temp_table_path, {}, upstream_schema->field_names(), 
{desc_array}));
-        ASSERT_OK(Commit(temp_table_path, upstream_commit_msgs));
-
-        // Copy the temp table to the fallback path (without .db).
-        ASSERT_TRUE(TestUtil::CopyDirectory(temp_table_path, 
fallback_table_path));
-    }
-
-    // Build the downstream table.
-    arrow::FieldVector fields = {arrow::field("f0", arrow::int32()),
-                                 BlobUtils::ToArrowField("view", true)};
-    std::map<std::string, std::string> options = {
-        {Options::FILE_FORMAT, file_format},
-        {Options::BUCKET, "-1"},
-        {Options::ROW_TRACKING_ENABLED, "true"},
-        {Options::DATA_EVOLUTION_ENABLED, "true"},
-        {Options::BLOB_VIEW_FIELD, "view"},
-        {Options::BLOB_VIEW_UPSTREAM_WAREHOUSE, upstream_dir->Str()},
-        {Options::FILE_SYSTEM, "local"}};
-    CreateTable(fields, /*partition_keys=*/{}, options);
-    std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
-
-    // Write downstream rows referencing the upstream fallback table.
-    Identifier upstream_identifier(upstream_db_name, upstream_table_name);
-    arrow::LargeBinaryBuilder view_builder;
-    for (int64_t row = 0; row < 2; ++row) {
-        BlobViewStruct view_struct(upstream_identifier, /*field_id=*/1, 
/*row_id=*/row);
-        ASSERT_OK_AND_ASSIGN(PAIMON_UNIQUE_PTR<Bytes> serialized, 
view_struct.Serialize(pool_));
-        ASSERT_TRUE(
-            view_builder
-                .Append(reinterpret_cast<const uint8_t*>(serialized->data()), 
serialized->size())
-                .ok());
-    }
-    std::shared_ptr<arrow::Array> write_view_array;
-    ASSERT_TRUE(view_builder.Finish(&write_view_array).ok());
-    auto write_f0_array =
-        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([100, 
101])").ValueOrDie();
-    auto write_struct = std::dynamic_pointer_cast<arrow::StructArray>(
-        arrow::StructArray::Make(arrow::ArrayVector({write_f0_array, 
write_view_array}),
-                                 std::vector<std::string>({"f0", "view"}))
-            .ValueOrDie());
-
-    auto schema = arrow::schema(fields);
-    ASSERT_OK_AND_ASSIGN(auto commit_msgs,
-                         WriteArray(table_path, {}, schema->field_names(), 
{write_struct}));
-    ASSERT_OK(Commit(table_path, commit_msgs));
-
-    // Read and verify
-    ASSERT_OK_AND_ASSIGN(auto plan, ScanTable(table_path));
-    ASSERT_OK_AND_ASSIGN(auto result,
-                         ReadTable(table_path, schema->field_names(), plan, 
/*predicate=*/nullptr));
-    ASSERT_TRUE(result);
-    auto read_concat = arrow::Concatenate(result->chunks()).ValueOrDie();
-    auto read_struct = 
std::dynamic_pointer_cast<arrow::StructArray>(read_concat);
-    ASSERT_EQ(read_struct->length(), 2);
-    ASSERT_OK_AND_ASSIGN(auto result_array, 
ConvertDescriptorToRawBlob(read_struct, {"view"}));
-
-    std::string expected_json = R"([[100, "hello"], [101, "world"]])";
-    auto expected_struct = std::dynamic_pointer_cast<arrow::StructArray>(
-        arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), 
expected_json)
-            .ValueOrDie());
-    ASSERT_OK_AND_ASSIGN(auto expected_with_rk, 
PrependRowKindColumn(expected_struct));
-    ASSERT_TRUE(result_array->Equals(expected_with_rk))
-        << "result_array:" << result_array->ToString() << std::endl
-        << "expected:" << expected_with_rk->ToString();
-}
-
 TEST_P(BlobTableInteTest, TestReadBlobDescriptorFieldFromJava) {
     auto file_format = GetParam();
     if (file_format == "avro" || file_format == "mosaic") {

Reply via email to