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 7351dad0 feat(core): complete file index write lifecycle (#279)
7351dad0 is described below
commit 7351dad0aa7b479e462fb4309de9cbfc8a378d40
Author: Zhang Jiawei <[email protected]>
AuthorDate: Fri Sep 4 14:30:42 2026 +0800
feat(core): complete file index write lifecycle (#279)
---
src/paimon/CMakeLists.txt | 1 +
src/paimon/core/append/append_only_writer.cpp | 4 +-
src/paimon/core/append/append_only_writer_test.cpp | 15 +++-
src/paimon/core/io/data_file_index_writer_test.cpp | 17 ++--
src/paimon/core/io/data_file_writer_base.h | 1 +
src/paimon/core/io/data_file_writer_base_test.cpp | 97 ++++++++++++++++++++++
.../core/io/key_value_data_file_writer_factory.cpp | 1 +
...hredding_key_value_data_file_writer_factory.cpp | 1 +
src/paimon/core/io/single_file_writer.h | 12 ++-
src/paimon/core/mergetree/lookup_levels.h | 4 +-
src/paimon/core/mergetree/merge_tree_writer.cpp | 14 +++-
src/paimon/core/mergetree/merge_tree_writer.h | 1 +
.../core/mergetree/merge_tree_writer_test.cpp | 40 +++++++--
src/paimon/core/operation/expire_snapshots.cpp | 38 ++++++---
src/paimon/core/operation/expire_snapshots.h | 8 +-
.../core/operation/expire_snapshots_test.cpp | 47 ++++++++++-
.../core/operation/file_store_commit_impl.cpp | 5 +-
.../core/operation/file_store_commit_impl_test.cpp | 11 ++-
.../core/operation/orphan_files_cleaner_impl.cpp | 16 ++++
.../core/operation/orphan_files_cleaner_test.cpp | 33 +++++++-
test/inte/clean_inte_test.cpp | 10 ++-
21 files changed, 328 insertions(+), 48 deletions(-)
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index e2746649..4c9b9b34 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -779,6 +779,7 @@ if(PAIMON_BUILD_TESTS)
core/io/complete_row_tracking_fields_reader_test.cpp
core/io/vector_file_batch_reader_test.cpp
core/io/data_file_meta_test.cpp
+ core/io/data_file_writer_base_test.cpp
core/io/data_file_index_writer_test.cpp
core/io/file_index_options_test.cpp
core/io/file_index_evaluator_test.cpp
diff --git a/src/paimon/core/append/append_only_writer.cpp
b/src/paimon/core/append/append_only_writer.cpp
index d70611d5..778e898f 100644
--- a/src/paimon/core/append/append_only_writer.cpp
+++ b/src/paimon/core/append/append_only_writer.cpp
@@ -284,7 +284,9 @@ Status AppendOnlyWriter::Close() {
for (const auto& file : compact_after_) {
// AppendOnlyCompactManager will rewrite the file and no file upgrade
will occur, so we
// can directly delete the file in compact_after_.
- [[maybe_unused]] auto s = fs->Delete(path_factory_->ToPath(file));
+ for (const std::string& path : path_factory_->CollectFiles(file)) {
+ [[maybe_unused]] Status s = fs->Delete(path);
+ }
}
if (writer_) {
diff --git a/src/paimon/core/append/append_only_writer_test.cpp
b/src/paimon/core/append/append_only_writer_test.cpp
index 4a4d5dc6..835717d4 100644
--- a/src/paimon/core/append/append_only_writer_test.cpp
+++ b/src/paimon/core/append/append_only_writer_test.cpp
@@ -443,6 +443,9 @@ TEST_F(AppendOnlyWriterTest, TestWriteAndClose) {
std::map<std::string, std::string> raw_options;
raw_options[Options::FILE_FORMAT] = "orc";
raw_options[Options::FILE_SYSTEM] = "local";
+ raw_options[Options::TARGET_FILE_ROW_NUM] = "1";
+ raw_options["file-index.bitmap.columns"] = "f0";
+ raw_options[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
ASSERT_OK_AND_ASSIGN(CoreOptions options,
CoreOptions::FromMap(raw_options));
arrow::FieldVector fields = {arrow::field("f0", arrow::utf8())};
@@ -475,11 +478,15 @@ TEST_F(AppendOnlyWriterTest, TestWriteAndClose) {
ASSERT_OK_AND_ASSIGN(auto record_batch, batch_builder.Finish());
ASSERT_OK(writer->Write(std::move(record_batch)));
ASSERT_TRUE(ArrowArrayIsReleased(&arrow_array));
- ASSERT_OK(writer->Close());
auto file_system = std::make_shared<LocalFileSystem>();
std::vector<BasicFileStatus> file_status_list;
ASSERT_OK(file_system->ListDir(dir->Str(), &file_status_list));
+ ASSERT_EQ(2, file_status_list.size());
+
+ ASSERT_OK(writer->Close());
+ file_status_list.clear();
+ ASSERT_OK(file_system->ListDir(dir->Str(), &file_status_list));
ASSERT_TRUE(file_status_list.empty());
}
@@ -634,9 +641,13 @@ TEST_F(AppendOnlyWriterTest,
TestCloseDeletesCompactAfterFiles) {
auto compact_manager = std::make_shared<FakeCompactManager>();
auto compact_after = NewAppendFile("compact-after.orc", 1, 0, 0);
+ compact_after->extra_files = {"compact-after.orc.index"};
auto compact_after_path = path_factory->ToPath(compact_after->file_name);
+ auto compact_after_index_path =
path_factory->ToPath(compact_after->extra_files[0].value());
ASSERT_OK_AND_ASSIGN(auto output,
options.GetFileSystem()->Create(compact_after_path, true));
ASSERT_OK(output->Close());
+ ASSERT_OK_AND_ASSIGN(output,
options.GetFileSystem()->Create(compact_after_index_path, true));
+ ASSERT_OK(output->Close());
auto result =
std::make_shared<CompactResult>(std::vector<std::shared_ptr<DataFileMeta>>{},
@@ -653,8 +664,10 @@ TEST_F(AppendOnlyWriterTest,
TestCloseDeletesCompactAfterFiles) {
ASSERT_OK(writer->Sync());
ASSERT_TRUE(options.GetFileSystem()->Exists(compact_after_path).value());
+
ASSERT_TRUE(options.GetFileSystem()->Exists(compact_after_index_path).value());
ASSERT_OK(writer->Close());
ASSERT_FALSE(options.GetFileSystem()->Exists(compact_after_path).value());
+
ASSERT_FALSE(options.GetFileSystem()->Exists(compact_after_index_path).value());
ASSERT_TRUE(compact_manager->request_cancel_called);
ASSERT_TRUE(compact_manager->wait_called);
ASSERT_TRUE(compact_manager->close_called);
diff --git a/src/paimon/core/io/data_file_index_writer_test.cpp
b/src/paimon/core/io/data_file_index_writer_test.cpp
index 9837b556..524fc139 100644
--- a/src/paimon/core/io/data_file_index_writer_test.cpp
+++ b/src/paimon/core/io/data_file_index_writer_test.cpp
@@ -143,9 +143,10 @@ class DataFileIndexWriterTest : public ::testing::Test {
std::shared_ptr<arrow::Schema> schema_;
};
-TEST_F(DataFileIndexWriterTest, TestBitmapAndRangeBitmapEmbeddedRoundTrip) {
+TEST_F(DataFileIndexWriterTest,
TestMultipleIndexesOnSameColumnEmbeddedRoundTrip) {
ASSERT_OK_AND_ASSIGN(auto writer,
CreateWriter({{"file-index.bitmap.columns", "f0"},
+ {"file-index.bsi.columns", "f0"},
{"file-index.range-bitmap.columns",
"f1"},
{"file-index.range-bitmap.f1.chunk-size", "1KB"},
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"}}));
@@ -159,12 +160,14 @@ TEST_F(DataFileIndexWriterTest,
TestBitmapAndRangeBitmapEmbeddedRoundTrip) {
ASSERT_TRUE(result.extra_files.empty());
ASSERT_OK_AND_ASSIGN(auto reader, CreateReader(result.embedded_index));
- ASSERT_OK_AND_ASSIGN(auto bitmap_readers, ReadColumn(reader.get(), "f0"));
- ASSERT_EQ(1, bitmap_readers.size());
- ASSERT_OK_AND_ASSIGN(auto equal_result,
bitmap_readers[0]->VisitEqual(Literal(1)));
- ASSERT_EQ("{0,2}", equal_result->ToString());
- ASSERT_OK_AND_ASSIGN(auto null_result, bitmap_readers[0]->VisitIsNull());
- ASSERT_EQ("{3}", null_result->ToString());
+ ASSERT_OK_AND_ASSIGN(auto f0_readers, ReadColumn(reader.get(), "f0"));
+ ASSERT_EQ(2, f0_readers.size());
+ for (const std::shared_ptr<FileIndexReader>& f0_reader : f0_readers) {
+ ASSERT_OK_AND_ASSIGN(auto equal_result,
f0_reader->VisitEqual(Literal(1)));
+ ASSERT_EQ("{0,2}", equal_result->ToString());
+ ASSERT_OK_AND_ASSIGN(auto null_result, f0_reader->VisitIsNull());
+ ASSERT_EQ("{3}", null_result->ToString());
+ }
ASSERT_OK_AND_ASSIGN(auto range_readers, ReadColumn(reader.get(), "f1"));
ASSERT_EQ(1, range_readers.size());
diff --git a/src/paimon/core/io/data_file_writer_base.h
b/src/paimon/core/io/data_file_writer_base.h
index 9b3d6e71..a0d01845 100644
--- a/src/paimon/core/io/data_file_writer_base.h
+++ b/src/paimon/core/io/data_file_writer_base.h
@@ -88,6 +88,7 @@ class DataFileWriterBase : public SingleFileWriter<Record,
std::shared_ptr<DataF
/// Extracts the pre-conversion Arrow batch from record for file index
construction, then
/// passes record to the underlying data file writer, which may convert it
to a physical schema.
Status WriteRecordWithFileIndex(Record record) {
+ PAIMON_RETURN_NOT_OK(this->CheckNotClosed());
PAIMON_RETURN_NOT_OK(AddFileIndexBatch(GetFileIndexBatch(record)));
return Base::Write(std::move(record));
}
diff --git a/src/paimon/core/io/data_file_writer_base_test.cpp
b/src/paimon/core/io/data_file_writer_base_test.cpp
new file mode 100644
index 00000000..9a3df741
--- /dev/null
+++ b/src/paimon/core/io/data_file_writer_base_test.cpp
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "paimon/core/io/data_file_writer_base.h"
+
+#include <memory>
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/json_simple.h"
+#include "gtest/gtest.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/io/data_file_index_writer.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/file_index_options.h"
+#include "paimon/defs.h"
+#include "paimon/format/file_format.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+class IndexedDataFileWriter : public DataFileWriterBase<::ArrowArray*> {
+ public:
+ IndexedDataFileWriter() : DataFileWriterBase(/*compression=*/"zstd",
/*converter=*/nullptr) {}
+
+ Status Write(::ArrowArray* batch) override {
+ return WriteRecordWithFileIndex(batch);
+ }
+
+ Result<std::shared_ptr<DataFileMeta>> GetResult() override {
+ return std::shared_ptr<DataFileMeta>();
+ }
+};
+
+TEST(DataFileWriterBaseTest, WriteAfterCloseDoesNotConsumeIndexBatch) {
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ std::shared_ptr<MemoryPool> pool = GetDefaultPool();
+ std::shared_ptr<arrow::Schema> schema = arrow::schema({arrow::field("col",
arrow::int32())});
+ std::shared_ptr<arrow::DataType> data_type =
arrow::struct_(schema->fields());
+ ASSERT_OK_AND_ASSIGN(
+ CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {"file-index.bitmap.columns", "col"},
+ {Options::FILE_INDEX_IN_MANIFEST_THRESHOLD,
"1MB"}}));
+ std::shared_ptr<FileSystem> file_system = options.GetFileSystem();
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", "data-", nullptr));
+ ASSERT_OK_AND_ASSIGN(FileIndexOptions file_index_options,
+ FileIndexOptions::FromCoreOptions(options));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<DataFileIndexWriter> file_index_writer,
+ DataFileIndexWriter::Create(schema, file_index_options, file_system,
path_factory, pool));
+
+ ArrowSchema c_schema;
+ ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<WriterBuilder> writer_builder,
+
options.GetFileFormat()->CreateWriterBuilder(&c_schema,
+
/*batch_size=*/100));
+ IndexedDataFileWriter writer;
+ writer.SetFileIndexWriter(std::move(file_index_writer), schema);
+ ASSERT_OK(writer.Init(file_system, path_factory->NewPath(),
writer_builder));
+
+ std::shared_ptr<arrow::Array> first_array =
+ arrow::ipc::internal::json::ArrayFromJSON(data_type,
R"([[1]])").ValueOrDie();
+ ::ArrowArray first_batch;
+ ASSERT_TRUE(arrow::ExportArray(*first_array, &first_batch).ok());
+ ASSERT_OK(writer.Write(&first_batch));
+ ASSERT_OK(writer.Close());
+
+ std::shared_ptr<arrow::Array> second_array =
+ arrow::ipc::internal::json::ArrayFromJSON(data_type,
R"([[2]])").ValueOrDie();
+ ::ArrowArray second_batch;
+ ASSERT_TRUE(arrow::ExportArray(*second_array, &second_batch).ok());
+ ASSERT_NOK_WITH_MSG(writer.Write(&second_batch), "Writer has already
closed");
+ ASSERT_NE(nullptr, second_batch.release);
+ ArrowArrayRelease(&second_batch);
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/core/io/key_value_data_file_writer_factory.cpp
b/src/paimon/core/io/key_value_data_file_writer_factory.cpp
index e0640ab6..ae3287ce 100644
--- a/src/paimon/core/io/key_value_data_file_writer_factory.cpp
+++ b/src/paimon/core/io/key_value_data_file_writer_factory.cpp
@@ -62,6 +62,7 @@ KeyValueDataFileWriterFactory::CreateWriter() const {
auto writer = std::make_unique<KeyValueDataFileWriter>(
GetFileCompression(), std::move(converter), schema_id_, level_,
file_source_, primary_keys_,
resources.stats_extractor, write_schema_,
path_factory_->IsExternalPath(), pool_);
+ // Changelog files are consumed sequentially and intentionally do not
produce file indexes.
if (!is_changelog_) {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter>
file_index_writer,
CreateFileIndexWriter(write_schema_,
path_factory_));
diff --git
a/src/paimon/core/io/shredding_key_value_data_file_writer_factory.cpp
b/src/paimon/core/io/shredding_key_value_data_file_writer_factory.cpp
index f56ba8d1..b8822324 100644
--- a/src/paimon/core/io/shredding_key_value_data_file_writer_factory.cpp
+++ b/src/paimon/core/io/shredding_key_value_data_file_writer_factory.cpp
@@ -90,6 +90,7 @@ ShreddingKeyValueDataFileWriterFactory::CreateShreddedWriter(
auto writer = std::make_unique<KeyValueDataFileWriter>(
compression, std::move(batch_converter), schema_id_, level_,
file_source_, primary_keys_,
resources.stats_extractor, file_schema,
path_factory_->IsExternalPath(), pool_);
+ // Changelog files are consumed sequentially and intentionally do not
produce file indexes.
if (!is_changelog_) {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter>
file_index_writer,
CreateFileIndexWriter(write_schema_,
path_factory_));
diff --git a/src/paimon/core/io/single_file_writer.h
b/src/paimon/core/io/single_file_writer.h
index 6db3a699..6dd1db9b 100644
--- a/src/paimon/core/io/single_file_writer.h
+++ b/src/paimon/core/io/single_file_writer.h
@@ -133,6 +133,14 @@ class SingleFileWriter : public FileWriter<T, R> {
}
protected:
+ /// Returns an error if this writer has already been closed.
+ Status CheckNotClosed() const {
+ if (PAIMON_UNLIKELY(closed_)) {
+ return Status::Invalid("Writer has already closed!");
+ }
+ return Status::OK();
+ }
+
/// Hook called after Flush() and before Finish() during Close().
/// Subclasses can override to update per-field metadata before the file
is finalized.
virtual Status BeforeFinish() {
@@ -196,9 +204,7 @@ Status SingleFileWriter<T, R>::Init(const
std::shared_ptr<FileSystem>& fs, const
template <typename T, typename R>
Status SingleFileWriter<T, R>::Write(T record) {
- if (PAIMON_UNLIKELY(closed_)) {
- return Status::Invalid("Writer has already closed!");
- }
+ PAIMON_RETURN_NOT_OK(CheckNotClosed());
ScopeGuard guard([this]() -> void { this->Abort(); });
int64_t record_count = 0;
if (!converter_) {
diff --git a/src/paimon/core/mergetree/lookup_levels.h
b/src/paimon/core/mergetree/lookup_levels.h
index 95625956..4fbb26a4 100644
--- a/src/paimon/core/mergetree/lookup_levels.h
+++ b/src/paimon/core/mergetree/lookup_levels.h
@@ -45,6 +45,8 @@ struct RemoteSstFile {
template <typename T>
class LookupLevels : public Levels::DropFileCallback {
public:
+ static constexpr const char* REMOTE_LOOKUP_FILE_SUFFIX = ".lookup";
+
static Result<std::unique_ptr<LookupLevels<T>>> Create(
const std::shared_ptr<FileSystem>& fs, const BinaryRow& partition,
int32_t bucket,
const CoreOptions& options, const std::shared_ptr<SchemaManager>&
schema_manager,
@@ -119,8 +121,6 @@ class LookupLevels : public Levels::DropFileCallback {
Result<std::shared_ptr<PersistProcessor<T>>> GetOrCreateProcessor(
int64_t schema_id, const std::string& ser_version);
- static constexpr const char* REMOTE_LOOKUP_FILE_SUFFIX = ".lookup";
-
private:
std::shared_ptr<MemoryPool> pool_;
std::shared_ptr<FileSystem> fs_;
diff --git a/src/paimon/core/mergetree/merge_tree_writer.cpp
b/src/paimon/core/mergetree/merge_tree_writer.cpp
index 748740f8..88df1b16 100644
--- a/src/paimon/core/mergetree/merge_tree_writer.cpp
+++ b/src/paimon/core/mergetree/merge_tree_writer.cpp
@@ -124,8 +124,7 @@ Status MergeTreeWriter::DoClose() {
}
}
for (const auto& file : delete_files) {
- // Keep Java parity: temporary file cleanup is quiet.
- [[maybe_unused]] auto s =
options_.GetFileSystem()->Delete(path_factory_->ToPath(file));
+ DeleteFileQuietly(file);
}
write_buffer_->Clear();
@@ -266,8 +265,7 @@ Status MergeTreeWriter::UpdateCompactResult(const
std::shared_ptr<CompactResult>
// 2. This file is not the input of upgraded.
if (!in_compact_before(file->file_name) &&
after_files.find(file->file_name) == after_files.end()) {
- auto fs = options_.GetFileSystem();
- [[maybe_unused]] auto s =
fs->Delete(path_factory_->ToPath(file));
+ DeleteFileQuietly(file);
}
} else {
compact_before_.push_back(file);
@@ -282,6 +280,14 @@ Status MergeTreeWriter::UpdateCompactResult(const
std::shared_ptr<CompactResult>
return UpdateCompactDeletionFile(compact_result->DeletionFile());
}
+void MergeTreeWriter::DeleteFileQuietly(const std::shared_ptr<DataFileMeta>&
file) const {
+ std::shared_ptr<FileSystem> fs = options_.GetFileSystem();
+ for (const std::string& path : path_factory_->CollectFiles(file)) {
+ // Keep Java parity: temporary and intermediate file cleanup is quiet.
+ [[maybe_unused]] Status status = fs->Delete(path);
+ }
+}
+
Status MergeTreeWriter::UpdateCompactDeletionFile(
const std::shared_ptr<CompactDeletionFile>& new_deletion_file) {
if (new_deletion_file) {
diff --git a/src/paimon/core/mergetree/merge_tree_writer.h
b/src/paimon/core/mergetree/merge_tree_writer.h
index 575bd76f..9f73e43b 100644
--- a/src/paimon/core/mergetree/merge_tree_writer.h
+++ b/src/paimon/core/mergetree/merge_tree_writer.h
@@ -111,6 +111,7 @@ class MergeTreeWriter : public BatchWriter {
Status TrySyncLatestCompaction(bool blocking);
Status UpdateCompactResult(const std::shared_ptr<CompactResult>&
compact_result);
Status UpdateCompactDeletionFile(const
std::shared_ptr<CompactDeletionFile>& new_deletion_file);
+ void DeleteFileQuietly(const std::shared_ptr<DataFileMeta>& file) const;
private:
MergeTreeWriter(const std::vector<std::string>& trimmed_primary_keys,
diff --git a/src/paimon/core/mergetree/merge_tree_writer_test.cpp
b/src/paimon/core/mergetree/merge_tree_writer_test.cpp
index 1885a976..f4cbaeaf 100644
--- a/src/paimon/core/mergetree/merge_tree_writer_test.cpp
+++ b/src/paimon/core/mergetree/merge_tree_writer_test.cpp
@@ -1266,7 +1266,9 @@ TEST_P(MergeTreeWriterTest, TestCloseBeforePrepareCommit)
{
TEST_P(MergeTreeWriterTest, TestCloseDeletesUncommittedFiles) {
ASSERT_OK_AND_ASSIGN(CoreOptions options,
- CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {"file-index.bitmap.columns",
"f1"},
+
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1B"}}));
auto dir = UniqueTestDirectory::Create();
ASSERT_TRUE(dir);
@@ -1291,10 +1293,13 @@ TEST_P(MergeTreeWriterTest,
TestCloseDeletesUncommittedFiles) {
ASSERT_OK(merge_writer->Compact(/*full_compaction=*/false));
std::string expected_data_file_path = dir->Str() + "/data-" + uuid +
"-0.orc";
+ std::string expected_index_file_path = expected_data_file_path + ".index";
ASSERT_TRUE(options.GetFileSystem()->Exists(expected_data_file_path).value());
+
ASSERT_TRUE(options.GetFileSystem()->Exists(expected_index_file_path).value());
ASSERT_OK(merge_writer->Close());
ASSERT_FALSE(options.GetFileSystem()->Exists(expected_data_file_path).value());
+
ASSERT_FALSE(options.GetFileSystem()->Exists(expected_index_file_path).value());
}
TEST_P(MergeTreeWriterTest, TestAutoFlush) {
@@ -1580,6 +1585,15 @@ TEST_P(MergeTreeWriterTest,
TestUpdateCompactResultDeleteIntermediateFile) {
auto file_a = CreateMeta("file_a", /*level=*/0);
auto file_x = CreateMeta("file_x", /*level=*/0);
auto file_y = CreateMeta("file_y", /*level=*/1);
+ file_x->extra_files = {"file_x.index"};
+
+ std::vector<std::string> file_x_paths = path_factory->CollectFiles(file_x);
+ ASSERT_EQ(2, file_x_paths.size());
+ for (const std::string& path : file_x_paths) {
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<OutputStream> output,
+ options.GetFileSystem()->Create(path,
/*overwrite=*/true));
+ ASSERT_OK(output->Close());
+ }
merge_writer->compact_before_ = {file_a};
merge_writer->compact_after_ = {file_x};
@@ -1590,6 +1604,9 @@ TEST_P(MergeTreeWriterTest,
TestUpdateCompactResultDeleteIntermediateFile) {
ASSERT_OK(merge_writer->UpdateCompactResult(compact_result));
ASSERT_EQ(merge_writer->compact_before_,
std::vector<std::shared_ptr<DataFileMeta>>({file_a}));
ASSERT_EQ(merge_writer->compact_after_,
std::vector<std::shared_ptr<DataFileMeta>>({file_y}));
+ for (const std::string& path : file_x_paths) {
+ ASSERT_FALSE(options.GetFileSystem()->Exists(path).value()) << path;
+ }
}
TEST_P(MergeTreeWriterTest, TestUpdateCompactResultPropagatesChangelog) {
@@ -1706,20 +1723,25 @@ TEST_P(MergeTreeWriterTest,
TestCloseSkipsDeleteForUpgradedFilesInCompactAfter)
std::string intermediate_file_name = "data-intermediate-0.orc";
std::string upgraded_file_path = dir->Str() + "/" + upgraded_file_name;
std::string intermediate_file_path = dir->Str() + "/" +
intermediate_file_name;
+ std::string upgraded_index_path = upgraded_file_path + ".index";
+ std::string intermediate_index_path = intermediate_file_path + ".index";
// Create placeholder files on disk
- ASSERT_OK_AND_ASSIGN(auto out1,
- options.GetFileSystem()->Create(upgraded_file_path,
/*overwrite=*/true));
- ASSERT_OK(out1->Close());
- ASSERT_OK_AND_ASSIGN(auto out2,
options.GetFileSystem()->Create(intermediate_file_path,
-
/*overwrite=*/true));
- ASSERT_OK(out2->Close());
+ const std::vector<std::string> placeholder_paths = {
+ upgraded_file_path, intermediate_file_path, upgraded_index_path,
intermediate_index_path};
+ for (const std::string& path : placeholder_paths) {
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<OutputStream> output,
+ options.GetFileSystem()->Create(path,
/*overwrite=*/true));
+ ASSERT_OK(output->Close());
+ }
ASSERT_TRUE(options.GetFileSystem()->Exists(upgraded_file_path).value());
ASSERT_TRUE(options.GetFileSystem()->Exists(intermediate_file_path).value());
auto upgraded_file = CreateMeta(upgraded_file_name, /*level=*/1);
auto intermediate_file = CreateMeta(intermediate_file_name, /*level=*/1);
+ upgraded_file->extra_files = {upgraded_file_name + ".index"};
+ intermediate_file->extra_files = {intermediate_file_name + ".index"};
// Setup: upgraded_file appears in both compact_before_ and compact_after_
// (simulating an upgrade operation where the file is promoted to a higher
level).
@@ -1732,10 +1754,14 @@ TEST_P(MergeTreeWriterTest,
TestCloseSkipsDeleteForUpgradedFilesInCompactAfter)
// upgraded_file should NOT be deleted (it's in compact_before_)
ASSERT_TRUE(options.GetFileSystem()->Exists(upgraded_file_path).value())
<< "Upgraded file should be preserved because it exists in
compact_before_";
+ ASSERT_TRUE(options.GetFileSystem()->Exists(upgraded_index_path).value())
+ << "Upgraded file index should be preserved with its data file";
// intermediate_file SHOULD be deleted (it's only in compact_after_)
ASSERT_FALSE(options.GetFileSystem()->Exists(intermediate_file_path).value())
<< "Intermediate file should be deleted because it's not in
compact_before_";
+
ASSERT_FALSE(options.GetFileSystem()->Exists(intermediate_index_path).value())
+ << "Intermediate file index should be deleted with its data file";
}
TEST_F(MergeTreeWriterTest, TestSpillWithSameKeyDeduplicate) {
diff --git a/src/paimon/core/operation/expire_snapshots.cpp
b/src/paimon/core/operation/expire_snapshots.cpp
index a5bd2505..9dcf1b5d 100644
--- a/src/paimon/core/operation/expire_snapshots.cpp
+++ b/src/paimon/core/operation/expire_snapshots.cpp
@@ -33,6 +33,7 @@
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/io/data_file_path_factory.h"
#include "paimon/core/manifest/file_kind.h"
#include "paimon/core/manifest/manifest_entry.h"
#include "paimon/core/manifest/manifest_file.h"
@@ -137,6 +138,7 @@ Result<int32_t> ExpireSnapshots::ExpireUntil(int64_t
earliest_snapshot_id,
// Since the data file deletion information for each snapshot is recorded
in the delta part of
// the next snapshot, it is necessary to check the next snapshot.
Otherwise, its data files will
// not be deleted in this round.
+ DataFilePathFactoryCache data_file_path_factory_cache;
for (int64_t id = begin_inclusive_id + 1; id <= end_exclusive_id; id++) {
PAIMON_ASSIGN_OR_RAISE(bool exist,
snapshot_manager_->SnapshotExists(id));
if (!exist) {
@@ -144,7 +146,8 @@ Result<int32_t> ExpireSnapshots::ExpireUntil(int64_t
earliest_snapshot_id,
continue;
}
PAIMON_ASSIGN_OR_RAISE(Snapshot snapshot,
snapshot_manager_->LoadSnapshot(id));
-
PAIMON_RETURN_NOT_OK(CleanUnusedDataFiles(snapshot.DeltaManifestList()));
+ PAIMON_RETURN_NOT_OK(
+ CleanUnusedDataFiles(snapshot.DeltaManifestList(),
&data_file_path_factory_cache));
}
// TODO(jinli.zjw): support delete changelog files
@@ -290,7 +293,8 @@ Status ExpireSnapshots::CleanUnusedManifests(const
std::string& manifest_list_na
return Status::OK();
}
-Status ExpireSnapshots::CleanUnusedDataFiles(const std::string&
manifest_list_name) {
+Status ExpireSnapshots::CleanUnusedDataFiles(
+ const std::string& manifest_list_name, DataFilePathFactoryCache*
data_file_path_factory_cache) {
std::vector<ManifestFileMeta> manifest_file_metas;
auto status = manifest_list_->Read(manifest_list_name, nullptr,
&manifest_file_metas);
if (status.ok()) {
@@ -311,13 +315,28 @@ Status ExpireSnapshots::CleanUnusedDataFiles(const
std::string& manifest_list_na
std::vector<std::future<void>> futures;
ScopeGuard guard([&futures]() { Wait(futures); });
- for (const auto& [data_file_to_delete, entry] : data_files_to_delete) {
- auto delete_file_path = data_file_to_delete;
- futures.push_back(Via(executor_.get(), [this, delete_file_path]() {
- auto status = fs_->Delete(delete_file_path);
- // delete quietly will ignore any status error
- (void)status;
- }));
+ for (const auto& [_, entry] : data_files_to_delete) {
+ std::unordered_map<int32_t, std::shared_ptr<DataFilePathFactory>>&
bucket_factories =
+ (*data_file_path_factory_cache)[entry.Partition()];
+ auto factory_iter = bucket_factories.find(entry.Bucket());
+ if (factory_iter == bucket_factories.end()) {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<DataFilePathFactory>
data_file_path_factory,
+
path_factory_->CreateDataFilePathFactory(entry.Partition(), entry.Bucket()));
+ factory_iter =
+ bucket_factories.emplace(entry.Bucket(),
std::move(data_file_path_factory))
+ .first;
+ }
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory
=
+ factory_iter->second;
+ for (const std::string& delete_file_path :
+ data_file_path_factory->CollectFiles(entry.File())) {
+ futures.push_back(Via(executor_.get(), [this,
delete_file_path]() {
+ auto status = fs_->Delete(delete_file_path);
+ // delete quietly will ignore any status error
+ (void)status;
+ }));
+ }
deletion_buckets_[entry.Partition()].insert(entry.Bucket());
}
}
@@ -334,7 +353,6 @@ Status ExpireSnapshots::GetDataFilesToDelete(
if (entry.Kind() == FileKind::Add()) {
data_files_to_delete->erase(data_file_path);
} else if (entry.Kind() == FileKind::Delete()) {
- // TODO(jinli.zjw): do not support extra files
data_files_to_delete->insert({data_file_path, entry});
} else {
return Status::Invalid(
diff --git a/src/paimon/core/operation/expire_snapshots.h
b/src/paimon/core/operation/expire_snapshots.h
index 238f599e..64af99a8 100644
--- a/src/paimon/core/operation/expire_snapshots.h
+++ b/src/paimon/core/operation/expire_snapshots.h
@@ -37,6 +37,7 @@ namespace paimon {
class Snapshot;
class SnapshotManager;
class FileStorePathFactory;
+class DataFilePathFactory;
class FileSystem;
class ManifestEntry;
class ManifestList;
@@ -56,9 +57,14 @@ class ExpireSnapshots {
Result<int32_t> Expire();
private:
+ using DataFilePathFactoryCache =
+ std::unordered_map<BinaryRow,
+ std::unordered_map<int32_t,
std::shared_ptr<DataFilePathFactory>>>;
+
Result<int32_t> ExpireUntil(int64_t earliest_snapshot_id, int64_t
end_exclusive_id);
- Status CleanUnusedDataFiles(const std::string& manifest_list_name);
+ Status CleanUnusedDataFiles(const std::string& manifest_list_name,
+ DataFilePathFactoryCache*
data_file_path_factory_cache);
Status CleanUnusedManifests(const std::string& manifest_list_name,
const std::set<std::string>& skipping_sets);
Status CleanEmptyDirectories();
diff --git a/src/paimon/core/operation/expire_snapshots_test.cpp
b/src/paimon/core/operation/expire_snapshots_test.cpp
index f3472d5b..f256476d 100644
--- a/src/paimon/core/operation/expire_snapshots_test.cpp
+++ b/src/paimon/core/operation/expire_snapshots_test.cpp
@@ -28,6 +28,7 @@
#include "paimon/common/data/binary_row_writer.h"
#include "paimon/core/core_options.h"
#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/data_file_path_factory.h"
#include "paimon/core/manifest/file_kind.h"
#include "paimon/core/manifest/manifest_entry.h"
#include "paimon/core/manifest/manifest_file.h"
@@ -72,7 +73,9 @@ class ExpireSnapshotsTest : public testing::Test {
FieldMapping::GetPartitionSchema(schema_,
partition_keys_));
fs_ = std::make_shared<LocalFileSystem>();
- test_data_path_ = "tmp";
+ directory_ = UniqueTestDirectory::Create();
+ ASSERT_TRUE(directory_);
+ test_data_path_ = directory_->Str();
path_factory_ = CreateFactory(test_data_path_);
std::shared_ptr<FileFormat> manifest_format =
options.GetManifestFormat();
@@ -132,6 +135,13 @@ class ExpireSnapshotsTest : public testing::Test {
ManifestEntry CreateManifestEntry(const std::string& file_name, int32_t
bucket,
const FileKind& kind) const {
+ return CreateManifestEntry(file_name, bucket, kind,
+ std::vector<std::optional<std::string>>());
+ }
+
+ ManifestEntry CreateManifestEntry(
+ const std::string& file_name, int32_t bucket, const FileKind& kind,
+ const std::vector<std::optional<std::string>>& extra_files) const {
int32_t arity = 2;
BinaryRow row(arity);
BinaryRowWriter writer(&row, 20, mem_pool_.get());
@@ -143,7 +153,7 @@ class ExpireSnapshotsTest : public testing::Test {
file_name, 1024, 8, DataFileMeta::EmptyMinKey(),
DataFileMeta::EmptyMaxKey(),
SimpleStats::EmptyStats(), SimpleStats::EmptyStats(),
/*min_seq_no=*/16,
/*max_seq_no=*/32,
- /*schema_id=*/1, /*level=*/2,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*schema_id=*/1, /*level=*/2, extra_files,
/*creation_time=*/Timestamp(0, 0), /*delete_row_count=*/3,
/*embedded_index=*/nullptr, /*file_source=*/std::nullopt,
/*external_path=*/std::nullopt,
@@ -154,6 +164,7 @@ class ExpireSnapshotsTest : public testing::Test {
private:
std::string test_data_path_;
+ std::unique_ptr<UniqueTestDirectory> directory_;
std::vector<std::string> partition_keys_;
std::shared_ptr<arrow::Schema> schema_;
std::shared_ptr<arrow::Schema> partition_schema_;
@@ -253,4 +264,36 @@ TEST_F(ExpireSnapshotsTest, TestGetDataFileToDelete) {
}
}
+TEST_F(ExpireSnapshotsTest, TestCleanUnusedDataFileDeletesExtraFiles) {
+ auto mgr = std::make_shared<SnapshotManager>(fs_, test_data_path_);
+ ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap({}));
+ ExpireSnapshots expire(mgr, path_factory_, manifest_list_, manifest_file_,
fs_,
+ options.GetExpireConfig(),
options.RealtimeEnabled(), executor_);
+
+ ManifestEntry entry = CreateManifestEntry("file1", /*bucket=*/0,
FileKind::Delete(),
+ {"file1.index", "file1.lookup"});
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<DataFilePathFactory> data_file_path_factory,
+ path_factory_->CreateDataFilePathFactory(entry.Partition(),
entry.Bucket()));
+ std::vector<std::string> files =
data_file_path_factory->CollectFiles(entry.File());
+ ASSERT_EQ(3, files.size());
+ for (const std::string& file : files) {
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<OutputStream> output,
+ fs_->Create(file, /*overwrite=*/true));
+ ASSERT_OK(output->Close());
+ }
+
+ ASSERT_OK_AND_ASSIGN(std::vector<ManifestFileMeta> manifest_files,
+ manifest_file_->Write({entry}));
+ std::pair<std::string, int64_t> manifest_list;
+ ASSERT_OK_AND_ASSIGN(manifest_list, manifest_list_->Write(manifest_files));
+ ExpireSnapshots::DataFilePathFactoryCache data_file_path_factory_cache;
+ ASSERT_OK(expire.CleanUnusedDataFiles(manifest_list.first,
&data_file_path_factory_cache));
+
+ for (const std::string& file : files) {
+ ASSERT_OK_AND_ASSIGN(bool exists, fs_->Exists(file));
+ ASSERT_FALSE(exists) << file;
+ }
+}
+
} // namespace paimon::test
diff --git a/src/paimon/core/operation/file_store_commit_impl.cpp
b/src/paimon/core/operation/file_store_commit_impl.cpp
index 8268e811..736825f7 100644
--- a/src/paimon/core/operation/file_store_commit_impl.cpp
+++ b/src/paimon/core/operation/file_store_commit_impl.cpp
@@ -230,8 +230,9 @@ Status FileStoreCommitImpl::Abort(
append_data_files(compact_increment.ChangelogFiles());
for (const auto& file : data_files_to_delete) {
// Best-effort cleanup: ignore delete failures, aligning with Java
deleteQuietly.
- [[maybe_unused]] Status status =
- fs_->Delete(data_file_path_factory->ToPath(file),
/*recursive=*/false);
+ for (const std::string& path :
data_file_path_factory->CollectFiles(file)) {
+ [[maybe_unused]] Status status = fs_->Delete(path,
/*recursive=*/false);
+ }
}
std::vector<std::shared_ptr<IndexFileMeta>> index_files_to_delete;
diff --git a/src/paimon/core/operation/file_store_commit_impl_test.cpp
b/src/paimon/core/operation/file_store_commit_impl_test.cpp
index a5079d7d..d55f21cb 100644
--- a/src/paimon/core/operation/file_store_commit_impl_test.cpp
+++ b/src/paimon/core/operation/file_store_commit_impl_test.cpp
@@ -1522,6 +1522,8 @@ TEST_F(FileStoreCommitImplTest,
TestAbortDeletesDataAndIndexFiles) {
const int32_t bucket = 0;
auto new_data_file = CreateAppendDataFileMeta("abort-new-data", 1);
auto compact_data_file = CreateAppendDataFileMeta("abort-compact-data", 1);
+ new_data_file->extra_files = {"abort-new-data.index"};
+ compact_data_file->extra_files = {"abort-compact-data.index"};
auto new_index_file = CreateIndexFileMeta("abort-new-index");
auto compact_index_file = CreateIndexFileMeta("abort-compact-index");
@@ -1540,9 +1542,12 @@ TEST_F(FileStoreCommitImplTest,
TestAbortDeletesDataAndIndexFiles) {
commit_impl->path_factory_->CreateDataFilePathFactory(partition, bucket));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<IndexPathFactory> index_pf,
commit_impl->path_factory_->CreateIndexFileFactory(partition, bucket));
- std::vector<std::string> paths = {
- data_pf->ToPath(new_data_file), data_pf->ToPath(compact_data_file),
- index_pf->ToPath(new_index_file),
index_pf->ToPath(compact_index_file)};
+ std::vector<std::string> paths = {data_pf->ToPath(new_data_file),
+
data_pf->ToPath(new_data_file->extra_files[0].value()),
+ data_pf->ToPath(compact_data_file),
+
data_pf->ToPath(compact_data_file->extra_files[0].value()),
+ index_pf->ToPath(new_index_file),
+ index_pf->ToPath(compact_index_file)};
for (const auto& path : paths) {
ASSERT_OK(file_system_->WriteFile(path, /*content=*/"",
/*overwrite=*/false));
ASSERT_OK_AND_ASSIGN(bool exist, file_system_->Exists(path));
diff --git a/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
b/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
index 015d1340..f42708c8 100644
--- a/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
+++ b/src/paimon/core/operation/orphan_files_cleaner_impl.cpp
@@ -31,10 +31,12 @@
#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/common/utils/string_utils.h"
+#include "paimon/core/io/data_file_path_factory.h"
#include "paimon/core/manifest/manifest_entry.h"
#include "paimon/core/manifest/manifest_file.h"
#include "paimon/core/manifest/manifest_file_meta.h"
#include "paimon/core/manifest/manifest_list.h"
+#include "paimon/core/mergetree/lookup_levels.h"
#include "paimon/core/operation/commit/realtime_commit_properties.h"
#include "paimon/core/operation/metrics/clean_metrics.h"
#include "paimon/core/snapshot.h"
@@ -86,6 +88,14 @@ bool OrphanFilesCleanerImpl::SupportToClean(const
std::string& file_name) {
return true;
}
}
+ static std::vector<std::string> supported_extra_file_suffixes = {
+ DataFilePathFactory::INDEX_PATH_SUFFIX,
LookupLevels<bool>::REMOTE_LOOKUP_FILE_SUFFIX};
+ for (const std::string& suffix : supported_extra_file_suffixes) {
+ if (StringUtils::StartsWith(file_name, "data-") &&
+ StringUtils::EndsWith(file_name, suffix)) {
+ return true;
+ }
+ }
return StringUtils::EndsWith(file_name, ".offsets");
}
@@ -319,6 +329,12 @@ Result<std::set<std::string>>
OrphanFilesCleanerImpl::GetUsedFilesBySnapshot(
manifest.FileName(), /*filter=*/nullptr, &manifest_entries));
for (const auto& manifest_entry : manifest_entries) {
used_files.insert(manifest_entry.FileName());
+ for (const std::optional<std::string>& extra_file :
+ manifest_entry.File()->extra_files) {
+ if (extra_file) {
+ used_files.insert(extra_file.value());
+ }
+ }
}
}
diff --git a/src/paimon/core/operation/orphan_files_cleaner_test.cpp
b/src/paimon/core/operation/orphan_files_cleaner_test.cpp
index 7749b692..b2774b20 100644
--- a/src/paimon/core/operation/orphan_files_cleaner_test.cpp
+++ b/src/paimon/core/operation/orphan_files_cleaner_test.cpp
@@ -53,8 +53,10 @@ TEST(OrphanFilesCleanerTest, TestSupportToClean) {
ASSERT_FALSE(OrphanFilesCleanerImpl::SupportToClean("bucket-0"));
ASSERT_FALSE(OrphanFilesCleanerImpl::SupportToClean(
"changelog-ce64d06d-c4cd-456b-a1b3-ae570042620f-0.parquet"));
- ASSERT_FALSE(OrphanFilesCleanerImpl::SupportToClean(
+ ASSERT_TRUE(OrphanFilesCleanerImpl::SupportToClean(
"data-5515726b-0f0f-4556-a942-e795e9f94c4a-0.orc.index"));
+ ASSERT_TRUE(OrphanFilesCleanerImpl::SupportToClean(
+
"data-5515726b-0f0f-4556-a942-e795e9f94c4a-0.orc.128.processor.v1.lookup"));
ASSERT_FALSE(
OrphanFilesCleanerImpl::SupportToClean("index-aa60193d-d7cd-434f-bc1a-c1adb210e1f7-0"));
ASSERT_FALSE(
@@ -123,6 +125,35 @@ TEST(OrphanFilesCleanerTest, TestTableWithIndex) {
ASSERT_TRUE(cleaned_paths.empty());
}
+TEST(OrphanFilesCleanerTest, TestCleanOrphanExtraFiles) {
+ std::string test_data_path =
+ paimon::test::GetDataDir() +
"/orc/append_with_bsi.db/append_with_bsi/";
+ auto dir = UniqueTestDirectory::Create();
+ std::string table_path = dir->Str();
+ ASSERT_TRUE(TestUtil::CopyDirectory(test_data_path, table_path));
+ auto file_system = std::make_shared<LocalFileSystem>();
+ std::string orphan_index_path =
+ PathUtil::JoinPath(table_path, "bucket-0/data-orphan-0.orc.index");
+ std::string orphan_lookup_path =
+ PathUtil::JoinPath(table_path,
"bucket-0/data-orphan-0.orc.128.processor.v1.lookup");
+ ASSERT_OK(file_system->WriteFile(orphan_index_path, "orphan",
/*overwrite=*/true));
+ ASSERT_OK(file_system->WriteFile(orphan_lookup_path, "orphan",
/*overwrite=*/true));
+
+ CleanContextBuilder clean_context_builder(table_path);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<CleanContext> clean_context,
+ clean_context_builder.AddOption(Options::FILE_SYSTEM,
"local")
+
.WithOlderThanMs(std::numeric_limits<int64_t>::max())
+ .Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<OrphanFilesCleaner> cleaner,
+ OrphanFilesCleaner::Create(std::move(clean_context)));
+ ASSERT_OK_AND_ASSIGN(std::set<std::string> cleaned_paths,
cleaner->Clean());
+ ASSERT_EQ(cleaned_paths, std::set<std::string>({orphan_index_path,
orphan_lookup_path}));
+ for (const std::string& path : cleaned_paths) {
+ ASSERT_OK_AND_ASSIGN(bool exists, file_system->Exists(path));
+ ASSERT_FALSE(exists) << path;
+ }
+}
+
TEST(OrphanFilesCleanerTest, TestTableWithBrokenSnapshot) {
std::string test_data_path = paimon::test::GetDataDir() +
"/orc/append_09.db/append_09/";
auto file_system = std::make_shared<LocalFileSystem>();
diff --git a/test/inte/clean_inte_test.cpp b/test/inte/clean_inte_test.cpp
index 3f7f353e..ec418d2c 100644
--- a/test/inte/clean_inte_test.cpp
+++ b/test/inte/clean_inte_test.cpp
@@ -671,8 +671,8 @@ TEST_F(CleanInteTest, TestOrphanFilesClean) {
.Finish());
ASSERT_OK_AND_ASSIGN(auto cleaner,
OrphanFilesCleaner::Create(std::move(clean_context)));
ASSERT_OK_AND_ASSIGN(std::set<std::string> cleaned_paths,
cleaner->Clean());
- ASSERT_TRUE(CheckEqual(
- cleaned_paths, {"data-orphan2.orc", "manifest-orphan",
".manifest-orphan.uuid.tmp"}));
+ ASSERT_TRUE(CheckEqual(cleaned_paths, {"data-orphan2.orc",
"data-orphan2.orc.index",
+ "manifest-orphan",
".manifest-orphan.uuid.tmp"}));
}
}
@@ -782,7 +782,8 @@ TEST_F(CleanInteTest,
TestOrphanFilesCleanWithFileRetainCondition) {
.Finish());
ASSERT_OK_AND_ASSIGN(auto cleaner,
OrphanFilesCleaner::Create(std::move(clean_context)));
ASSERT_OK_AND_ASSIGN(std::set<std::string> cleaned_paths,
cleaner->Clean());
- ASSERT_TRUE(CheckEqual(cleaned_paths, {"manifest-orphan",
".manifest-orphan.uuid.tmp"}));
+ ASSERT_TRUE(CheckEqual(
+ cleaned_paths, {"data-orphan2.orc.index", "manifest-orphan",
".manifest-orphan.uuid.tmp"}));
}
TEST_F(CleanInteTest, TestOrphanFilesCleanWithIOException) {
@@ -921,7 +922,8 @@ TEST_F(CleanInteTest, TestOrphanFilesCleanWithIOException) {
}
ASSERT_OK(clean_result);
- std::set<std::string> expected_orphans = {"data-orphan2.orc",
"manifest-orphan",
+ std::set<std::string> expected_orphans = {"data-orphan2.orc",
"data-orphan2.orc.index",
+ "manifest-orphan",
".manifest-orphan.uuid.tmp"};
// In the first clean, IO errors may already have been triggered in
// TryBestListingDirs or MinimalTryBestListingDirs. Those errors
are handled quietly,