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 bb7cb2e5 fix(global-index): support external path for global index 
checkpoints (#473)
bb7cb2e5 is described below

commit bb7cb2e5e1fe4a8a7e93c73316cda3f36ceaebe9
Author: lszskye <[email protected]>
AuthorDate: Wed Oct 28 16:50:15 2026 +0800

    fix(global-index): support external path for global index checkpoints (#473)
---
 docs/source/api/file_index.rst                     |  64 ------------
 docs/source/user_guide.rst                         |   1 +
 docs/source/{api => user_guide}/file_index.rst     |  41 +-------
 docs/source/user_guide/global_index.rst            |  12 ++-
 .../paimon/global_index/global_index_write_task.h  |   8 +-
 .../global_index_file_manager_test.cpp             | 110 ++++++++++++++++++++-
 .../core/global_index/global_index_write_task.cpp  |  11 ++-
 .../core/index/index_checkpoint_path_factory.h     |  11 ++-
 src/paimon/core/utils/file_store_path_factory.cpp  |   4 +-
 .../core/utils/file_store_path_factory_test.cpp    |  48 ++++++---
 test/inte/global_index_test.cpp                    |  73 ++++++++------
 11 files changed, 221 insertions(+), 162 deletions(-)

diff --git a/docs/source/api/file_index.rst b/docs/source/api/file_index.rst
index b6a0eb14..c9fedf37 100644
--- a/docs/source/api/file_index.rst
+++ b/docs/source/api/file_index.rst
@@ -21,70 +21,6 @@ File Index
 
 .. _cpp-api-file-index:
 
-Full-text predicates and root TopK
-=================================
-
-``PredicateBuilder::FullTextMatch`` creates a ``LeafPredicate`` for a full-text
-condition without TopK truncation. It holds a shared ``FullTextMatch`` request,
-whose query uses the same JSON DSL as ``FullTextSearch``. Do not modify a 
request
-after passing it to a predicate or read context.
-
-Compose full-text and scalar predicates with the existing
-``PredicateBuilder::And`` and ``PredicateBuilder::Or`` factories. Every field
-referenced by this tree must appear in the read schema; field indices refer to
-positions in that schema. For example, the following constructs
-``body matches "apple" OR price < 10``:
-
-.. code-block:: cpp
-
-   auto request = std::make_shared<paimon::FullTextMatch>(
-       "body", R"({"match":{"query":"apple"}})");
-   PAIMON_ASSIGN_OR_RAISE(
-       std::shared_ptr<paimon::Predicate> text,
-       paimon::PredicateBuilder::FullTextMatch(0, request));
-   auto price = paimon::PredicateBuilder::LessThan(
-       1, "price", paimon::FieldType::INT, paimon::Literal(10));
-   PAIMON_ASSIGN_OR_RAISE(
-       std::shared_ptr<paimon::Predicate> filter,
-       paimon::PredicateBuilder::Or({text, price}));
-   paimon::ReadContextBuilder builder(table_path);
-   builder.SetReadFieldNames({"body", "price"}).SetPredicate(filter);
-
-``SetVectorSearch`` and ``SetFullTextSearch`` configure a separate root TopK
-request. They are mutually exclusive and require a positive limit. The intended
-semantics are to filter by the complete predicate before ranking; the TopK 
search
-field does not need to be projected. A full-text match inside a predicate never
-has a TopK limit.
-
-Score output is requested through an explicit ``_<field>_INDEX_SCORE``
-projection. For example, request the score of a full-text
-predicate by projecting ``_body_INDEX_SCORE``:
-
-.. code-block:: cpp
-
-   auto request = std::make_shared<paimon::FullTextMatch>(
-       "body", R"({"match":{"query":"apple"}})");
-   PAIMON_ASSIGN_OR_RAISE(
-       std::shared_ptr<paimon::Predicate> text,
-       paimon::PredicateBuilder::FullTextMatch(0, request));
-   paimon::ReadContextBuilder builder(table_path);
-   builder.SetReadFieldNames({"body", "_body_INDEX_SCORE"}).SetPredicate(text);
-
-Score fields have type ``float32`` and preserve the source field's exact name,
-including case. Select them with ``SetReadFieldNames`` or ``SetReadSchema``;
-they have no persistent field IDs for ``SetReadFieldIds``. Reading all table
-fields does not implicitly add score fields.
-
-Each projected score must have exactly one search condition as its source.
-The source may be a root TopK request or a full-text predicate. A full-text
-predicate whose score is projected cannot have an OR ancestor, and its field
-cannot occur in another search condition, including the root TopK request.
-An ordinary scalar predicate on the same field is allowed. For example, when
-projecting the score of ``match``, ``match AND (p OR q)`` is valid, while
-``match OR p`` is rejected. Without that score projection, both OR branches and
-repeated full-text search fields are allowed. These checks use the effective
-read projection, after resolving the read schema.
-
 Interface
 =========
 
diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst
index c03efd88..a8d3c7f8 100644
--- a/docs/source/user_guide.rst
+++ b/docs/source/user_guide.rst
@@ -43,5 +43,6 @@ User Guide
    user_guide/clean
    user_guide/prefetch
    user_guide/arrow
+   user_guide/file_index
    user_guide/global_index
    user_guide/primary_key_global_index
diff --git a/docs/source/api/file_index.rst 
b/docs/source/user_guide/file_index.rst
similarity index 85%
copy from docs/source/api/file_index.rst
copy to docs/source/user_guide/file_index.rst
index b6a0eb14..a402cc6e 100644
--- a/docs/source/api/file_index.rst
+++ b/docs/source/user_guide/file_index.rst
@@ -15,14 +15,11 @@
 .. specific language governing permissions and limitations
 .. under the License.
 
-===========
 File Index
-===========
-
-.. _cpp-api-file-index:
+==========
 
 Full-text predicates and root TopK
-=================================
+---------------------------------
 
 ``PredicateBuilder::FullTextMatch`` creates a ``LeafPredicate`` for a full-text
 condition without TopK truncation. It holds a shared ``FullTextMatch`` request,
@@ -84,37 +81,3 @@ projecting the score of ``match``, ``match AND (p OR q)`` is 
valid, while
 ``match OR p`` is rejected. Without that score projection, both OR branches and
 repeated full-text search fields are allowed. These checks use the effective
 read projection, after resolving the read schema.
-
-Interface
-=========
-
-.. doxygenstruct:: paimon::FullTextMatch
-   :members:
-
-.. doxygenclass:: paimon::FileIndexer
-   :members:
-   :undoc-members:
-
-.. doxygenclass:: paimon::FileIndexerFactory
-   :members:
-   :undoc-members:
-
-.. doxygenclass:: paimon::FileIndexWriter
-   :members:
-   :undoc-members:
-
-.. doxygenclass:: paimon::FileIndexReader
-   :members:
-   :undoc-members:
-
-.. doxygenclass:: paimon::FileIndexFormat
-   :members:
-   :undoc-members:
-
-.. doxygenclass:: paimon::FileIndexResult
-   :members:
-   :undoc-members:
-
-.. doxygenclass:: paimon::BitmapIndexResult
-   :members:
-   :undoc-members:
diff --git a/docs/source/user_guide/global_index.rst 
b/docs/source/user_guide/global_index.rst
index 897cfebb..c744d018 100644
--- a/docs/source/user_guide/global_index.rst
+++ b/docs/source/user_guide/global_index.rst
@@ -50,9 +50,15 @@ task once it is no longer running or being retried:
    PAIMON_RETURN_NOT_OK(paimon::GlobalIndexWriteTask::DeleteCheckpoint(
        table_path, "build-task-1", options, file_system));
 
-Pass the same table path and task id used in ``WriteIndex``. Task ids must be 
unique within the
-table. The cleanup recursively removes the whole task directory, other tasks' 
directories and completed
-index files are preserved.
+Checkpoints are stored in ``<table_path>/index/checkpoint/<task_id>`` by 
default. When
+``global-index.external-path`` is configured, they are stored in
+``<global-index.external-path>/checkpoint/<task_id>`` instead.
+
+Pass the same table path, task id, and ``global-index.external-path`` 
configuration used in
+``WriteIndex``. Cleanup reads the configuration from the table schema and 
applies any supplied
+options as overrides. Task ids must be unique among builds sharing the same 
global index directory,
+including builds for different tables sharing an external path. The cleanup 
recursively removes
+the whole task directory, other tasks' directories and completed index files 
are preserved.
 
 Bitmap Index
 ------------
diff --git a/include/paimon/global_index/global_index_write_task.h 
b/include/paimon/global_index/global_index_write_task.h
index 8c89a9c1..15cc6b7d 100644
--- a/include/paimon/global_index/global_index_write_task.h
+++ b/include/paimon/global_index/global_index_write_task.h
@@ -50,7 +50,8 @@ class PAIMON_EXPORT GlobalIndexWriteTask {
     ///                     filters).
     /// @param task_id      When checkpoints are enabled, the caller must 
provide a non-empty task
     ///                     identifier that uniquely identifies an index build 
task within the
-    ///                     table. Reuse it when retrying the same build. If 
the source data, build
+    ///                     global index directory, including tables sharing 
an external path.
+    ///                     Reuse it when retrying the same build. If the 
source data, build
     ///                     configuration, or build source code changes, use a 
new identifier;
     ///                     otherwise, the index build may fail. Pass nullopt 
when checkpoints are
     ///                     disabled. Index types without checkpoint support 
ignore this value.
@@ -73,9 +74,12 @@ class PAIMON_EXPORT GlobalIndexWriteTask {
     /// @param table_path   Path to the table root directory used for the 
build.
     /// @param task_id      Non-empty, unique task identifier passed to 
`WriteIndex`.
     /// @param options      Configuration overrides. Used to load the schema, 
then merged with
-    ///                     schema options, with these overrides taking 
precedence.
+    ///                     schema options, with these overrides taking 
precedence. Checkpoints
+    ///                     are deleted from the configured global index 
external path, or from
+    ///                     the table's index directory when no external path 
is configured.
     /// @param file_system  Specifies the file system. If `nullptr`, use 
default file system.
     /// @return OK if the task directory was deleted or does not exist.
+    ///         File system errors are propagated to the caller without being 
suppressed.
     static Status DeleteCheckpoint(const std::string& table_path, const 
std::string& task_id,
                                    const std::map<std::string, std::string>& 
options,
                                    const std::shared_ptr<FileSystem>& 
file_system = nullptr);
diff --git a/src/paimon/core/global_index/global_index_file_manager_test.cpp 
b/src/paimon/core/global_index/global_index_file_manager_test.cpp
index d5cce4ad..f375a266 100644
--- a/src/paimon/core/global_index/global_index_file_manager_test.cpp
+++ b/src/paimon/core/global_index/global_index_file_manager_test.cpp
@@ -216,6 +216,61 @@ TEST_F(GlobalIndexFileManagerTest, 
TestCheckpointIOAndRestart) {
     ASSERT_TRUE(exists);
 }
 
+TEST_F(GlobalIndexFileManagerTest, TestExternalCheckpointIOAndRestart) {
+    std::string external_path = "file:" + PathUtil::JoinPath(dir_->Str(), 
"external-index");
+    ASSERT_OK_AND_ASSIGN(
+        path_factory_,
+        FileStorePathFactory::Create(
+            dir_->Str(), arrow::schema({}), /*partition_keys=*/{}, 
/*default_part_value=*/"",
+            /*identifier=*/"mock", /*data_file_prefix=*/"data-",
+            /*legacy_partition_name_enabled=*/true, /*external_paths=*/{}, 
external_path,
+            /*index_file_in_data_file_dir=*/false, GetDefaultPool()));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager, 
CreateManager());
+    std::string directory = PathUtil::JoinPath(external_path, 
"checkpoint/task-1");
+    std::string file_name = "lumina-global-index-vector-10-20-0.index.ckpt";
+    std::string local_path =
+        PathUtil::JoinPath(dir_->Str(), "index/checkpoint/task-1/" + 
file_name);
+    ASSERT_OK(fs_->WriteFile(local_path, "local", /*overwrite=*/false));
+    ASSERT_OK_AND_ASSIGN(bool exists, manager->CheckpointExists());
+    ASSERT_FALSE(exists);
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<OutputStream> output,
+                         manager->CreateCheckpointOutputStream());
+    ASSERT_OK_AND_ASSIGN(int64_t written, output->Write("external", 8));
+    ASSERT_EQ(written, 8);
+    ASSERT_OK(output->Close());
+    ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(PathUtil::JoinPath(directory, 
file_name)));
+    ASSERT_TRUE(exists);
+
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> restarted, 
CreateManager());
+    ASSERT_OK_AND_ASSIGN(exists, restarted->CheckpointExists());
+    ASSERT_TRUE(exists);
+    ASSERT_OK_AND_ASSIGN(std::unique_ptr<InputStream> input,
+                         restarted->OpenCheckpointInputStream());
+    char buffer[8];
+    ASSERT_OK_AND_ASSIGN(int64_t read, input->Read(buffer, sizeof(buffer)));
+    ASSERT_EQ(read, 8);
+    ASSERT_EQ(std::string(buffer, sizeof(buffer)), "external");
+    ASSERT_OK(input->Close());
+    ASSERT_OK_AND_ASSIGN(std::string next_name, 
CreateCheckpointFile(restarted));
+    ASSERT_EQ(next_name, "lumina-global-index-vector-10-20-1.index.ckpt");
+
+    std::vector<std::string> retained_paths = {
+        local_path, PathUtil::JoinPath(external_path, "checkpoint/task-2/" + 
file_name),
+        PathUtil::JoinPath(directory, 
"lumina-global-index-other-10-20-0.index.ckpt"),
+        manager->ToPath("retained.index")};
+    for (size_t i = 1; i < retained_paths.size(); ++i) {
+        ASSERT_OK(fs_->WriteFile(retained_paths[i], "retained", 
/*overwrite=*/false));
+    }
+    ASSERT_OK(restarted->DeleteCheckpoint());
+    ASSERT_OK_AND_ASSIGN(exists, restarted->CheckpointExists());
+    ASSERT_FALSE(exists);
+    ASSERT_OK(restarted->DeleteCheckpoint());
+    for (const std::string& path : retained_paths) {
+        ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(path));
+        ASSERT_TRUE(exists) << path;
+    }
+}
+
 TEST_F(GlobalIndexFileManagerTest, TestInitializationFailureCanRetry) {
     ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexFileManager> manager, 
CreateManager());
     fs_->fail_list_ = true;
@@ -357,6 +412,54 @@ TEST_F(GlobalIndexFileManagerTest, 
TestDeleteTaskCheckpoint) {
     ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1", 
/*options=*/{}, fs_));
 }
 
+TEST_F(GlobalIndexFileManagerTest, 
TestDeleteExternalTaskCheckpointFromSchemaAndOverrides) {
+    std::string schema_path = "file:" + PathUtil::JoinPath(dir_->Str(), 
"schema-index");
+    std::string override_path = "file:" + PathUtil::JoinPath(dir_->Str(), 
"override-index");
+    ASSERT_OK(CreateSchema({{Options::GLOBAL_INDEX_EXTERNAL_PATH, " " + 
schema_path + "/ "}}));
+    std::string local_directory = PathUtil::JoinPath(dir_->Str(), 
"index/checkpoint/task-1");
+    std::string schema_directory = PathUtil::JoinPath(schema_path, 
"checkpoint/task-1");
+    std::string override_directory = PathUtil::JoinPath(override_path, 
"checkpoint/task-1");
+    for (const std::string& directory : {local_directory, schema_directory, 
override_directory}) {
+        ASSERT_OK(fs_->WriteFile(PathUtil::JoinPath(directory, 
"nested/unfinished-checkpoint"),
+                                 "checkpoint", /*overwrite=*/false));
+    }
+    std::vector<std::string> retained_paths = {
+        PathUtil::JoinPath(schema_path, "checkpoint/task-2/checkpoint"),
+        PathUtil::JoinPath(override_path, 
"checkpoint/task-1-extended/checkpoint"),
+        PathUtil::JoinPath(schema_path, "retained.index"),
+        PathUtil::JoinPath(override_path, "retained.index")};
+    for (const std::string& path : retained_paths) {
+        ASSERT_OK(fs_->WriteFile(path, "retained", /*overwrite=*/false));
+    }
+    std::map<std::string, std::string> overrides = {
+        {Options::GLOBAL_INDEX_EXTERNAL_PATH, " " + override_path + "/ "}};
+    ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1", 
overrides, fs_));
+    ASSERT_OK_AND_ASSIGN(bool exists, fs_->Exists(override_directory));
+    ASSERT_FALSE(exists);
+    ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(schema_directory));
+    ASSERT_TRUE(exists);
+    ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(local_directory));
+    ASSERT_TRUE(exists);
+    ASSERT_EQ(fs_->delete_paths_, 
std::vector<std::string>({override_directory}));
+    ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1", 
overrides, fs_));
+
+    ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1", 
/*options=*/{}));
+    ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(schema_directory));
+    ASSERT_FALSE(exists);
+    ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(local_directory));
+    ASSERT_TRUE(exists);
+    ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), "task-1", 
/*options=*/{}));
+    for (const std::string& path : retained_paths) {
+        ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(path));
+        ASSERT_TRUE(exists) << path;
+    }
+
+    ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(
+        dir_->Str(), "task-1", {{Options::GLOBAL_INDEX_EXTERNAL_PATH, ""}}, 
fs_));
+    ASSERT_OK_AND_ASSIGN(exists, fs_->Exists(local_directory));
+    ASSERT_FALSE(exists);
+}
+
 TEST_F(GlobalIndexFileManagerTest, 
TestDeleteTaskCheckpointWithoutCustomFileSystem) {
     ASSERT_OK(CreateSchema());
     ASSERT_NOK_WITH_MSG(GlobalIndexWriteTask::DeleteCheckpoint(
@@ -376,12 +479,13 @@ TEST_F(GlobalIndexFileManagerTest, 
TestDeleteTaskCheckpointWithoutCustomFileSyst
 }
 
 TEST_F(GlobalIndexFileManagerTest, 
TestDeleteTaskCheckpointRejectsInvalidTaskId) {
+    ASSERT_OK(CreateSchema());
     for (const std::string& invalid : std::vector<std::string>{"", ".", "..", 
" ", "a/b", "a\\b",
                                                                "a\nb", 
std::string("a\0b", 3)}) {
-        ASSERT_NOK(
-            GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), invalid, 
/*options=*/{}, fs_));
+        ASSERT_NOK_WITH_MSG(
+            GlobalIndexWriteTask::DeleteCheckpoint(dir_->Str(), invalid, 
/*options=*/{}, fs_),
+            "checkpoint task id");
     }
-    ASSERT_EQ(fs_->list_count_, 0);
     ASSERT_TRUE(fs_->delete_paths_.empty());
 }
 
diff --git a/src/paimon/core/global_index/global_index_write_task.cpp 
b/src/paimon/core/global_index/global_index_write_task.cpp
index f04d96bb..cb11fd92 100644
--- a/src/paimon/core/global_index/global_index_write_task.cpp
+++ b/src/paimon/core/global_index/global_index_write_task.cpp
@@ -31,6 +31,7 @@
 #include "paimon/common/utils/arrow/mem_utils.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/common/utils/scope_guard.h"
 #include "paimon/core/casting/casting_utils.h"
 #include "paimon/core/core_options.h"
@@ -424,12 +425,16 @@ Status GlobalIndexWriteTask::DeleteCheckpoint(const 
std::string& table_path,
                                               const std::string& task_id,
                                               const std::map<std::string, 
std::string>& options,
                                               const 
std::shared_ptr<FileSystem>& file_system) {
-    PAIMON_ASSIGN_OR_RAISE(std::string task_directory,
-                           
IndexCheckpointPathFactory::GetTaskDirectory(table_path, task_id));
     std::pair<std::shared_ptr<TableSchema>, CoreOptions> schema_and_options;
     PAIMON_ASSIGN_OR_RAISE(schema_and_options,
                            LoadSchemaAndOptions(table_path, options, 
file_system));
-    std::shared_ptr<FileSystem> fs = schema_and_options.second.GetFileSystem();
+    const CoreOptions& core_options = schema_and_options.second;
+    PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> 
global_index_external_path,
+                           core_options.CreateGlobalIndexExternalPath());
+    PAIMON_ASSIGN_OR_RAISE(std::string task_directory,
+                           IndexCheckpointPathFactory::GetTaskDirectory(
+                               table_path, task_id, 
global_index_external_path));
+    std::shared_ptr<FileSystem> fs = core_options.GetFileSystem();
     PAIMON_ASSIGN_OR_RAISE(bool exists, fs->Exists(task_directory));
     if (!exists) {
         return Status::OK();
diff --git a/src/paimon/core/index/index_checkpoint_path_factory.h 
b/src/paimon/core/index/index_checkpoint_path_factory.h
index 0bfa56aa..cdc043ba 100644
--- a/src/paimon/core/index/index_checkpoint_path_factory.h
+++ b/src/paimon/core/index/index_checkpoint_path_factory.h
@@ -43,11 +43,14 @@ class IndexCheckpointPathFactory {
     /// Returns the checkpoint id when the file name belongs to this factory.
     virtual std::optional<int64_t> GetCheckpointId(const std::string& 
file_name) const = 0;
 
-    /// Returns the task's checkpoint directory after validating its 
identifier.
-    static Result<std::string> GetTaskDirectory(const std::string& table_path,
-                                                const std::string& task_id) {
+    /// Returns the task's checkpoint directory under the configured global 
index directory.
+    static Result<std::string> GetTaskDirectory(
+        const std::string& table_path, const std::string& task_id,
+        const std::optional<std::string>& global_index_external_path) {
         PAIMON_RETURN_NOT_OK(PathUtil::CheckSinglePathComponent("checkpoint 
task id", task_id));
-        return PathUtil::JoinPath(PathUtil::JoinPath(table_path, 
"index/checkpoint"), task_id);
+        std::string index_directory =
+            global_index_external_path.value_or(PathUtil::JoinPath(table_path, 
"index"));
+        return PathUtil::JoinPath(PathUtil::JoinPath(index_directory, 
"checkpoint"), task_id);
     }
 };
 
diff --git a/src/paimon/core/utils/file_store_path_factory.cpp 
b/src/paimon/core/utils/file_store_path_factory.cpp
index 9acb6d4e..cbb767fc 100644
--- a/src/paimon/core/utils/file_store_path_factory.cpp
+++ b/src/paimon/core/utils/file_store_path_factory.cpp
@@ -248,8 +248,8 @@ 
FileStorePathFactory::CreateGlobalIndexCheckpointPathFactory(const std::string&
     };
     PAIMON_RETURN_NOT_OK(PathUtil::CheckSinglePathComponent("checkpoint index 
type", index_type));
     PAIMON_RETURN_NOT_OK(PathUtil::CheckSinglePathComponent("checkpoint 
field", field_name));
-    PAIMON_ASSIGN_OR_RAISE(std::string directory,
-                           IndexCheckpointPathFactory::GetTaskDirectory(root_, 
task_id));
+    PAIMON_ASSIGN_OR_RAISE(std::string directory, 
IndexCheckpointPathFactory::GetTaskDirectory(
+                                                      root_, task_id, 
global_index_external_path_));
     std::string file_name_prefix =
         fmt::format("{}-global-index-{}-{}-{}-", index_type, field_name, 
range.from, range.to);
     return std::unique_ptr<IndexCheckpointPathFactory>(
diff --git a/src/paimon/core/utils/file_store_path_factory_test.cpp 
b/src/paimon/core/utils/file_store_path_factory_test.cpp
index 447cb4f4..1d38035e 100644
--- a/src/paimon/core/utils/file_store_path_factory_test.cpp
+++ b/src/paimon/core/utils/file_store_path_factory_test.cpp
@@ -53,7 +53,9 @@ class FileStorePathFactoryTest : public ::testing::Test {
     }
     void TearDown() override {}
 
-    std::shared_ptr<FileStorePathFactory> CreateFactory(const std::string& 
root) const {
+    std::shared_ptr<FileStorePathFactory> CreateFactory(
+        const std::string& root,
+        const std::optional<std::string>& global_index_external_path = 
std::nullopt) const {
         arrow::FieldVector fields = {arrow::field("f0", arrow::boolean()),
                                      arrow::field("f1", arrow::int8()),
                                      arrow::field("f2", arrow::int8()),
@@ -76,13 +78,13 @@ class FileStorePathFactoryTest : public ::testing::Test {
         EXPECT_OK_AND_ASSIGN(CoreOptions options, 
CoreOptions::FromMap(raw_options));
         EXPECT_OK_AND_ASSIGN(std::vector<std::string> external_paths,
                              options.CreateExternalPaths());
-        EXPECT_OK_AND_ASSIGN(auto path_factory,
-                             FileStorePathFactory::Create(
-                                 root, schema, {"f0", "f3"}, 
options.GetPartitionDefaultName(),
-                                 options.GetFileFormat()->Identifier(), 
options.DataFilePrefix(),
-                                 options.LegacyPartitionNameEnabled(), 
external_paths,
-                                 /*global_index_external_path=*/std::nullopt,
-                                 options.IndexFileInDataFileDir(), mem_pool_));
+        EXPECT_OK_AND_ASSIGN(
+            auto path_factory,
+            FileStorePathFactory::Create(
+                root, schema, {"f0", "f3"}, options.GetPartitionDefaultName(),
+                options.GetFileFormat()->Identifier(), 
options.DataFilePrefix(),
+                options.LegacyPartitionNameEnabled(), external_paths, 
global_index_external_path,
+                options.IndexFileInDataFileDir(), mem_pool_));
         return path_factory;
     }
 
@@ -601,8 +603,8 @@ TEST_F(FileStorePathFactoryTest, 
TestCreateGlobalIndexCheckpointPathFactory) {
                          
file_store_path_factory->CreateGlobalIndexCheckpointPathFactory(
                              "lumina", "vector", Range(10, 20), "task-1"));
     std::string checkpoint_dir = PathUtil::JoinPath(dir->Str(), 
"index/checkpoint/task-1");
-    ASSERT_OK_AND_ASSIGN(std::string task_directory,
-                         
IndexCheckpointPathFactory::GetTaskDirectory(dir->Str(), "task-1"));
+    ASSERT_OK_AND_ASSIGN(std::string task_directory, 
IndexCheckpointPathFactory::GetTaskDirectory(
+                                                         dir->Str(), "task-1", 
std::nullopt));
     ASSERT_EQ(task_directory, checkpoint_dir);
     ASSERT_EQ(checkpoint_path_factory->GetDirectoryPath(), checkpoint_dir);
     std::string first_path = checkpoint_path_factory->NewPath(0);
@@ -634,6 +636,30 @@ TEST_F(FileStorePathFactoryTest, 
TestCreateGlobalIndexCheckpointPathFactory) {
     ASSERT_FALSE(exists);
 }
 
+TEST_F(FileStorePathFactoryTest, 
TestCreateGlobalIndexCheckpointPathFactoryWithExternalPath) {
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    for (const std::string& external_path :
+         {"file://" + dir->Str() + "/external-index/", 
std::string("s3://bucket/external-index")}) {
+        auto factory = CreateFactory(dir->Str(), external_path);
+        ASSERT_OK_AND_ASSIGN(std::unique_ptr<IndexCheckpointPathFactory> 
checkpoint_factory,
+                             factory->CreateGlobalIndexCheckpointPathFactory(
+                                 "lumina", "vector", Range(10, 20), "task-1"));
+        std::string checkpoint_dir = PathUtil::JoinPath(external_path, 
"checkpoint/task-1");
+        ASSERT_OK_AND_ASSIGN(
+            std::string task_directory,
+            IndexCheckpointPathFactory::GetTaskDirectory(dir->Str(), "task-1", 
external_path));
+        ASSERT_EQ(task_directory, checkpoint_dir);
+        ASSERT_EQ(checkpoint_factory->GetDirectoryPath(), checkpoint_dir);
+        std::string file_name = 
"lumina-global-index-vector-10-20-9.index.ckpt";
+        ASSERT_EQ(checkpoint_factory->NewPath(9), 
PathUtil::JoinPath(checkpoint_dir, file_name));
+        ASSERT_EQ(checkpoint_factory->ToPath(file_name), 
checkpoint_factory->NewPath(9));
+    }
+    ASSERT_OK_AND_ASSIGN(bool exists, dir->GetFileSystem()->Exists(
+                                          PathUtil::JoinPath(dir->Str(), 
"external-index")));
+    ASSERT_FALSE(exists);
+}
+
 TEST_F(FileStorePathFactoryTest, TestCheckpointFileIdPath) {
     auto dir = UniqueTestDirectory::Create();
     ASSERT_TRUE(dir);
@@ -707,7 +733,7 @@ TEST_F(FileStorePathFactoryTest, 
TestCheckpointRejectsInvalidPathComponents) {
     auto factory = CreateFactory(dir->Str());
     for (const std::string& invalid : std::vector<std::string>{"", ".", "..", 
" ", "a/b", "a\\b",
                                                                "a\nb", 
std::string("a\0b", 3)}) {
-        ASSERT_NOK(IndexCheckpointPathFactory::GetTaskDirectory(dir->Str(), 
invalid));
+        ASSERT_NOK(IndexCheckpointPathFactory::GetTaskDirectory(dir->Str(), 
invalid, std::nullopt));
         ASSERT_NOK(factory->CreateGlobalIndexCheckpointPathFactory(invalid, 
"vector", Range(10, 20),
                                                                    "task"));
         ASSERT_NOK(factory->CreateGlobalIndexCheckpointPathFactory("lumina", 
invalid, Range(10, 20),
diff --git a/test/inte/global_index_test.cpp b/test/inte/global_index_test.cpp
index c5b42162..a978b299 100644
--- a/test/inte/global_index_test.cpp
+++ b/test/inte/global_index_test.cpp
@@ -575,37 +575,48 @@ TEST_P(GlobalIndexTest, 
TestWriteLuminaIndexWithCheckpoint) {
     ASSERT_OK_AND_ASSIGN(auto split, ScanData(table_path, 
/*partition_filters=*/{}));
     std::shared_ptr<FileSystem> checkpoint_fs = dir_->GetFileSystem();
     std::string task_id = "task-1";
-    std::string checkpoint_dir = PathUtil::JoinPath(table_path, 
"index/checkpoint");
-    std::string task_checkpoint_dir = PathUtil::JoinPath(checkpoint_dir, 
task_id);
-    ASSERT_OK(checkpoint_fs->Mkdirs(task_checkpoint_dir));
-    ASSERT_OK(checkpoint_fs->Mkdirs(PathUtil::JoinPath(checkpoint_dir, 
"another-task")));
-    auto checkpoint_path = [&](const std::string& task, int64_t id) {
-        return PathUtil::JoinPath(PathUtil::JoinPath(checkpoint_dir, task),
-                                  
fmt::format("lumina-global-index-f1-0-3-{}.index.ckpt", id));
-    };
-    std::string matching_checkpoint = checkpoint_path(task_id, 9);
-    ASSERT_OK(checkpoint_fs->WriteFile(matching_checkpoint, "invalid 
checkpoint",
-                                       /*overwrite=*/false));
-    std::string unrelated_checkpoint = checkpoint_path("another-task", 999);
-    ASSERT_OK(checkpoint_fs->WriteFile(unrelated_checkpoint, "another task",
-                                       /*overwrite=*/false));
-
-    ASSERT_OK(GlobalIndexWriteTask::WriteIndex(
-        table_path, "f1", "lumina",
-        std::make_shared<IndexedSplitImpl>(split, std::vector<Range>{Range(0, 
3)}), lumina_options,
-        task_id, pool_));
-    std::vector<BasicFileStatus> checkpoint_files;
-    ASSERT_OK(checkpoint_fs->ListDir(task_checkpoint_dir, &checkpoint_files));
-    ASSERT_TRUE(checkpoint_files.empty());
-    ASSERT_OK_AND_ASSIGN(bool unrelated_exists, 
checkpoint_fs->Exists(unrelated_checkpoint));
-    ASSERT_TRUE(unrelated_exists);
-
-    ASSERT_OK(GlobalIndexWriteTask::DeleteCheckpoint(table_path, 
"another-task", lumina_options));
-    ASSERT_OK_AND_ASSIGN(unrelated_exists, 
checkpoint_fs->Exists(unrelated_checkpoint));
-    ASSERT_FALSE(unrelated_exists);
-    ASSERT_OK_AND_ASSIGN(bool unrelated_directory_exists,
-                         
checkpoint_fs->Exists(PathUtil::JoinPath(checkpoint_dir, "another-task")));
-    ASSERT_FALSE(unrelated_directory_exists);
+    auto external_dir = UniqueTestDirectory::Create("local");
+    ASSERT_TRUE(external_dir);
+    for (bool use_external_path : {false, true}) {
+        std::map<std::string, std::string> checkpoint_options = lumina_options;
+        std::string checkpoint_dir = PathUtil::JoinPath(table_path, 
"index/checkpoint");
+        if (use_external_path) {
+            checkpoint_options["global-index.external-path"] = "FILE://" + 
external_dir->Str();
+            checkpoint_dir = PathUtil::JoinPath(external_dir->Str(), 
"checkpoint");
+        }
+        std::string task_checkpoint_dir = PathUtil::JoinPath(checkpoint_dir, 
task_id);
+        ASSERT_OK(checkpoint_fs->Mkdirs(task_checkpoint_dir));
+        ASSERT_OK(checkpoint_fs->Mkdirs(PathUtil::JoinPath(checkpoint_dir, 
"another-task")));
+        auto checkpoint_path = [&](const std::string& task, int64_t id) {
+            return PathUtil::JoinPath(PathUtil::JoinPath(checkpoint_dir, task),
+                                      
fmt::format("lumina-global-index-f1-0-3-{}.index.ckpt", id));
+        };
+        std::string matching_checkpoint = checkpoint_path(task_id, 9);
+        ASSERT_OK(checkpoint_fs->WriteFile(matching_checkpoint, "invalid 
checkpoint",
+                                           /*overwrite=*/false));
+        std::string unrelated_checkpoint = checkpoint_path("another-task", 
999);
+        ASSERT_OK(checkpoint_fs->WriteFile(unrelated_checkpoint, "another 
task",
+                                           /*overwrite=*/false));
+
+        ASSERT_OK(GlobalIndexWriteTask::WriteIndex(
+            table_path, "f1", "lumina",
+            std::make_shared<IndexedSplitImpl>(split, 
std::vector<Range>{Range(0, 3)}),
+            checkpoint_options, task_id, pool_));
+        std::vector<BasicFileStatus> checkpoint_files;
+        ASSERT_OK(checkpoint_fs->ListDir(task_checkpoint_dir, 
&checkpoint_files));
+        ASSERT_TRUE(checkpoint_files.empty());
+        ASSERT_OK_AND_ASSIGN(bool unrelated_exists, 
checkpoint_fs->Exists(unrelated_checkpoint));
+        ASSERT_TRUE(unrelated_exists);
+
+        ASSERT_OK(
+            GlobalIndexWriteTask::DeleteCheckpoint(table_path, "another-task", 
checkpoint_options));
+        ASSERT_OK_AND_ASSIGN(unrelated_exists, 
checkpoint_fs->Exists(unrelated_checkpoint));
+        ASSERT_FALSE(unrelated_exists);
+        ASSERT_OK_AND_ASSIGN(
+            bool unrelated_directory_exists,
+            checkpoint_fs->Exists(PathUtil::JoinPath(checkpoint_dir, 
"another-task")));
+        ASSERT_FALSE(unrelated_directory_exists);
+    }
 }
 
 TEST_P(GlobalIndexTest, TestWriteLuminaIndexWithMismatchedDimension) {

Reply via email to