Skip to content

Commit 7351dad

Browse files
authored
feat(core): complete file index write lifecycle (#279)
1 parent f26b62c commit 7351dad

21 files changed

Lines changed: 328 additions & 48 deletions

‎src/paimon/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@ if(PAIMON_BUILD_TESTS)
779779
core/io/complete_row_tracking_fields_reader_test.cpp
780780
core/io/vector_file_batch_reader_test.cpp
781781
core/io/data_file_meta_test.cpp
782+
core/io/data_file_writer_base_test.cpp
782783
core/io/data_file_index_writer_test.cpp
783784
core/io/file_index_options_test.cpp
784785
core/io/file_index_evaluator_test.cpp

‎src/paimon/core/append/append_only_writer.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ Status AppendOnlyWriter::Close() {
284284
for (const auto& file : compact_after_) {
285285
// AppendOnlyCompactManager will rewrite the file and no file upgrade will occur, so we
286286
// can directly delete the file in compact_after_.
287-
[[maybe_unused]] auto s = fs->Delete(path_factory_->ToPath(file));
287+
for (const std::string& path : path_factory_->CollectFiles(file)) {
288+
[[maybe_unused]] Status s = fs->Delete(path);
289+
}
288290
}
289291

290292
if (writer_) {

‎src/paimon/core/append/append_only_writer_test.cpp‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,9 @@ TEST_F(AppendOnlyWriterTest, TestWriteAndClose) {
443443
std::map<std::string, std::string> raw_options;
444444
raw_options[Options::FILE_FORMAT] = "orc";
445445
raw_options[Options::FILE_SYSTEM] = "local";
446+
raw_options[Options::TARGET_FILE_ROW_NUM] = "1";
447+
raw_options["file-index.bitmap.columns"] = "f0";
448+
raw_options[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
446449
ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap(raw_options));
447450

448451
arrow::FieldVector fields = {arrow::field("f0", arrow::utf8())};
@@ -475,11 +478,15 @@ TEST_F(AppendOnlyWriterTest, TestWriteAndClose) {
475478
ASSERT_OK_AND_ASSIGN(auto record_batch, batch_builder.Finish());
476479
ASSERT_OK(writer->Write(std::move(record_batch)));
477480
ASSERT_TRUE(ArrowArrayIsReleased(&arrow_array));
478-
ASSERT_OK(writer->Close());
479481

480482
auto file_system = std::make_shared<LocalFileSystem>();
481483
std::vector<BasicFileStatus> file_status_list;
482484
ASSERT_OK(file_system->ListDir(dir->Str(), &file_status_list));
485+
ASSERT_EQ(2, file_status_list.size());
486+
487+
ASSERT_OK(writer->Close());
488+
file_status_list.clear();
489+
ASSERT_OK(file_system->ListDir(dir->Str(), &file_status_list));
483490
ASSERT_TRUE(file_status_list.empty());
484491
}
485492

@@ -634,9 +641,13 @@ TEST_F(AppendOnlyWriterTest, TestCloseDeletesCompactAfterFiles) {
634641
auto compact_manager = std::make_shared<FakeCompactManager>();
635642

636643
auto compact_after = NewAppendFile("compact-after.orc", 1, 0, 0);
644+
compact_after->extra_files = {"compact-after.orc.index"};
637645
auto compact_after_path = path_factory->ToPath(compact_after->file_name);
646+
auto compact_after_index_path = path_factory->ToPath(compact_after->extra_files[0].value());
638647
ASSERT_OK_AND_ASSIGN(auto output, options.GetFileSystem()->Create(compact_after_path, true));
639648
ASSERT_OK(output->Close());
649+
ASSERT_OK_AND_ASSIGN(output, options.GetFileSystem()->Create(compact_after_index_path, true));
650+
ASSERT_OK(output->Close());
640651

641652
auto result =
642653
std::make_shared<CompactResult>(std::vector<std::shared_ptr<DataFileMeta>>{},
@@ -653,8 +664,10 @@ TEST_F(AppendOnlyWriterTest, TestCloseDeletesCompactAfterFiles) {
653664

654665
ASSERT_OK(writer->Sync());
655666
ASSERT_TRUE(options.GetFileSystem()->Exists(compact_after_path).value());
667+
ASSERT_TRUE(options.GetFileSystem()->Exists(compact_after_index_path).value());
656668
ASSERT_OK(writer->Close());
657669
ASSERT_FALSE(options.GetFileSystem()->Exists(compact_after_path).value());
670+
ASSERT_FALSE(options.GetFileSystem()->Exists(compact_after_index_path).value());
658671
ASSERT_TRUE(compact_manager->request_cancel_called);
659672
ASSERT_TRUE(compact_manager->wait_called);
660673
ASSERT_TRUE(compact_manager->close_called);

‎src/paimon/core/io/data_file_index_writer_test.cpp‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,10 @@ class DataFileIndexWriterTest : public ::testing::Test {
143143
std::shared_ptr<arrow::Schema> schema_;
144144
};
145145

146-
TEST_F(DataFileIndexWriterTest, TestBitmapAndRangeBitmapEmbeddedRoundTrip) {
146+
TEST_F(DataFileIndexWriterTest, TestMultipleIndexesOnSameColumnEmbeddedRoundTrip) {
147147
ASSERT_OK_AND_ASSIGN(auto writer,
148148
CreateWriter({{"file-index.bitmap.columns", "f0"},
149+
{"file-index.bsi.columns", "f0"},
149150
{"file-index.range-bitmap.columns", "f1"},
150151
{"file-index.range-bitmap.f1.chunk-size", "1KB"},
151152
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"}}));
@@ -159,12 +160,14 @@ TEST_F(DataFileIndexWriterTest, TestBitmapAndRangeBitmapEmbeddedRoundTrip) {
159160
ASSERT_TRUE(result.extra_files.empty());
160161
ASSERT_OK_AND_ASSIGN(auto reader, CreateReader(result.embedded_index));
161162

162-
ASSERT_OK_AND_ASSIGN(auto bitmap_readers, ReadColumn(reader.get(), "f0"));
163-
ASSERT_EQ(1, bitmap_readers.size());
164-
ASSERT_OK_AND_ASSIGN(auto equal_result, bitmap_readers[0]->VisitEqual(Literal(1)));
165-
ASSERT_EQ("{0,2}", equal_result->ToString());
166-
ASSERT_OK_AND_ASSIGN(auto null_result, bitmap_readers[0]->VisitIsNull());
167-
ASSERT_EQ("{3}", null_result->ToString());
163+
ASSERT_OK_AND_ASSIGN(auto f0_readers, ReadColumn(reader.get(), "f0"));
164+
ASSERT_EQ(2, f0_readers.size());
165+
for (const std::shared_ptr<FileIndexReader>& f0_reader : f0_readers) {
166+
ASSERT_OK_AND_ASSIGN(auto equal_result, f0_reader->VisitEqual(Literal(1)));
167+
ASSERT_EQ("{0,2}", equal_result->ToString());
168+
ASSERT_OK_AND_ASSIGN(auto null_result, f0_reader->VisitIsNull());
169+
ASSERT_EQ("{3}", null_result->ToString());
170+
}
168171

169172
ASSERT_OK_AND_ASSIGN(auto range_readers, ReadColumn(reader.get(), "f1"));
170173
ASSERT_EQ(1, range_readers.size());

‎src/paimon/core/io/data_file_writer_base.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class DataFileWriterBase : public SingleFileWriter<Record, std::shared_ptr<DataF
8888
/// Extracts the pre-conversion Arrow batch from record for file index construction, then
8989
/// passes record to the underlying data file writer, which may convert it to a physical schema.
9090
Status WriteRecordWithFileIndex(Record record) {
91+
PAIMON_RETURN_NOT_OK(this->CheckNotClosed());
9192
PAIMON_RETURN_NOT_OK(AddFileIndexBatch(GetFileIndexBatch(record)));
9293
return Base::Write(std::move(record));
9394
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
#include "paimon/core/io/data_file_writer_base.h"
20+
21+
#include <memory>
22+
#include <utility>
23+
24+
#include "arrow/api.h"
25+
#include "arrow/c/bridge.h"
26+
#include "arrow/ipc/json_simple.h"
27+
#include "gtest/gtest.h"
28+
#include "paimon/core/core_options.h"
29+
#include "paimon/core/io/data_file_index_writer.h"
30+
#include "paimon/core/io/data_file_path_factory.h"
31+
#include "paimon/core/io/file_index_options.h"
32+
#include "paimon/defs.h"
33+
#include "paimon/format/file_format.h"
34+
#include "paimon/memory/memory_pool.h"
35+
#include "paimon/testing/utils/testharness.h"
36+
37+
namespace paimon::test {
38+
39+
class IndexedDataFileWriter : public DataFileWriterBase<::ArrowArray*> {
40+
public:
41+
IndexedDataFileWriter() : DataFileWriterBase(/*compression=*/"zstd", /*converter=*/nullptr) {}
42+
43+
Status Write(::ArrowArray* batch) override {
44+
return WriteRecordWithFileIndex(batch);
45+
}
46+
47+
Result<std::shared_ptr<DataFileMeta>> GetResult() override {
48+
return std::shared_ptr<DataFileMeta>();
49+
}
50+
};
51+
52+
TEST(DataFileWriterBaseTest, WriteAfterCloseDoesNotConsumeIndexBatch) {
53+
auto dir = UniqueTestDirectory::Create();
54+
ASSERT_TRUE(dir);
55+
std::shared_ptr<MemoryPool> pool = GetDefaultPool();
56+
std::shared_ptr<arrow::Schema> schema = arrow::schema({arrow::field("col", arrow::int32())});
57+
std::shared_ptr<arrow::DataType> data_type = arrow::struct_(schema->fields());
58+
ASSERT_OK_AND_ASSIGN(
59+
CoreOptions options,
60+
CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
61+
{"file-index.bitmap.columns", "col"},
62+
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"}}));
63+
std::shared_ptr<FileSystem> file_system = options.GetFileSystem();
64+
auto path_factory = std::make_shared<DataFilePathFactory>();
65+
ASSERT_OK(path_factory->Init(dir->Str(), "orc", "data-", nullptr));
66+
ASSERT_OK_AND_ASSIGN(FileIndexOptions file_index_options,
67+
FileIndexOptions::FromCoreOptions(options));
68+
ASSERT_OK_AND_ASSIGN(
69+
std::unique_ptr<DataFileIndexWriter> file_index_writer,
70+
DataFileIndexWriter::Create(schema, file_index_options, file_system, path_factory, pool));
71+
72+
ArrowSchema c_schema;
73+
ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
74+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<WriterBuilder> writer_builder,
75+
options.GetFileFormat()->CreateWriterBuilder(&c_schema,
76+
/*batch_size=*/100));
77+
IndexedDataFileWriter writer;
78+
writer.SetFileIndexWriter(std::move(file_index_writer), schema);
79+
ASSERT_OK(writer.Init(file_system, path_factory->NewPath(), writer_builder));
80+
81+
std::shared_ptr<arrow::Array> first_array =
82+
arrow::ipc::internal::json::ArrayFromJSON(data_type, R"([[1]])").ValueOrDie();
83+
::ArrowArray first_batch;
84+
ASSERT_TRUE(arrow::ExportArray(*first_array, &first_batch).ok());
85+
ASSERT_OK(writer.Write(&first_batch));
86+
ASSERT_OK(writer.Close());
87+
88+
std::shared_ptr<arrow::Array> second_array =
89+
arrow::ipc::internal::json::ArrayFromJSON(data_type, R"([[2]])").ValueOrDie();
90+
::ArrowArray second_batch;
91+
ASSERT_TRUE(arrow::ExportArray(*second_array, &second_batch).ok());
92+
ASSERT_NOK_WITH_MSG(writer.Write(&second_batch), "Writer has already closed");
93+
ASSERT_NE(nullptr, second_batch.release);
94+
ArrowArrayRelease(&second_batch);
95+
}
96+
97+
} // namespace paimon::test

‎src/paimon/core/io/key_value_data_file_writer_factory.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ KeyValueDataFileWriterFactory::CreateWriter() const {
6262
auto writer = std::make_unique<KeyValueDataFileWriter>(
6363
GetFileCompression(), std::move(converter), schema_id_, level_, file_source_, primary_keys_,
6464
resources.stats_extractor, write_schema_, path_factory_->IsExternalPath(), pool_);
65+
// Changelog files are consumed sequentially and intentionally do not produce file indexes.
6566
if (!is_changelog_) {
6667
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter> file_index_writer,
6768
CreateFileIndexWriter(write_schema_, path_factory_));

‎src/paimon/core/io/shredding_key_value_data_file_writer_factory.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ ShreddingKeyValueDataFileWriterFactory::CreateShreddedWriter(
9090
auto writer = std::make_unique<KeyValueDataFileWriter>(
9191
compression, std::move(batch_converter), schema_id_, level_, file_source_, primary_keys_,
9292
resources.stats_extractor, file_schema, path_factory_->IsExternalPath(), pool_);
93+
// Changelog files are consumed sequentially and intentionally do not produce file indexes.
9394
if (!is_changelog_) {
9495
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter> file_index_writer,
9596
CreateFileIndexWriter(write_schema_, path_factory_));

‎src/paimon/core/io/single_file_writer.h‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ class SingleFileWriter : public FileWriter<T, R> {
133133
}
134134

135135
protected:
136+
/// Returns an error if this writer has already been closed.
137+
Status CheckNotClosed() const {
138+
if (PAIMON_UNLIKELY(closed_)) {
139+
return Status::Invalid("Writer has already closed!");
140+
}
141+
return Status::OK();
142+
}
143+
136144
/// Hook called after Flush() and before Finish() during Close().
137145
/// Subclasses can override to update per-field metadata before the file is finalized.
138146
virtual Status BeforeFinish() {
@@ -196,9 +204,7 @@ Status SingleFileWriter<T, R>::Init(const std::shared_ptr<FileSystem>& fs, const
196204

197205
template <typename T, typename R>
198206
Status SingleFileWriter<T, R>::Write(T record) {
199-
if (PAIMON_UNLIKELY(closed_)) {
200-
return Status::Invalid("Writer has already closed!");
201-
}
207+
PAIMON_RETURN_NOT_OK(CheckNotClosed());
202208
ScopeGuard guard([this]() -> void { this->Abort(); });
203209
int64_t record_count = 0;
204210
if (!converter_) {

‎src/paimon/core/mergetree/lookup_levels.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ struct RemoteSstFile {
4545
template <typename T>
4646
class LookupLevels : public Levels::DropFileCallback {
4747
public:
48+
static constexpr const char* REMOTE_LOOKUP_FILE_SUFFIX = ".lookup";
49+
4850
static Result<std::unique_ptr<LookupLevels<T>>> Create(
4951
const std::shared_ptr<FileSystem>& fs, const BinaryRow& partition, int32_t bucket,
5052
const CoreOptions& options, const std::shared_ptr<SchemaManager>& schema_manager,
@@ -119,8 +121,6 @@ class LookupLevels : public Levels::DropFileCallback {
119121
Result<std::shared_ptr<PersistProcessor<T>>> GetOrCreateProcessor(
120122
int64_t schema_id, const std::string& ser_version);
121123

122-
static constexpr const char* REMOTE_LOOKUP_FILE_SUFFIX = ".lookup";
123-
124124
private:
125125
std::shared_ptr<MemoryPool> pool_;
126126
std::shared_ptr<FileSystem> fs_;

0 commit comments

Comments
 (0)