This is an automated email from the ASF dual-hosted git repository.
SteNicholas 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 8a110d48 feat(file-index): support writing file indexes (#210)
8a110d48 is described below
commit 8a110d488413adbafca638315a5062cd99dd26e3
Author: Zhang Jiawei <[email protected]>
AuthorDate: Thu Aug 20 15:24:41 2026 +0800
feat(file-index): support writing file indexes (#210)
---
include/paimon/defs.h | 4 +
include/paimon/file_index/file_index_format.h | 33 ++-
src/paimon/CMakeLists.txt | 6 +
src/paimon/common/defs.cpp | 1 +
src/paimon/common/file_index/file_index_format.cpp | 135 +++++++++++
.../common/file_index/file_index_format_test.cpp | 28 ++-
src/paimon/common/io/byte_array_output_stream.cpp | 80 +++++++
src/paimon/common/io/byte_array_output_stream.h | 69 ++++++
.../common/io/byte_array_output_stream_test.cpp | 86 +++++++
.../common/io/data_input_output_stream_test.cpp | 8 +-
.../common/io/memory_segment_output_stream.cpp | 6 +-
.../io/memory_segment_output_stream_test.cpp | 12 +
src/paimon/core/append/append_only_writer_test.cpp | 37 +++
src/paimon/core/core_options.cpp | 8 +
src/paimon/core/core_options.h | 1 +
src/paimon/core/core_options_test.cpp | 3 +
.../core/io/append_data_file_writer_factory.cpp | 6 +
src/paimon/core/io/data_file_index_writer.cpp | 179 +++++++++++++++
src/paimon/core/io/data_file_index_writer.h | 100 ++++++++
src/paimon/core/io/data_file_index_writer_test.cpp | 253 +++++++++++++++++++++
src/paimon/core/io/data_file_writer.cpp | 25 +-
src/paimon/core/io/data_file_writer.h | 17 +-
src/paimon/core/io/data_file_writer_base.h | 145 ++++++++++++
src/paimon/core/io/data_file_writer_factory.cpp | 15 ++
src/paimon/core/io/data_file_writer_factory.h | 6 +
src/paimon/core/io/file_index_options.cpp | 109 +++++++++
..._file_writer_factory.h => file_index_options.h} | 59 ++---
src/paimon/core/io/file_index_options_test.cpp | 58 +++++
src/paimon/core/io/key_value_data_file_writer.cpp | 29 +--
src/paimon/core/io/key_value_data_file_writer.h | 19 +-
.../core/io/key_value_data_file_writer_factory.cpp | 6 +
.../shredding_append_data_file_writer_factory.cpp | 6 +
...hredding_key_value_data_file_writer_factory.cpp | 6 +
src/paimon/core/io/single_file_writer.h | 25 +-
test/inte/write_and_read_inte_test.cpp | 83 +++++++
35 files changed, 1536 insertions(+), 127 deletions(-)
diff --git a/include/paimon/defs.h b/include/paimon/defs.h
index d1ebf507..e944587f 100644
--- a/include/paimon/defs.h
+++ b/include/paimon/defs.h
@@ -405,6 +405,10 @@ struct PAIMON_EXPORT Options {
/// "file-index.read.enabled" - Whether enabled read file index. Default
value is "true".
static const char FILE_INDEX_READ_ENABLED[];
+ /// "file-index.in-manifest-threshold" - The threshold to store file index
bytes in the
+ /// manifest. Default value is 500B.
+ static const char FILE_INDEX_IN_MANIFEST_THRESHOLD[];
+
/// "data-file.external-paths" - The external paths where the data of this
table will be
/// written, multiple elements separated by commas.
static const char DATA_FILE_EXTERNAL_PATHS[];
diff --git a/include/paimon/file_index/file_index_format.h
b/include/paimon/file_index/file_index_format.h
index b46dee8c..3993b624 100644
--- a/include/paimon/file_index/file_index_format.h
+++ b/include/paimon/file_index/file_index_format.h
@@ -19,6 +19,7 @@
#pragma once
#include <cstdint>
+#include <map>
#include <memory>
#include <string>
#include <vector>
@@ -32,6 +33,8 @@ struct ArrowSchema;
namespace paimon {
class InputStream;
class MemoryPool;
+class Bytes;
+class OutputStream;
/// Defines the on-disk format and versioning for Paimon file-level indexes.
/// File index file format. Put all column and offset in the header.
@@ -88,9 +91,15 @@ class MemoryPool;
class PAIMON_EXPORT FileIndexFormat {
public:
class Reader;
+ class Writer;
+
+ /// Serialized file indexes grouped as column name -> index type -> index
bytes. A null bytes
+ /// pointer represents an empty index for that column and index type.
+ /// For example, indexes["col1"]["bsi"] = <bytes>;
+ using ColumnIndexes = std::map<std::string, std::map<std::string,
std::shared_ptr<Bytes>>>;
+
/// Creates a `Reader` to parse a index file (may contain multiple
indexes) from the given input
/// stream.
- ///
/// @param input_stream Input stream containing serialized index data.
/// @param pool Memory pool for temporary allocations during reading.
/// @return A unique pointer to a `Reader` on success, or an error if the
stream is invalid
@@ -98,18 +107,38 @@ class PAIMON_EXPORT FileIndexFormat {
static Result<std::unique_ptr<Reader>> CreateReader(
const std::shared_ptr<InputStream>& input_stream, const
std::shared_ptr<MemoryPool>& pool);
+ /// Creates a `Writer` which serializes a complete V1 file index container.
+ ///
+ /// @param output_stream Destination stream for serialized index data.
+ /// @param pool Memory pool for writer-side allocations.
+ /// @return A unique pointer to a `Writer` on success.
+ static Result<std::unique_ptr<Writer>> CreateWriter(
+ const std::shared_ptr<OutputStream>& output_stream,
+ const std::shared_ptr<MemoryPool>& pool);
+
public:
static const int64_t MAGIC;
static const int32_t EMPTY_INDEX_FLAG;
static const int32_t V_1;
};
+/// Writer for file index file.
+class FileIndexFormat::Writer {
+ public:
+ virtual ~Writer() = default;
+
+ /// Writes all column indexes. This is a terminal, one-shot operation.
+ virtual Status WriteColumnIndexes(const FileIndexFormat::ColumnIndexes&
indexes) = 0;
+
+ /// Flushes and closes the output stream supplied to `CreateWriter()`.
+ virtual Status Close() = 0;
+};
+
/// Reader for file index file.
class FileIndexFormat::Reader {
public:
virtual ~Reader() = default;
/// Reads index data for a specific column from the index file.
- ///
/// @param column_name Name of the column to retrieve index data for.
/// @param arrow_schema Arrow schema that must contain a field
corresponding to `column_name`.
/// @return A vector of shared pointers to FileIndexReader objects, each
corresponding to a
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index 9b0807b6..bdb11005 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -85,6 +85,7 @@ set(PAIMON_COMMON_SRCS
common/global_index/global_indexer_factory.cpp
common/io/buffered_input_stream.cpp
common/io/byte_array_input_stream.cpp
+ common/io/byte_array_output_stream.cpp
common/io/data_input_stream.cpp
common/io/data_output_stream.cpp
common/io/memory_segment_output_stream.cpp
@@ -270,6 +271,8 @@ set(PAIMON_CORE_SRCS
core/io/data_file_meta.cpp
core/io/data_file_meta_serializer.cpp
core/io/data_file_path_factory.cpp
+ core/io/data_file_index_writer.cpp
+ core/io/file_index_options.cpp
core/io/append_data_file_writer_factory.cpp
core/io/blob_data_file_writer_factory.cpp
core/io/data_file_writer_factory.cpp
@@ -577,6 +580,7 @@ if(PAIMON_BUILD_TESTS)
common/global_index/rangebitmap/range_bitmap_global_index_test.cpp
common/global_index/wrap/file_index_reader_wrapper_test.cpp
common/io/byte_array_input_stream_test.cpp
+ common/io/byte_array_output_stream_test.cpp
common/io/data_input_output_stream_test.cpp
common/io/buffered_input_stream_test.cpp
common/io/memory_segment_output_stream_test.cpp
@@ -752,6 +756,8 @@ 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_index_writer_test.cpp
+ core/io/file_index_options_test.cpp
core/io/file_index_evaluator_test.cpp
core/io/single_file_writer_test.cpp
core/io/rolling_blob_file_writer_test.cpp
diff --git a/src/paimon/common/defs.cpp b/src/paimon/common/defs.cpp
index bac4f16f..ef35940e 100644
--- a/src/paimon/common/defs.cpp
+++ b/src/paimon/common/defs.cpp
@@ -100,6 +100,7 @@ const char
Options::PARTIAL_UPDATE_REMOVE_RECORD_ON_SEQUENCE_GROUP[] =
const char Options::SCAN_FALLBACK_BRANCH[] = "scan.fallback-branch";
const char Options::BRANCH[] = "branch";
const char Options::FILE_INDEX_READ_ENABLED[] = "file-index.read.enabled";
+const char Options::FILE_INDEX_IN_MANIFEST_THRESHOLD[] =
"file-index.in-manifest-threshold";
const char Options::DATA_FILE_EXTERNAL_PATHS[] = "data-file.external-paths";
const char Options::DATA_FILE_EXTERNAL_PATHS_STRATEGY[] =
"data-file.external-paths.strategy";
const char Options::DATA_FILE_PREFIX[] = "data-file.prefix";
diff --git a/src/paimon/common/file_index/file_index_format.cpp
b/src/paimon/common/file_index/file_index_format.cpp
index 85500845..fab5c7a7 100644
--- a/src/paimon/common/file_index/file_index_format.cpp
+++ b/src/paimon/common/file_index/file_index_format.cpp
@@ -27,7 +27,9 @@
#include "arrow/type.h"
#include "fmt/format.h"
#include "paimon/common/file_index/empty/empty_file_index_reader.h"
+#include "paimon/common/io/data_output_stream.h"
#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/math.h"
#include "paimon/file_index/file_indexer.h"
#include "paimon/file_index/file_indexer_factory.h"
#include "paimon/io/byte_array_input_stream.h"
@@ -39,6 +41,128 @@ namespace paimon {
class InputStream;
class MemoryPool;
+class FileIndexFormatWriterImpl : public FileIndexFormat::Writer {
+ public:
+ explicit FileIndexFormatWriterImpl(const std::shared_ptr<OutputStream>&
output_stream)
+ : output_stream_(output_stream) {
+ assert(output_stream_);
+ }
+
+ Status WriteColumnIndexes(const FileIndexFormat::ColumnIndexes& indexes)
override {
+ if (written_) {
+ return Status::Invalid("File index column indexes have already
been written");
+ }
+
+ PAIMON_RETURN_NOT_OK(WriteHead(indexes));
+ // Write body.
+ DataOutputStream data_output(output_stream_);
+ for (const auto& [column_name, column_indexes] : indexes) {
+ for (const auto& [index_type, bytes] : column_indexes) {
+ if (bytes) {
+ PAIMON_RETURN_NOT_OK(data_output.WriteBytes(bytes));
+ }
+ }
+ }
+ written_ = true;
+ return Status::OK();
+ }
+
+ Status Close() override {
+ if (closed_) {
+ return Status::OK();
+ }
+ closed_ = true;
+ PAIMON_RETURN_NOT_OK(output_stream_->Flush());
+ return output_stream_->Close();
+ }
+
+ private:
+ static constexpr int32_t kRedundantLength = 0;
+
+ static Result<int32_t> CalculateHeadLength(const
FileIndexFormat::ColumnIndexes& indexes) {
+ // magic(8), version(4), header length(4), and column count(4).
+ int64_t head_length = 8 + 4 + 4 + 4;
+ int64_t body_length = 0;
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(indexes.size(), "file index column
count"));
+ for (const auto& [column_name, column_indexes] : indexes) {
+
PAIMON_RETURN_NOT_OK(ValidateValueInRange<uint16_t>(column_name.size(),
+ "file index
column name length"));
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(column_indexes.size(), "column
index count"));
+ // column name(2 + N) + index count(4)
+ head_length += 2 + static_cast<int64_t>(column_name.size()) + 4;
+ for (const auto& [index_type, bytes] : column_indexes) {
+
PAIMON_RETURN_NOT_OK(ValidateValueInRange<uint16_t>(index_type.size(),
+ "file
index type name length"));
+ // index type(2 + N) + body offset(4) + body length(4)
+ head_length += 2 + static_cast<int64_t>(index_type.size()) + 4
+ 4;
+ if (bytes) {
+ PAIMON_RETURN_NOT_OK(AddChecked(bytes->size(), "index
body", &body_length));
+ }
+ }
+ }
+
+ head_length += 4; // The trailing redundant-length field(4).
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(head_length, "file index header
length"));
+ int64_t container_length = head_length + body_length;
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int32_t>(container_length,
"file index size"));
+ return static_cast<int32_t>(head_length);
+ }
+
+ Status WriteHead(const FileIndexFormat::ColumnIndexes& indexes) {
+ PAIMON_ASSIGN_OR_RAISE(int32_t head_length,
CalculateHeadLength(indexes));
+ DataOutputStream data_output(output_stream_);
+ // Write magic.
+
PAIMON_RETURN_NOT_OK(data_output.WriteValue<int64_t>(FileIndexFormat::MAGIC));
+ // Write version.
+
PAIMON_RETURN_NOT_OK(data_output.WriteValue<int32_t>(FileIndexFormat::V_1));
+ // Write head length.
+ PAIMON_RETURN_NOT_OK(data_output.WriteValue<int32_t>(head_length));
+ // Write column count.
+
PAIMON_RETURN_NOT_OK(data_output.WriteValue<int32_t>(static_cast<int32_t>(indexes.size())));
+
+ int64_t body_offset = head_length;
+ for (const auto& [column_name, column_indexes] : indexes) {
+ // Write column name.
+ PAIMON_RETURN_NOT_OK(data_output.WriteString(column_name));
+ // Write index count for the column.
+ PAIMON_RETURN_NOT_OK(
+
data_output.WriteValue<int32_t>(static_cast<int32_t>(column_indexes.size())));
+ for (const auto& [index_type, bytes] : column_indexes) {
+ // Write index type.
+ PAIMON_RETURN_NOT_OK(data_output.WriteString(index_type));
+ // Write body offset and length.
+ if (bytes) {
+ PAIMON_RETURN_NOT_OK(
+
data_output.WriteValue<int32_t>(static_cast<int32_t>(body_offset)));
+ PAIMON_RETURN_NOT_OK(
+
data_output.WriteValue<int32_t>(static_cast<int32_t>(bytes->size())));
+ body_offset += static_cast<int64_t>(bytes->size());
+ } else {
+ PAIMON_RETURN_NOT_OK(
+
data_output.WriteValue<int32_t>(FileIndexFormat::EMPTY_INDEX_FLAG));
+ PAIMON_RETURN_NOT_OK(data_output.WriteValue<int32_t>(0));
+ }
+ }
+ }
+ // Write redundant length for future format extensions.
+ return data_output.WriteValue<int32_t>(kRedundantLength);
+ }
+
+ template <typename T>
+ static Status AddChecked(T value, const char* name, int64_t* total) {
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int32_t>(value, name));
+ *total += static_cast<int64_t>(value);
+ return ValidateValueInRange<int32_t>(*total, name);
+ }
+
+ std::shared_ptr<OutputStream> output_stream_;
+ bool written_ = false;
+ bool closed_ = false;
+};
+
class FileIndexFormatReaderImpl : public FileIndexFormat::Reader {
public:
using HeaderType =
@@ -153,4 +277,15 @@ Result<std::unique_ptr<FileIndexFormat::Reader>>
FileIndexFormat::CreateReader(
const std::shared_ptr<InputStream>& input_stream, const
std::shared_ptr<MemoryPool>& pool) {
return FileIndexFormatReaderImpl::Create(input_stream, pool);
}
+
+Result<std::unique_ptr<FileIndexFormat::Writer>> FileIndexFormat::CreateWriter(
+ const std::shared_ptr<OutputStream>& output_stream, const
std::shared_ptr<MemoryPool>& pool) {
+ if (!output_stream) {
+ return Status::Invalid("File index output stream cannot be null");
+ }
+ if (!pool) {
+ return Status::Invalid("File index memory pool cannot be null");
+ }
+ return std::make_unique<FileIndexFormatWriterImpl>(output_stream);
+}
} // namespace paimon
diff --git a/src/paimon/common/file_index/file_index_format_test.cpp
b/src/paimon/common/file_index/file_index_format_test.cpp
index 7851d57e..40989f7e 100644
--- a/src/paimon/common/file_index/file_index_format_test.cpp
+++ b/src/paimon/common/file_index/file_index_format_test.cpp
@@ -24,17 +24,20 @@
#include "paimon/common/file_index/bloomfilter/bloom_filter_file_index.h"
#include "paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.h"
#include "paimon/common/file_index/empty/empty_file_index_reader.h"
+#include "paimon/common/io/byte_array_output_stream.h"
#include "paimon/data/timestamp.h"
#include "paimon/defs.h"
#include "paimon/file_index/file_index_result.h"
#include "paimon/fs/local/local_file_system.h"
#include "paimon/io/byte_array_input_stream.h"
+#include "paimon/memory/bytes.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/predicate/literal.h"
#include "paimon/status.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
+
class FileIndexFormatTest : public ::testing::Test {
public:
void SetUp() override {
@@ -55,14 +58,25 @@ class FileIndexFormatTest : public ::testing::Test {
std::shared_ptr<MemoryPool> pool_;
};
-TEST_F(FileIndexFormatTest, TestCreateEmptyFileIndexReader) {
+TEST_F(FileIndexFormatTest, TestWriteAndReadEmptyIndexGoldenBytes) {
+ // the expected bytes are generated from Java Paimon
+ std::vector<char> expected = {0, 5, 78, 78, -48, 26, 53, -82, 0, 0,
0, 1, 0, 0, 0, 47,
+ 0, 0, 0, 1, 0, 2, 99, 49, 0, 0,
0, 1, 0, 5, 101, 109,
+ 112, 116, 121, -1, -1, -1, -1, 0, 0, 0,
0, 0, 0, 0, 0};
+ FileIndexFormat::ColumnIndexes indexes;
+ indexes["c1"]["empty"] = nullptr;
+ auto segment_output = std::make_unique<MemorySegmentOutputStream>(
+ MemorySegmentOutputStream::DEFAULT_SEGMENT_SIZE, pool_);
+ auto output =
std::make_shared<ByteArrayOutputStream>(std::move(segment_output));
+
+ ASSERT_OK_AND_ASSIGN(auto writer, FileIndexFormat::CreateWriter(output,
pool_));
+ ASSERT_OK(writer->WriteColumnIndexes(indexes));
+ ASSERT_OK(writer->Close());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> actual,
output->Finish(pool_.get()));
+
+ ASSERT_EQ(expected, std::vector<char>(actual->data(), actual->data() +
actual->size()));
auto schema = arrow::schema({arrow::field("c1", arrow::utf8())});
- std::vector<char> index_file_bytes = {0, 5, 78, 78, -48, 26, 53, -82,
0, 0, 0, 1,
- 0, 0, 0, 47, 0, 0, 0, 1,
0, 2, 99, 49,
- 0, 0, 0, 1, 0, 5, 101, 109,
112, 116, 121, -1,
- -1, -1, -1, 0, 0, 0, 0, 0,
0, 0, 0};
- auto input_stream =
- std::make_shared<ByteArrayInputStream>(index_file_bytes.data(),
index_file_bytes.size());
+ auto input_stream = std::make_shared<ByteArrayInputStream>(actual->data(),
actual->size());
ASSERT_OK_AND_ASSIGN(auto reader,
FileIndexFormat::CreateReader(input_stream, pool_));
ASSERT_OK_AND_ASSIGN(auto index_file_readers,
reader->ReadColumnIndex("c1",
CreateArrowSchema(schema).get()));
diff --git a/src/paimon/common/io/byte_array_output_stream.cpp
b/src/paimon/common/io/byte_array_output_stream.cpp
new file mode 100644
index 00000000..bc6d1f1a
--- /dev/null
+++ b/src/paimon/common/io/byte_array_output_stream.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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/common/io/byte_array_output_stream.h"
+
+#include <algorithm>
+#include <cassert>
+#include <limits>
+#include <utility>
+#include <vector>
+
+#include "paimon/common/memory/memory_segment_utils.h"
+#include "paimon/common/utils/math.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+
+namespace paimon {
+
+ByteArrayOutputStream::ByteArrayOutputStream(std::unique_ptr<MemorySegmentOutputStream>&&
output)
+ : output_(std::move(output)) {
+ assert(output_);
+}
+
+Result<int64_t> ByteArrayOutputStream::Write(const char* buffer, int64_t size)
{
+ if (closed_) {
+ return Status::Invalid("Byte array output stream is closed");
+ }
+ PAIMON_RETURN_NOT_OK(ValidateValueNonNegative(size, "write length"));
+ if (buffer == nullptr && size > 0) {
+ return Status::Invalid("Write buffer must not be null when size is
positive");
+ }
+ int64_t remaining = size;
+ while (remaining > 0) {
+ uint32_t to_write = static_cast<uint32_t>(std::min<int64_t>(
+ remaining,
static_cast<int64_t>(std::numeric_limits<uint32_t>::max())));
+ output_->Write(buffer, to_write);
+ buffer += to_write;
+ remaining -= to_write;
+ }
+ return size;
+}
+
+Status ByteArrayOutputStream::Close() {
+ closed_ = true;
+ return Status::OK();
+}
+
+Result<std::shared_ptr<Bytes>> ByteArrayOutputStream::Finish(MemoryPool* pool)
{
+ assert(pool);
+ PAIMON_RETURN_NOT_OK(Close());
+ if (result_) {
+ return result_;
+ }
+ // TODO(jinli.zjw): Support int64_t lengths in
MemorySegmentUtils::CopyToBytes and remove this
+ // limit.
+ const int64_t size = output_->CurrentSize();
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int32_t>(size, "byte array
output stream size"));
+ const std::vector<MemorySegment>& segments = output_->Segments();
+ result_ = std::make_shared<Bytes>(static_cast<size_t>(size), pool);
+ MemorySegmentUtils::CopyToBytes(segments, /*offset=*/0, result_.get(),
+ /*bytes_offset=*/0,
static_cast<int32_t>(size));
+ return result_;
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/io/byte_array_output_stream.h
b/src/paimon/common/io/byte_array_output_stream.h
new file mode 100644
index 00000000..9b87ca42
--- /dev/null
+++ b/src/paimon/common/io/byte_array_output_stream.h
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "paimon/common/io/memory_segment_output_stream.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+class Bytes;
+class MemoryPool;
+
+/// An in-memory output stream backed by segments allocated from a Paimon
MemoryPool.
+class ByteArrayOutputStream : public OutputStream {
+ public:
+ /// Takes ownership of an initialized segmented output stream.
+ explicit
ByteArrayOutputStream(std::unique_ptr<MemorySegmentOutputStream>&& output);
+
+ ~ByteArrayOutputStream() override = default;
+
+ Result<int64_t> Write(const char* buffer, int64_t size) override;
+
+ Status Flush() override {
+ return Status::OK();
+ }
+
+ Result<int64_t> GetPos() const override {
+ return output_->CurrentSize();
+ }
+
+ Result<std::string> GetUri() const override {
+ return std::string();
+ }
+
+ Status Close() override;
+
+ /// Closes the stream and returns its contents as an exactly-sized
contiguous byte array.
+ /// @note The caller must keep `pool` alive until the returned bytes are
destroyed.
+ Result<std::shared_ptr<Bytes>> Finish(MemoryPool* pool);
+
+ private:
+ std::unique_ptr<MemorySegmentOutputStream> output_;
+ std::shared_ptr<Bytes> result_;
+ bool closed_ = false;
+};
+
+} // namespace paimon
diff --git a/src/paimon/common/io/byte_array_output_stream_test.cpp
b/src/paimon/common/io/byte_array_output_stream_test.cpp
new file mode 100644
index 00000000..bd185095
--- /dev/null
+++ b/src/paimon/common/io/byte_array_output_stream_test.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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/common/io/byte_array_output_stream.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(ByteArrayOutputStreamTest, TestWriteAndFinish) {
+ std::shared_ptr<MemoryPool> pool = GetMemoryPool();
+ auto output =
std::make_unique<MemorySegmentOutputStream>(/*segment_size=*/2, pool);
+ std::shared_ptr<ByteArrayOutputStream> stream =
+ std::make_shared<ByteArrayOutputStream>(std::move(output));
+ ASSERT_GT(pool->CurrentUsage(), 0);
+ ASSERT_OK_AND_ASSIGN(int64_t first_write, stream->Write("ab", 2));
+ ASSERT_EQ(2, first_write);
+ ASSERT_OK_AND_ASSIGN(int64_t second_write, stream->Write("cdef", 4));
+ ASSERT_EQ(4, second_write);
+ ASSERT_OK_AND_ASSIGN(int64_t position, stream->GetPos());
+ ASSERT_EQ(6, position);
+ ASSERT_EQ(pool->CurrentUsage(), pool->MaxMemoryUsage());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> result,
stream->Finish(pool.get()));
+ ASSERT_EQ("abcdef", std::string(result->data(), result->size()));
+ ASSERT_NOK_WITH_MSG(stream->Write("x", 1), "closed");
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> repeated,
stream->Finish(pool.get()));
+ ASSERT_EQ(result, repeated);
+ stream.reset();
+ ASSERT_EQ(6, pool->CurrentUsage());
+}
+
+TEST(ByteArrayOutputStreamTest, TestWriteValidation) {
+ std::shared_ptr<MemoryPool> pool = GetDefaultPool();
+ auto output =
std::make_unique<MemorySegmentOutputStream>(/*segment_size=*/8, pool);
+ std::shared_ptr<ByteArrayOutputStream> stream =
+ std::make_shared<ByteArrayOutputStream>(std::move(output));
+ ASSERT_NOK(stream->Write(nullptr, 1));
+ ASSERT_NOK(stream->Write("", -1));
+ ASSERT_OK_AND_ASSIGN(int64_t written, stream->Write(nullptr, 0));
+ ASSERT_EQ(0, written);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> result,
stream->Finish(pool.get()));
+ ASSERT_EQ(0, result->size());
+}
+
+TEST(ByteArrayOutputStreamTest, TestCallerKeepsMemoryPoolAlive) {
+ std::shared_ptr<MemoryPool> pool = GetMemoryPool();
+ auto output =
std::make_unique<MemorySegmentOutputStream>(/*segment_size=*/8, pool);
+ std::shared_ptr<ByteArrayOutputStream> stream =
+ std::make_shared<ByteArrayOutputStream>(std::move(output));
+ ASSERT_OK_AND_ASSIGN(int64_t written, stream->Write("data", 4));
+ ASSERT_EQ(4, written);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> result,
stream->Finish(pool.get()));
+
+ stream.reset();
+ ASSERT_GT(pool->CurrentUsage(), 0);
+ ASSERT_EQ("data", std::string(result->data(), result->size()));
+
+ result.reset();
+ ASSERT_EQ(0, pool->CurrentUsage());
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/common/io/data_input_output_stream_test.cpp
b/src/paimon/common/io/data_input_output_stream_test.cpp
index 4e606370..0a5dd574 100644
--- a/src/paimon/common/io/data_input_output_stream_test.cpp
+++ b/src/paimon/common/io/data_input_output_stream_test.cpp
@@ -79,12 +79,8 @@ class DataInputOutputStreamTest : public ::testing::Test,
(void)data_output_stream->WriteValue(static_cast<int64_t>(9223372036854775805));
// 8 bytes
(void)data_output_stream->WriteValue(true);
// 1 byte
std::string str1 = "This is a very very very long sentence.";
- if constexpr (std::is_same_v<T, MemorySegmentOutputStream>) {
- (void)data_output_stream->WriteString(str1); // 39 bytes + 2
bytes len
- } else {
- (void)data_output_stream->WriteString(str1); // 39 bytes + 2
bytes len
- }
- std::string str2 = "我是一个粉刷匠~"; // 24 bytes
+ (void)data_output_stream->WriteString(str1); // 39 bytes + 2 bytes len
+ std::string str2 = "我是一个粉刷匠~"; // 24 bytes
auto bytes = std::make_shared<Bytes>(str2, pool_.get());
(void)data_output_stream->WriteBytes(bytes);
}
diff --git a/src/paimon/common/io/memory_segment_output_stream.cpp
b/src/paimon/common/io/memory_segment_output_stream.cpp
index 5355f72b..2d0d274a 100644
--- a/src/paimon/common/io/memory_segment_output_stream.cpp
+++ b/src/paimon/common/io/memory_segment_output_stream.cpp
@@ -54,11 +54,7 @@ void MemorySegmentOutputStream::WriteString(const
std::string& str) {
}
void MemorySegmentOutputStream::Write(const char* data, uint32_t size) {
- auto bytes = std::make_shared<Bytes>(size, pool_.get());
- if (size != 0) {
- memcpy(bytes->data(), data, size);
- }
- auto segment = MemorySegment::Wrap(bytes);
+ MemorySegment segment = MemorySegment::WrapView(data, size);
Write(segment, 0, segment.Size());
}
diff --git a/src/paimon/common/io/memory_segment_output_stream_test.cpp
b/src/paimon/common/io/memory_segment_output_stream_test.cpp
index 61fbfe30..69c207ce 100644
--- a/src/paimon/common/io/memory_segment_output_stream_test.cpp
+++ b/src/paimon/common/io/memory_segment_output_stream_test.cpp
@@ -82,4 +82,16 @@ TEST_P(MemorySegmentOutputStreamTest, TestSimple) {
ASSERT_EQ(out.CurrentSize(), input_stream->GetPos().value());
}
+TEST(MemorySegmentOutputStreamTest,
TestRawWriteDoesNotAllocateTemporaryBuffer) {
+ std::shared_ptr<MemoryPool> pool = GetMemoryPool();
+ MemorySegmentOutputStream out(/*segment_size=*/8, pool);
+ uint64_t allocated_before_write = pool->CurrentUsage();
+
+ out.Write("abc", 3);
+
+ ASSERT_EQ(allocated_before_write, pool->CurrentUsage());
+ ASSERT_EQ(pool->CurrentUsage(), pool->MaxMemoryUsage());
+ ASSERT_EQ(3, out.CurrentSize());
+}
+
} // namespace paimon::test
diff --git a/src/paimon/core/append/append_only_writer_test.cpp
b/src/paimon/core/append/append_only_writer_test.cpp
index 8ef6e135..11401609 100644
--- a/src/paimon/core/append/append_only_writer_test.cpp
+++ b/src/paimon/core/append/append_only_writer_test.cpp
@@ -56,9 +56,11 @@
#include "paimon/core/stats/simple_stats.h"
#include "paimon/core/utils/commit_increment.h"
#include "paimon/defs.h"
+#include "paimon/file_index/file_index_format.h"
#include "paimon/format/file_format_factory.h"
#include "paimon/fs/file_system.h"
#include "paimon/fs/local/local_file_system.h"
+#include "paimon/io/byte_array_input_stream.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/record_batch.h"
#include "paimon/testing/utils/binary_row_generator.h"
@@ -404,6 +406,41 @@ TEST_F(AppendOnlyWriterTest, TestWriteAndPrepareCommit) {
ASSERT_OK(writer->Close());
}
+TEST_F(AppendOnlyWriterTest, TestWritePublishesEmbeddedBitmapIndex) {
+ CoreOptions options = CreateOptions(
+ {{"file-index.bitmap.columns", "f0"},
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"}});
+ auto schema =
+ arrow::schema({arrow::field("f0", arrow::int32()), arrow::field("f1",
arrow::int32())});
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = CreatePathFactory(dir->Str(), "mock_format", options);
+ ASSERT_OK_AND_ASSIGN(
+ auto writer, CreateAppendOnlyWriter(
+ options, /*schema_id=*/0, schema,
/*write_cols=*/std::nullopt,
+ /*max_sequence_number=*/-1, path_factory,
compact_manager_, memory_pool_));
+
+ ASSERT_OK(writer->Write(CreateBatch(schema, R"([{"f0": 1, "f1": 10},
+ {"f0": 2, "f1": 20},
+ {"f0": 1, "f1": 30}])")));
+ ASSERT_OK_AND_ASSIGN(CommitIncrement increment,
+ writer->PrepareCommit(/*wait_compaction=*/true));
+ const auto& files = increment.GetNewFilesIncrement().NewFiles();
+ ASSERT_EQ(1, files.size());
+ ASSERT_TRUE(files[0]->embedded_index);
+ ASSERT_TRUE(files[0]->extra_files.empty());
+
+ auto input =
std::make_shared<ByteArrayInputStream>(files[0]->embedded_index->data(),
+
files[0]->embedded_index->size());
+ ASSERT_OK_AND_ASSIGN(auto index_reader,
FileIndexFormat::CreateReader(input, memory_pool_));
+ ::ArrowSchema c_schema;
+ ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
+ ASSERT_OK_AND_ASSIGN(auto column_readers,
index_reader->ReadColumnIndex("f0", &c_schema));
+ ASSERT_EQ(1, column_readers.size());
+ ASSERT_OK_AND_ASSIGN(auto result,
column_readers[0]->VisitEqual(Literal(1)));
+ ASSERT_EQ("{0,2}", result->ToString());
+ ASSERT_OK(writer->Close());
+}
+
TEST_F(AppendOnlyWriterTest, TestWriteAndClose) {
std::map<std::string, std::string> raw_options;
raw_options[Options::FILE_FORMAT] = "orc";
diff --git a/src/paimon/core/core_options.cpp b/src/paimon/core/core_options.cpp
index 578950fb..1c2e164b 100644
--- a/src/paimon/core/core_options.cpp
+++ b/src/paimon/core/core_options.cpp
@@ -386,6 +386,7 @@ struct CoreOptions::Impl {
int64_t manifest_target_file_size = 8 * 1024 * 1024;
int64_t deletion_vector_target_file_size = 2 * 1024 * 1024;
int64_t manifest_full_compaction_file_size = 16 * 1024 * 1024;
+ int64_t file_index_in_manifest_threshold = 500;
int64_t write_buffer_size = 256 * 1024 * 1024;
int64_t commit_timeout = std::numeric_limits<int64_t>::max();
int64_t commit_min_retry_wait = 10;
@@ -838,6 +839,9 @@ struct CoreOptions::Impl {
// Parse index-related configurations: file index, global index.
Status ParseIndexOptions(const ConfigParser& parser) {
+ // Parse file-index.in-manifest-threshold - max inline file index
size, default 500B
+
PAIMON_RETURN_NOT_OK(parser.ParseMemorySize(Options::FILE_INDEX_IN_MANIFEST_THRESHOLD,
+
&file_index_in_manifest_threshold));
// Parse file-index.read.enabled - whether to enable reading file
index, default true
PAIMON_RETURN_NOT_OK(
parser.Parse<bool>(Options::FILE_INDEX_READ_ENABLED,
&file_index_read_enabled));
@@ -1654,6 +1658,10 @@ bool CoreOptions::FileIndexReadEnabled() const {
return impl_->file_index_read_enabled;
}
+int64_t CoreOptions::FileIndexInManifestThreshold() const {
+ return impl_->file_index_in_manifest_threshold;
+}
+
std::optional<std::string> CoreOptions::GetDataFileExternalPaths() const {
return impl_->data_file_external_paths;
}
diff --git a/src/paimon/core/core_options.h b/src/paimon/core/core_options.h
index 9eb28988..53ef4ad0 100644
--- a/src/paimon/core/core_options.h
+++ b/src/paimon/core/core_options.h
@@ -207,6 +207,7 @@ class PAIMON_EXPORT CoreOptions {
bool NeedLookup() const;
bool PrepareCommitWaitCompaction() const;
bool FileIndexReadEnabled() const;
+ int64_t FileIndexInManifestThreshold() const;
std::map<std::string, std::string> GetFieldsSequenceGroups() const;
bool PartialUpdateRemoveRecordOnDelete() const;
diff --git a/src/paimon/core/core_options_test.cpp
b/src/paimon/core/core_options_test.cpp
index ebc127ed..0054a5b5 100644
--- a/src/paimon/core/core_options_test.cpp
+++ b/src/paimon/core/core_options_test.cpp
@@ -134,6 +134,7 @@ TEST(CoreOptionsTest, TestDefaultValue) {
ASSERT_EQ(std::nullopt, core_options.GetScanFallbackBranch());
ASSERT_EQ("main", core_options.GetBranch());
ASSERT_TRUE(core_options.FileIndexReadEnabled());
+ ASSERT_EQ(500, core_options.FileIndexInManifestThreshold());
ASSERT_EQ(std::nullopt, core_options.GetDataFileExternalPaths());
ASSERT_EQ(ExternalPathStrategy::NONE,
core_options.GetExternalPathStrategy());
ASSERT_TRUE(core_options.EnableAdaptivePrefetchStrategy());
@@ -248,6 +249,7 @@ TEST(CoreOptionsTest, TestFromMap) {
{Options::SCAN_FALLBACK_BRANCH, "fallback"},
{Options::BRANCH, "rt"},
{Options::FILE_INDEX_READ_ENABLED, "false"},
+ {Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "2KB"},
{Options::DATA_FILE_EXTERNAL_PATHS, "FILE:///tmp/index"},
{Options::DATA_FILE_EXTERNAL_PATHS_STRATEGY, "round-robin"},
{Options::FILE_COMPRESSION, "snappy"},
@@ -398,6 +400,7 @@ TEST(CoreOptionsTest, TestFromMap) {
ASSERT_EQ(core_options.GetScanFallbackBranch(),
std::optional<std::string>("fallback"));
ASSERT_EQ(core_options.GetBranch(), "rt");
ASSERT_FALSE(core_options.FileIndexReadEnabled());
+ ASSERT_EQ(2 * 1024, core_options.FileIndexInManifestThreshold());
ASSERT_EQ(core_options.GetDataFileExternalPaths(),
std::optional<std::string>("FILE:///tmp/index"));
ASSERT_EQ(core_options.GetExternalPathStrategy(),
ExternalPathStrategy::ROUND_ROBIN);
diff --git a/src/paimon/core/io/append_data_file_writer_factory.cpp
b/src/paimon/core/io/append_data_file_writer_factory.cpp
index e12374dd..d0b677a7 100644
--- a/src/paimon/core/io/append_data_file_writer_factory.cpp
+++ b/src/paimon/core/io/append_data_file_writer_factory.cpp
@@ -24,6 +24,7 @@
#include "arrow/c/abi.h"
#include "arrow/c/helpers.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/fs/file_system.h"
@@ -57,6 +58,11 @@ AppendDataFileWriterFactory::CreateWriter() const {
options_.GetFileCompression(), std::function<Status(::ArrowArray*,
::ArrowArray*)>(),
schema_id_, seq_num_counter, file_source_, resources.stats_extractor,
path_factory_->IsExternalPath(), write_cols_, pool_);
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter>
file_index_writer,
+ CreateFileIndexWriter(write_schema_,
path_factory_));
+ if (file_index_writer) {
+ writer->SetFileIndexWriter(std::move(file_index_writer),
write_schema_);
+ }
PAIMON_RETURN_NOT_OK(
writer->Init(options_.GetFileSystem(), path_factory_->NewPath(),
resources.writer_builder));
return std::unique_ptr<SingleFileWriter<::ArrowArray*,
std::shared_ptr<DataFileMeta>>>(
diff --git a/src/paimon/core/io/data_file_index_writer.cpp
b/src/paimon/core/io/data_file_index_writer.cpp
new file mode 100644
index 00000000..5c97bb3d
--- /dev/null
+++ b/src/paimon/core/io/data_file_index_writer.cpp
@@ -0,0 +1,179 @@
+/*
+ * 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_index_writer.h"
+
+#include <cassert>
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/c/bridge.h"
+#include "arrow/c/helpers.h"
+#include "fmt/format.h"
+#include "paimon/common/io/byte_array_output_stream.h"
+#include "paimon/common/io/memory_segment_output_stream.h"
+#include "paimon/common/table/special_fields.h"
+#include "paimon/common/utils/arrow/status_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/file_index/file_index_format.h"
+#include "paimon/file_index/file_index_writer.h"
+#include "paimon/file_index/file_indexer.h"
+#include "paimon/file_index/file_indexer_factory.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/status.h"
+
+namespace paimon {
+Result<std::unique_ptr<DataFileIndexWriter>> DataFileIndexWriter::Create(
+ const std::shared_ptr<arrow::Schema>& logical_schema, const
FileIndexOptions& options,
+ const std::shared_ptr<FileSystem>& file_system,
+ const std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<MemoryPool>& pool) {
+ assert(logical_schema);
+ assert(file_system);
+ assert(path_factory);
+ assert(pool);
+ std::vector<IndexWriterEntry> writers;
+ writers.reserve(options.Definitions().size());
+ for (const FileIndexDefinition& definition : options.Definitions()) {
+ if (SpecialFields::IsSystemField(definition.column_name)) {
+ return Status::Invalid(
+ fmt::format("File index column '{}' is a system field",
definition.column_name));
+ }
+ int32_t field_index =
logical_schema->GetFieldIndex(definition.column_name);
+ if (field_index < 0) {
+ return Status::Invalid(
+ fmt::format("File index column '{}' does not exist in the
write schema",
+ definition.column_name));
+ }
+ std::shared_ptr<arrow::Field> field =
logical_schema->field(field_index);
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileIndexer> indexer,
+ FileIndexerFactory::Get(definition.index_type,
definition.options));
+ if (!indexer) {
+ return Status::Invalid(
+ fmt::format("File index type '{}' is not registered",
definition.index_type));
+ }
+ ::ArrowSchema c_schema;
+ ArrowSchemaMarkReleased(&c_schema);
+ ScopeGuard schema_guard([&c_schema]() { ArrowSchemaRelease(&c_schema);
});
+
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*arrow::schema({field}),
&c_schema));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileIndexWriter> writer,
+ indexer->CreateWriter(&c_schema, pool));
+ writers.push_back(
+ {definition.column_name, definition.index_type, field_index,
field, std::move(writer)});
+ }
+ return std::unique_ptr<DataFileIndexWriter>(new DataFileIndexWriter(
+ std::move(writers), options.InManifestThreshold(), file_system,
path_factory, pool));
+}
+
+DataFileIndexWriter::DataFileIndexWriter(std::vector<IndexWriterEntry>&&
writers,
+ int64_t in_manifest_threshold,
+ const std::shared_ptr<FileSystem>&
file_system,
+ const
std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<MemoryPool>&
pool)
+ : writers_(std::move(writers)),
+ in_manifest_threshold_(in_manifest_threshold),
+ file_system_(file_system),
+ path_factory_(path_factory),
+ pool_(pool) {}
+
+Status DataFileIndexWriter::AddBatch(const
std::shared_ptr<arrow::StructArray>& logical_batch) {
+ if (finished_) {
+ return Status::Invalid("Data file index writer has already finished");
+ }
+ for (const IndexWriterEntry& entry : writers_) {
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::StructArray> projected,
+
arrow::StructArray::Make({logical_batch->field(entry.field_index)},
{entry.field}));
+ ::ArrowArray c_array;
+ ArrowArrayMarkReleased(&c_array);
+ ScopeGuard array_guard([&c_array]() { ArrowArrayRelease(&c_array); });
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*projected,
&c_array));
+ PAIMON_RETURN_NOT_OK(entry.writer->AddBatch(&c_array));
+ }
+ return Status::OK();
+}
+
+Result<std::shared_ptr<Bytes>> DataFileIndexWriter::SerializeContainer() {
+ FileIndexFormat::ColumnIndexes column_indexes;
+ for (const IndexWriterEntry& entry : writers_) {
+
PAIMON_ASSIGN_OR_RAISE(column_indexes[entry.column_name][entry.index_type],
+ entry.writer->SerializedBytes());
+ }
+
+ auto segment_output = std::make_unique<MemorySegmentOutputStream>(
+ MemorySegmentOutputStream::DEFAULT_SEGMENT_SIZE, pool_);
+ std::shared_ptr<ByteArrayOutputStream> output =
+ std::make_shared<ByteArrayOutputStream>(std::move(segment_output));
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileIndexFormat::Writer>
format_writer,
+ FileIndexFormat::CreateWriter(output, pool_));
+ PAIMON_RETURN_NOT_OK(format_writer->WriteColumnIndexes(column_indexes));
+ PAIMON_RETURN_NOT_OK(format_writer->Close());
+ return output->Finish(pool_.get());
+}
+
+Result<FileIndexWriteResult> DataFileIndexWriter::Finish(const std::string&
data_file_path) {
+ if (finished_) {
+ return Status::Invalid("Data file index writer has already finished");
+ }
+ finished_ = true;
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> bytes, SerializeContainer());
+ if (static_cast<int64_t>(bytes->size()) <= in_manifest_threshold_) {
+ return FileIndexWriteResult{bytes, {}};
+ }
+
+ external_index_path_ = path_factory_->ToFileIndexPath(data_file_path);
+ PAIMON_RETURN_NOT_OK(WriteExternal(external_index_path_.value(), bytes));
+ return FileIndexWriteResult{nullptr,
{PathUtil::GetName(external_index_path_.value())}};
+}
+
+Status DataFileIndexWriter::WriteExternal(const std::string& path,
+ const std::shared_ptr<Bytes>& bytes)
{
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<OutputStream> output,
+ file_system_->Create(path, /*overwrite=*/false));
+ ScopeGuard guard([this, &output]() {
+ if (output) {
+ [[maybe_unused]] Status _ = output->Close();
+ }
+ Abort();
+ });
+ PAIMON_ASSIGN_OR_RAISE(int64_t written,
+ output->Write(bytes->data(),
static_cast<int64_t>(bytes->size())));
+ if (written != static_cast<int64_t>(bytes->size())) {
+ return Status::IOError(fmt::format("Short write for file index {}:
expected {}, wrote {}",
+ path, bytes->size(), written));
+ }
+ PAIMON_RETURN_NOT_OK(output->Flush());
+ Status close_status = output->Close();
+ output.reset();
+ PAIMON_RETURN_NOT_OK(close_status);
+ guard.Release();
+ return Status::OK();
+}
+
+void DataFileIndexWriter::Abort() {
+ if (external_index_path_) {
+ [[maybe_unused]] Status _ =
file_system_->Delete(external_index_path_.value());
+ }
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/io/data_file_index_writer.h
b/src/paimon/core/io/data_file_index_writer.h
new file mode 100644
index 00000000..883b719f
--- /dev/null
+++ b/src/paimon/core/io/data_file_index_writer.h
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "paimon/core/io/file_index_options.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class Field;
+class Schema;
+class StructArray;
+} // namespace arrow
+
+namespace paimon {
+
+class Bytes;
+class DataFilePathFactory;
+class FileIndexWriter;
+class FileSystem;
+class MemoryPool;
+
+struct FileIndexWriteResult {
+ std::shared_ptr<Bytes> embedded_index;
+ std::vector<std::optional<std::string>> extra_files;
+};
+
+/// Builds every configured column index for one data file.
+class DataFileIndexWriter {
+ public:
+ static Result<std::unique_ptr<DataFileIndexWriter>> Create(
+ const std::shared_ptr<arrow::Schema>& logical_schema, const
FileIndexOptions& options,
+ const std::shared_ptr<FileSystem>& file_system,
+ const std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<MemoryPool>& pool);
+
+ Status AddBatch(const std::shared_ptr<arrow::StructArray>& logical_batch);
+
+ /// Finalizes and publishes all configured indexes. This is a terminal,
one-shot operation.
+ ///
+ /// @param data_file_path Path of the data file associated with these
indexes.
+ /// @return Embedded index bytes or the external index file name.
+ Result<FileIndexWriteResult> Finish(const std::string& data_file_path);
+
+ void Abort();
+
+ const std::optional<std::string>& ExternalIndexPath() const {
+ return external_index_path_;
+ }
+
+ private:
+ struct IndexWriterEntry {
+ std::string column_name;
+ std::string index_type;
+ int32_t field_index;
+ std::shared_ptr<arrow::Field> field;
+ std::shared_ptr<FileIndexWriter> writer;
+ };
+
+ DataFileIndexWriter(std::vector<IndexWriterEntry>&& writers, int64_t
in_manifest_threshold,
+ const std::shared_ptr<FileSystem>& file_system,
+ const std::shared_ptr<DataFilePathFactory>&
path_factory,
+ const std::shared_ptr<MemoryPool>& pool);
+
+ Result<std::shared_ptr<Bytes>> SerializeContainer();
+ Status WriteExternal(const std::string& path, const
std::shared_ptr<Bytes>& bytes);
+
+ std::vector<IndexWriterEntry> writers_;
+ int64_t in_manifest_threshold_;
+ std::shared_ptr<FileSystem> file_system_;
+ std::shared_ptr<DataFilePathFactory> path_factory_;
+ std::shared_ptr<MemoryPool> pool_;
+ std::optional<std::string> external_index_path_;
+ bool finished_ = false;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/io/data_file_index_writer_test.cpp
b/src/paimon/core/io/data_file_index_writer_test.cpp
new file mode 100644
index 00000000..e9ab7940
--- /dev/null
+++ b/src/paimon/core/io/data_file_index_writer_test.cpp
@@ -0,0 +1,253 @@
+/*
+ * 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_index_writer.h"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/json_simple.h"
+#include "arrow/type.h"
+#include "gtest/gtest.h"
+#include "paimon/common/table/special_fields.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/checked_cast.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/file_index_options.h"
+#include "paimon/defs.h"
+#include "paimon/file_index/file_index_format.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/io/byte_array_input_stream.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/predicate/literal.h"
+#include "paimon/testing/mock/mock_file_system.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+namespace {
+
+struct CloseFailingState {
+ int32_t close_count = 0;
+ int32_t delete_count = 0;
+};
+
+class CloseFailingOutputStream : public MockOutputStream {
+ public:
+ explicit CloseFailingOutputStream(const
std::shared_ptr<CloseFailingState>& state)
+ : state_(state) {}
+
+ Result<int64_t> Write(const char*, int64_t size) override {
+ return size;
+ }
+
+ Status Close() override {
+ ++state_->close_count;
+ return Status::IOError("close failed");
+ }
+
+ private:
+ std::shared_ptr<CloseFailingState> state_;
+};
+
+class CloseFailingFileSystem : public MockFileSystem {
+ public:
+ explicit CloseFailingFileSystem(const std::shared_ptr<CloseFailingState>&
state)
+ : state_(state) {}
+
+ Result<std::unique_ptr<OutputStream>> Create(const std::string&, bool)
const override {
+ return std::unique_ptr<OutputStream>(new
CloseFailingOutputStream(state_));
+ }
+
+ Status Delete(const std::string&, bool = true) const override {
+ ++state_->delete_count;
+ return Status::OK();
+ }
+
+ private:
+ std::shared_ptr<CloseFailingState> state_;
+};
+
+} // namespace
+
+class DataFileIndexWriterTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ pool_ = GetDefaultPool();
+ file_system_ = std::make_shared<LocalFileSystem>();
+ directory_ = UniqueTestDirectory::Create();
+ ASSERT_TRUE(directory_);
+ path_factory_ = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory_->Init(directory_->Str(), "orc", "data-",
nullptr));
+ schema_ =
+ arrow::schema({arrow::field("f0", arrow::int32()),
arrow::field("f1", arrow::int32())});
+ }
+
+ Result<std::unique_ptr<DataFileIndexWriter>> CreateWriter(
+ const std::map<std::string, std::string>& index_options) const {
+ PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
+ CoreOptions::FromMap(index_options,
file_system_));
+ PAIMON_ASSIGN_OR_RAISE(FileIndexOptions parsed,
+
FileIndexOptions::FromCoreOptions(core_options));
+ return DataFileIndexWriter::Create(schema_, parsed, file_system_,
path_factory_, pool_);
+ }
+
+ std::shared_ptr<arrow::StructArray> CreateBatch(const std::string& json)
const {
+ std::shared_ptr<arrow::Array> array =
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(schema_->fields()),
json)
+ .ValueOrDie();
+ return checked_pointer_cast<arrow::StructArray>(array);
+ }
+
+ Result<std::unique_ptr<FileIndexFormat::Reader>> CreateReader(
+ const std::shared_ptr<Bytes>& bytes) const {
+ auto input = std::make_shared<ByteArrayInputStream>(bytes->data(),
bytes->size());
+ return FileIndexFormat::CreateReader(input, pool_);
+ }
+
+ Result<std::vector<std::shared_ptr<FileIndexReader>>> ReadColumn(
+ FileIndexFormat::Reader* reader, const std::string& column_name) const
{
+ ::ArrowSchema c_schema;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema_,
&c_schema));
+ return reader->ReadColumnIndex(column_name, &c_schema);
+ }
+
+ protected:
+ std::shared_ptr<MemoryPool> pool_;
+ std::shared_ptr<LocalFileSystem> file_system_;
+ std::unique_ptr<UniqueTestDirectory> directory_;
+ std::shared_ptr<DataFilePathFactory> path_factory_;
+ std::shared_ptr<arrow::Schema> schema_;
+};
+
+TEST_F(DataFileIndexWriterTest, TestBitmapAndRangeBitmapEmbeddedRoundTrip) {
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ CreateWriter({{"file-index.bitmap.columns", "f0"},
+ {"file-index.range-bitmap.columns",
"f1"},
+
{"file-index.range-bitmap.f1.chunk-size", "1KB"},
+
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"}}));
+ ASSERT_OK(writer->AddBatch(CreateBatch(R"([{"f0": 1, "f1": 10},
+ {"f0": 2, "f1": 20}])")));
+ ASSERT_OK(writer->AddBatch(CreateBatch(R"([{"f0": 1, "f1": 30},
+ {"f0": null, "f1": 40}])")));
+
+ ASSERT_OK_AND_ASSIGN(FileIndexWriteResult result,
writer->Finish("unused.orc"));
+ ASSERT_TRUE(result.embedded_index);
+ 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 range_readers, ReadColumn(reader.get(), "f1"));
+ ASSERT_EQ(1, range_readers.size());
+ ASSERT_OK_AND_ASSIGN(auto greater_result,
range_readers[0]->VisitGreaterThan(Literal(20)));
+ ASSERT_EQ("{2,3}", greater_result->ToString());
+}
+
+TEST_F(DataFileIndexWriterTest, TestExternalIndexAndAbortCleanup) {
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ CreateWriter({{"file-index.bitmap.columns", "f0"},
+
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1B"}}));
+ ASSERT_OK(writer->AddBatch(CreateBatch(R"([{"f0": 1, "f1": 10}])")));
+ std::string data_path = path_factory_->NewPath();
+
+ ASSERT_OK_AND_ASSIGN(FileIndexWriteResult result,
writer->Finish(data_path));
+ ASSERT_FALSE(result.embedded_index);
+ ASSERT_EQ(1, result.extra_files.size());
+ ASSERT_TRUE(result.extra_files[0]);
+ ASSERT_EQ(PathUtil::GetName(path_factory_->ToFileIndexPath(data_path)),
+ result.extra_files[0].value());
+ std::string index_path = path_factory_->ToFileIndexPath(data_path);
+ ASSERT_OK_AND_ASSIGN(bool exists, file_system_->Exists(index_path));
+ ASSERT_TRUE(exists);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input,
file_system_->Open(index_path));
+ ASSERT_OK_AND_ASSIGN(auto reader, FileIndexFormat::CreateReader(input,
pool_));
+ 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}", equal_result->ToString());
+
+ writer->Abort();
+ ASSERT_OK_AND_ASSIGN(exists, file_system_->Exists(index_path));
+ ASSERT_FALSE(exists);
+}
+
+TEST_F(DataFileIndexWriterTest, TestUnavailableWriterFailsCreation) {
+ ASSERT_NOK_WITH_MSG(CreateWriter({{"file-index.unknown.columns", "f0"}}),
+ "File index type 'unknown' is not registered");
+ ASSERT_NOK_WITH_MSG(CreateWriter({{"file-index.bloom-filter.columns",
"f0"}}),
+ "do not support index writer in bloom filter");
+}
+
+TEST_F(DataFileIndexWriterTest, TestRejectSystemFieldIndex) {
+ std::shared_ptr<arrow::Schema> key_value_schema =
+ SpecialFields::CompleteSequenceAndValueKindField(schema_);
+ for (const std::string& field_name :
+ {SpecialFields::SequenceNumber().Name(),
SpecialFields::ValueKind().Name()}) {
+ ASSERT_OK_AND_ASSIGN(
+ CoreOptions core_options,
+ CoreOptions::FromMap({{"file-index.bitmap.columns", field_name}},
file_system_));
+ ASSERT_OK_AND_ASSIGN(FileIndexOptions options,
+ FileIndexOptions::FromCoreOptions(core_options));
+ ASSERT_NOK_WITH_MSG(DataFileIndexWriter::Create(key_value_schema,
options, file_system_,
+ path_factory_, pool_),
+ "is a system field");
+ }
+}
+
+TEST_F(DataFileIndexWriterTest, TestFinishIsOneShot) {
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ CreateWriter({{"file-index.bitmap.columns", "f0"},
+
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"}}));
+ ASSERT_OK(writer->AddBatch(CreateBatch(R"([{"f0": 1, "f1": 10}])")));
+ ASSERT_OK(writer->Finish("unused.orc"));
+
+ ASSERT_NOK_WITH_MSG(writer->Finish("unused.orc"), "already finished");
+ ASSERT_NOK_WITH_MSG(writer->AddBatch(CreateBatch(R"([{"f0": 2, "f1":
20}])")),
+ "already finished");
+}
+
+TEST_F(DataFileIndexWriterTest, TestCloseFailureClosesExternalStreamOnce) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions core_options,
+ CoreOptions::FromMap({{"file-index.bitmap.columns",
"f0"},
+
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1B"}},
+ file_system_));
+ ASSERT_OK_AND_ASSIGN(FileIndexOptions options,
FileIndexOptions::FromCoreOptions(core_options));
+ auto state = std::make_shared<CloseFailingState>();
+ auto close_failing_file_system =
std::make_shared<CloseFailingFileSystem>(state);
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ DataFileIndexWriter::Create(schema_, options,
close_failing_file_system,
+ path_factory_, pool_));
+ ASSERT_OK(writer->AddBatch(CreateBatch(R"([{"f0": 1, "f1": 10}])")));
+
+ ASSERT_NOK_WITH_MSG(writer->Finish(path_factory_->NewPath()), "close
failed");
+ ASSERT_EQ(1, state->close_count);
+ ASSERT_EQ(1, state->delete_count);
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/core/io/data_file_writer.cpp
b/src/paimon/core/io/data_file_writer.cpp
index 4ed3e040..9275fcf2 100644
--- a/src/paimon/core/io/data_file_writer.cpp
+++ b/src/paimon/core/io/data_file_writer.cpp
@@ -19,6 +19,7 @@
#include "paimon/core/io/data_file_writer.h"
#include <cassert>
+#include <utility>
#include "arrow/c/abi.h"
#include "paimon/common/utils/long_counter.h"
@@ -36,7 +37,7 @@ DataFileWriter::DataFileWriter(
const std::shared_ptr<FormatStatsExtractor>& stats_extractor, bool
is_external_path,
const std::optional<std::vector<std::string>>& write_cols,
const std::shared_ptr<MemoryPool>& pool)
- : SingleFileWriter(compression, converter),
+ : DataFileWriterBase(compression, std::move(converter)),
pool_(pool),
schema_id_(schema_id),
is_external_path_(is_external_path),
@@ -45,28 +46,13 @@ DataFileWriter::DataFileWriter(
stats_extractor_(stats_extractor),
write_cols_(write_cols) {}
-void DataFileWriter::SetMetadataFinalizer(MetadataFinalizer finalizer) {
- metadata_finalizer_ = std::move(finalizer);
-}
-
Status DataFileWriter::Write(ArrowArray* batch) {
int64_t record_count = batch->length;
- PAIMON_RETURN_NOT_OK(SingleFileWriter::Write(batch));
+ PAIMON_RETURN_NOT_OK(WriteRecordWithFileIndex(batch));
seq_num_counter_->Add(record_count);
return Status::OK();
}
-Status DataFileWriter::BeforeFinish() {
- if (metadata_finalizer_) {
- PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> updated_schema,
- metadata_finalizer_());
- if (updated_schema) {
- PAIMON_RETURN_NOT_OK(UpdateSchema(updated_schema));
- }
- }
- return Status::OK();
-}
-
Result<std::shared_ptr<DataFileMeta>> DataFileWriter::GetResult() {
PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<ColumnStats>>
field_stats, GetFieldStats());
PAIMON_ASSIGN_OR_RAISE(SimpleStats stats,
@@ -77,11 +63,12 @@ Result<std::shared_ptr<DataFileMeta>>
DataFileWriter::GetResult() {
PAIMON_ASSIGN_OR_RAISE(Path external_path, PathUtil::ToPath(path_));
final_path = external_path.ToString();
}
+ const FileIndexWriteResult& file_index = GetFileIndexWriteResult();
return DataFileMeta::ForAppend(
PathUtil::GetName(path_), output_bytes_, RecordCount(), stats,
seq_num_counter_->GetValue() - RecordCount(),
seq_num_counter_->GetValue() - 1, schema_id_,
- {}, /*embedded_index=*/nullptr, file_source_,
/*value_stats_cols=*/std::nullopt, final_path,
- /*first_row_id=*/std::nullopt, write_cols_);
+ file_index.extra_files, file_index.embedded_index, file_source_,
+ /*value_stats_cols=*/std::nullopt, final_path,
/*first_row_id=*/std::nullopt, write_cols_);
}
Result<std::vector<std::shared_ptr<ColumnStats>>>
DataFileWriter::GetFieldStats() {
diff --git a/src/paimon/core/io/data_file_writer.h
b/src/paimon/core/io/data_file_writer.h
index 60cc808a..f56f3495 100644
--- a/src/paimon/core/io/data_file_writer.h
+++ b/src/paimon/core/io/data_file_writer.h
@@ -28,7 +28,7 @@
#include "arrow/c/abi.h"
#include "paimon/common/utils/long_counter.h"
#include "paimon/core/io/data_file_meta.h"
-#include "paimon/core/io/single_file_writer.h"
+#include "paimon/core/io/data_file_writer_base.h"
#include "paimon/core/manifest/file_source.h"
#include "paimon/result.h"
#include "paimon/status.h"
@@ -44,13 +44,8 @@ class FormatStatsExtractor;
class LongCounter;
class MemoryPool;
-class DataFileWriter : public SingleFileWriter<::ArrowArray*,
std::shared_ptr<DataFileMeta>> {
+class DataFileWriter : public DataFileWriterBase<::ArrowArray*> {
public:
- /// Callback invoked during BeforeFinish() to finalize file metadata.
- /// Produces an updated schema with per-field metadata (e.g. shredding
metadata)
- /// and may perform other finalization work (e.g. reporting stats to
cross-file context).
- using MetadataFinalizer =
std::function<Result<std::shared_ptr<arrow::Schema>>()>;
-
DataFileWriter(const std::string& compression,
std::function<Status(::ArrowArray*, ::ArrowArray*)>
converter, int64_t schema_id,
const std::shared_ptr<LongCounter>& seq_num_counter,
FileSource file_source,
@@ -58,17 +53,10 @@ class DataFileWriter : public
SingleFileWriter<::ArrowArray*, std::shared_ptr<Da
bool is_external_path, const
std::optional<std::vector<std::string>>& write_cols,
const std::shared_ptr<MemoryPool>& pool);
- /// Sets the metadata finalizer. Called during BeforeFinish() to produce
an updated
- /// schema and perform finalization callbacks. Must be set before Close().
- void SetMetadataFinalizer(MetadataFinalizer finalizer);
-
Status Write(::ArrowArray* batch) override;
Result<std::shared_ptr<DataFileMeta>> GetResult() override;
- protected:
- Status BeforeFinish() override;
-
private:
Result<std::vector<std::shared_ptr<ColumnStats>>> GetFieldStats();
@@ -80,7 +68,6 @@ class DataFileWriter : public SingleFileWriter<::ArrowArray*,
std::shared_ptr<Da
FileSource file_source_;
std::shared_ptr<FormatStatsExtractor> stats_extractor_;
std::optional<std::vector<std::string>> write_cols_;
- MetadataFinalizer metadata_finalizer_;
};
} // namespace paimon
diff --git a/src/paimon/core/io/data_file_writer_base.h
b/src/paimon/core/io/data_file_writer_base.h
new file mode 100644
index 00000000..ccea898a
--- /dev/null
+++ b/src/paimon/core/io/data_file_writer_base.h
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <type_traits>
+#include <utility>
+
+#include "arrow/c/bridge.h"
+#include "arrow/type.h"
+#include "paimon/common/utils/checked_cast.h"
+#include "paimon/core/io/data_file_index_writer.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/single_file_writer.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+struct KeyValueBatch;
+
+/// Common lifecycle for data file writers which may finalize schema metadata
and publish a
+/// file-level index. Concrete writers remain responsible for their
record-specific state and
+/// DataFileMeta construction.
+template <typename Record>
+class DataFileWriterBase : public SingleFileWriter<Record,
std::shared_ptr<DataFileMeta>> {
+ public:
+ using Base = SingleFileWriter<Record, std::shared_ptr<DataFileMeta>>;
+ using AbortExecutor = typename Base::AbortExecutor;
+ /// Callback invoked during BeforeFinish() to finalize file metadata.
+ /// Produces an updated schema with per-field metadata (e.g. shredding
metadata)
+ /// and may perform other finalization work (e.g. reporting stats to
cross-file context).
+ using MetadataFinalizer =
std::function<Result<std::shared_ptr<arrow::Schema>>()>;
+
+ /// Sets the metadata finalizer. Called during BeforeFinish() to produce
an updated
+ /// schema and perform finalization callbacks. Must be set before Close().
+ void SetMetadataFinalizer(MetadataFinalizer finalizer) {
+ metadata_finalizer_ = std::move(finalizer);
+ }
+
+ void SetFileIndexWriter(std::unique_ptr<DataFileIndexWriter>&&
file_index_writer,
+ const std::shared_ptr<arrow::Schema>&
logical_schema) {
+ file_index_writer_ = std::move(file_index_writer);
+ logical_type_ = arrow::struct_(logical_schema->fields());
+ }
+
+ void Abort() override {
+ if (file_index_writer_) {
+ // The external index uses a path different from the data file
path deleted by Base.
+ file_index_writer_->Abort();
+ }
+ Base::Abort();
+ }
+
+ Result<AbortExecutor> GetAbortExecutor() const override {
+ PAIMON_ASSIGN_OR_RAISE(AbortExecutor executor,
Base::GetAbortExecutor());
+ if (file_index_writer_ && file_index_writer_->ExternalIndexPath()) {
+ executor.Add(this->fs_,
file_index_writer_->ExternalIndexPath().value());
+ }
+ return executor;
+ }
+
+ protected:
+ DataFileWriterBase(const std::string& compression,
+ std::function<Status(Record, ::ArrowArray*)> converter)
+ : Base(compression, std::move(converter)) {}
+
+ /// 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(AddFileIndexBatch(GetFileIndexBatch(record)));
+ return Base::Write(std::move(record));
+ }
+
+ const FileIndexWriteResult& GetFileIndexWriteResult() const {
+ return file_index_result_;
+ }
+
+ Status BeforeFinish() override {
+ if (metadata_finalizer_) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema>
updated_schema,
+ metadata_finalizer_());
+ if (updated_schema) {
+ PAIMON_RETURN_NOT_OK(this->UpdateSchema(updated_schema));
+ }
+ }
+ return Status::OK();
+ }
+
+ Status BeforeCompletion() override {
+ if (file_index_writer_) {
+ PAIMON_ASSIGN_OR_RAISE(file_index_result_,
file_index_writer_->Finish(this->path_));
+ }
+ return Status::OK();
+ }
+
+ private:
+ static ::ArrowArray* GetFileIndexBatch(Record& record) {
+ if constexpr (std::is_same_v<Record, ::ArrowArray*>) {
+ return record;
+ } else {
+ static_assert(std::is_same_v<Record, KeyValueBatch>,
+ "Unsupported data file record type");
+ return record.batch.get();
+ }
+ }
+
+ Status AddFileIndexBatch(::ArrowArray* batch) {
+ if (!file_index_writer_) {
+ return Status::OK();
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array>
logical_array,
+ arrow::ImportArray(batch,
logical_type_));
+ std::shared_ptr<arrow::StructArray> logical_batch =
+ checked_pointer_cast<arrow::StructArray>(logical_array);
+ PAIMON_RETURN_NOT_OK(file_index_writer_->AddBatch(logical_batch));
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*logical_batch,
batch));
+ return Status::OK();
+ }
+
+ MetadataFinalizer metadata_finalizer_;
+ std::unique_ptr<DataFileIndexWriter> file_index_writer_;
+ std::shared_ptr<arrow::DataType> logical_type_;
+ FileIndexWriteResult file_index_result_;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/io/data_file_writer_factory.cpp
b/src/paimon/core/io/data_file_writer_factory.cpp
index b929dde8..07195ab7 100644
--- a/src/paimon/core/io/data_file_writer_factory.cpp
+++ b/src/paimon/core/io/data_file_writer_factory.cpp
@@ -24,6 +24,9 @@
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/scope_guard.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/format/file_format.h"
#include "paimon/format/writer_builder.h"
@@ -58,4 +61,16 @@ Result<DataFileWriterFactory::WriterResources>
DataFileWriterFactory::CreateWrit
return resources;
}
+Result<std::unique_ptr<DataFileIndexWriter>>
DataFileWriterFactory::CreateFileIndexWriter(
+ const std::shared_ptr<arrow::Schema>& logical_schema,
+ const std::shared_ptr<DataFilePathFactory>& path_factory) const {
+ PAIMON_ASSIGN_OR_RAISE(FileIndexOptions file_index_options,
+ FileIndexOptions::FromCoreOptions(options_));
+ if (file_index_options.Empty()) {
+ return std::unique_ptr<DataFileIndexWriter>();
+ }
+ return DataFileIndexWriter::Create(logical_schema, file_index_options,
options_.GetFileSystem(),
+ path_factory, pool_);
+}
+
} // namespace paimon
diff --git a/src/paimon/core/io/data_file_writer_factory.h
b/src/paimon/core/io/data_file_writer_factory.h
index c727b47d..cab942f0 100644
--- a/src/paimon/core/io/data_file_writer_factory.h
+++ b/src/paimon/core/io/data_file_writer_factory.h
@@ -32,6 +32,8 @@ class Schema;
namespace paimon {
class FileFormat;
+class DataFileIndexWriter;
+class DataFilePathFactory;
class FormatStatsExtractor;
class MemoryPool;
class WriterBuilder;
@@ -52,6 +54,10 @@ class DataFileWriterFactory {
const
std::shared_ptr<arrow::Schema>& file_schema,
bool create_stats_extractor)
const;
+ Result<std::unique_ptr<DataFileIndexWriter>> CreateFileIndexWriter(
+ const std::shared_ptr<arrow::Schema>& logical_schema,
+ const std::shared_ptr<DataFilePathFactory>& path_factory) const;
+
CoreOptions options_;
int64_t schema_id_;
std::shared_ptr<MemoryPool> pool_;
diff --git a/src/paimon/core/io/file_index_options.cpp
b/src/paimon/core/io/file_index_options.cpp
new file mode 100644
index 00000000..a9587363
--- /dev/null
+++ b/src/paimon/core/io/file_index_options.cpp
@@ -0,0 +1,109 @@
+/*
+ * 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/file_index_options.h"
+
+#include <cstddef>
+#include <set>
+#include <utility>
+
+#include "fmt/format.h"
+#include "paimon/common/utils/string_utils.h"
+#include "paimon/core/core_options.h"
+#include "paimon/defs.h"
+#include "paimon/status.h"
+
+namespace paimon {
+namespace {
+
+constexpr char kFileIndexPrefix[] = "file-index.";
+constexpr char kColumnsSuffix[] = ".columns";
+constexpr size_t kFileIndexPrefixLength = sizeof(kFileIndexPrefix) - 1;
+constexpr size_t kColumnsSuffixLength = sizeof(kColumnsSuffix) - 1;
+
+} // namespace
+
+Result<FileIndexOptions> FileIndexOptions::FromCoreOptions(const CoreOptions&
options) {
+ FileIndexOptions result;
+ const std::map<std::string, std::string>& raw_options = options.ToMap();
+ result.in_manifest_threshold_ = options.FileIndexInManifestThreshold();
+
+ std::set<std::pair<std::string, std::string>> declared;
+ for (const auto& [key, value] : raw_options) {
+ if (!StringUtils::StartsWith(key, kFileIndexPrefix) ||
+ !StringUtils::EndsWith(key, kColumnsSuffix)) {
+ continue;
+ }
+ if (key.size() < kFileIndexPrefixLength + kColumnsSuffixLength) {
+ return Status::Invalid(fmt::format("Invalid file index option {}",
key));
+ }
+ const size_t index_type_length = key.size() - kFileIndexPrefixLength -
kColumnsSuffixLength;
+ const std::string index_type = key.substr(kFileIndexPrefixLength,
index_type_length);
+ if (index_type.empty()) {
+ return Status::Invalid(fmt::format("Invalid file index option {}",
key));
+ }
+ // TODO(jinli.zjw): Align malformed list option parsing (for example,
"f1,f2,,") with Java.
+ // Update this together with ConfigParser::ParseList to keep option
parsing consistent.
+ for (std::string column_name : StringUtils::Split(value, ",",
/*ignore_empty=*/false)) {
+ StringUtils::Trim(&column_name);
+ if (column_name.empty()) {
+ return Status::Invalid(
+ fmt::format("Wrong option in {}, should not have empty
column", key));
+ }
+ if (column_name.find('[') != std::string::npos &&
+ StringUtils::EndsWith(column_name, "]")) {
+ return Status::NotImplemented(
+ "Writing file indexes for nested map columns is not
supported");
+ }
+ if (declared.emplace(column_name, index_type).second) {
+ result.definitions_.push_back({column_name, index_type, {}});
+ }
+ }
+ }
+
+ for (const auto& [key, value] : raw_options) {
+ if (!StringUtils::StartsWith(key, kFileIndexPrefix) ||
+ StringUtils::EndsWith(key, kColumnsSuffix) ||
+ key == Options::FILE_INDEX_IN_MANIFEST_THRESHOLD) {
+ continue;
+ }
+ std::vector<std::string> parts =
+ StringUtils::Split(key.substr(kFileIndexPrefixLength), ".",
/*ignore_empty=*/false);
+ if (parts.size() != 3) {
+ continue;
+ }
+ bool found = false;
+ for (FileIndexDefinition& definition : result.definitions_) {
+ if (definition.index_type == parts[0] && definition.column_name ==
parts[1]) {
+ definition.options[parts[2]] = value;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return Status::Invalid(
+ fmt::format("Wrong file index option '{}': column '{}' is not
declared in "
+ "'file-index.{}.columns'",
+ key, parts[1], parts[0]));
+ }
+ }
+ return result;
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/io/data_file_writer_factory.h
b/src/paimon/core/io/file_index_options.h
similarity index 50%
copy from src/paimon/core/io/data_file_writer_factory.h
copy to src/paimon/core/io/file_index_options.h
index c727b47d..7b7c019b 100644
--- a/src/paimon/core/io/data_file_writer_factory.h
+++ b/src/paimon/core/io/file_index_options.h
@@ -20,41 +20,44 @@
#pragma once
#include <cstdint>
-#include <memory>
+#include <map>
+#include <string>
+#include <vector>
-#include "paimon/core/core_options.h"
#include "paimon/result.h"
-namespace arrow {
-class Schema;
-} // namespace arrow
-
namespace paimon {
-class FileFormat;
-class FormatStatsExtractor;
-class MemoryPool;
-class WriterBuilder;
+class CoreOptions;
+
+struct FileIndexDefinition {
+ std::string column_name;
+ std::string index_type;
+ std::map<std::string, std::string> options;
+};
-class DataFileWriterFactory {
+/// Parsed write-side file index configuration.
+class FileIndexOptions {
public:
- DataFileWriterFactory(const CoreOptions& options, int64_t schema_id,
- const std::shared_ptr<MemoryPool>& pool);
- virtual ~DataFileWriterFactory() = default;
-
- protected:
- struct WriterResources {
- std::shared_ptr<WriterBuilder> writer_builder;
- std::shared_ptr<FormatStatsExtractor> stats_extractor;
- };
-
- Result<WriterResources> CreateWriterResources(const FileFormat& format,
- const
std::shared_ptr<arrow::Schema>& file_schema,
- bool create_stats_extractor)
const;
-
- CoreOptions options_;
- int64_t schema_id_;
- std::shared_ptr<MemoryPool> pool_;
+ static Result<FileIndexOptions> FromCoreOptions(const CoreOptions&
options);
+
+ const std::vector<FileIndexDefinition>& Definitions() const {
+ return definitions_;
+ }
+
+ int64_t InManifestThreshold() const {
+ return in_manifest_threshold_;
+ }
+
+ bool Empty() const {
+ return definitions_.empty();
+ }
+
+ private:
+ FileIndexOptions() = default;
+
+ std::vector<FileIndexDefinition> definitions_;
+ int64_t in_manifest_threshold_ = 0;
};
} // namespace paimon
diff --git a/src/paimon/core/io/file_index_options_test.cpp
b/src/paimon/core/io/file_index_options_test.cpp
new file mode 100644
index 00000000..157203f9
--- /dev/null
+++ b/src/paimon/core/io/file_index_options_test.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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/file_index_options.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "paimon/core/core_options.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+namespace {
+
+Result<FileIndexOptions> ParseOptions(const std::map<std::string,
std::string>& index_options) {
+ std::shared_ptr<LocalFileSystem> file_system =
std::make_shared<LocalFileSystem>();
+ PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
+ CoreOptions::FromMap(index_options, file_system));
+ return FileIndexOptions::FromCoreOptions(core_options);
+}
+
+} // namespace
+
+TEST(FileIndexOptionsTest, TestRejectOverlappingPrefixAndSuffix) {
+ ASSERT_NOK_WITH_MSG(ParseOptions({{"file-index.columns", "f0"}}),
+ "Invalid file index option file-index.columns");
+}
+
+TEST(FileIndexOptionsTest, TestNestedMapColumnSyntax) {
+ ASSERT_OK_AND_ASSIGN(FileIndexOptions options,
+ ParseOptions({{"file-index.bitmap.columns",
"col[key"}}));
+ ASSERT_EQ(1, options.Definitions().size());
+ ASSERT_EQ("col[key", options.Definitions()[0].column_name);
+
+ ASSERT_NOK_WITH_MSG(ParseOptions({{"file-index.bitmap.columns",
"col[key]"}}),
+ "nested map columns is not supported");
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/core/io/key_value_data_file_writer.cpp
b/src/paimon/core/io/key_value_data_file_writer.cpp
index 9393c7c3..9c32e067 100644
--- a/src/paimon/core/io/key_value_data_file_writer.cpp
+++ b/src/paimon/core/io/key_value_data_file_writer.cpp
@@ -25,7 +25,6 @@
#include <utility>
#include <variant>
-#include "arrow/type.h"
#include "fmt/format.h"
#include "paimon/common/data/binary_array.h"
#include "paimon/common/data/binary_array_writer.h"
@@ -53,7 +52,7 @@ KeyValueDataFileWriter::KeyValueDataFileWriter(
const std::shared_ptr<FormatStatsExtractor>& stats_extractor,
const std::shared_ptr<arrow::Schema>& write_schema, bool is_external_path,
const std::shared_ptr<MemoryPool>& pool)
- : SingleFileWriter(compression, converter),
+ : DataFileWriterBase(compression, std::move(converter)),
pool_(pool),
schema_id_(schema_id),
level_(level),
@@ -64,10 +63,6 @@ KeyValueDataFileWriter::KeyValueDataFileWriter(
is_external_path_(is_external_path),
disable_stats_(stats_extractor == nullptr) {}
-void KeyValueDataFileWriter::SetMetadataFinalizer(MetadataFinalizer finalizer)
{
- metadata_finalizer_ = std::move(finalizer);
-}
-
Status KeyValueDataFileWriter::Write(KeyValueBatch batch) {
// update min and max key
if (!min_key_) {
@@ -80,19 +75,7 @@ Status KeyValueDataFileWriter::Write(KeyValueBatch batch) {
// update delete row count
delete_row_count_ += batch.delete_row_count;
- PAIMON_RETURN_NOT_OK(SingleFileWriter::Write(std::move(batch)));
- return Status::OK();
-}
-
-Status KeyValueDataFileWriter::BeforeFinish() {
- if (metadata_finalizer_) {
- PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> updated_schema,
- metadata_finalizer_());
- if (updated_schema) {
- PAIMON_RETURN_NOT_OK(UpdateSchema(updated_schema));
- }
- }
- return Status::OK();
+ return WriteRecordWithFileIndex(std::move(batch));
}
Result<std::shared_ptr<DataFileMeta>> KeyValueDataFileWriter::GetResult() {
@@ -120,14 +103,14 @@ Result<std::shared_ptr<DataFileMeta>>
KeyValueDataFileWriter::GetResult() {
final_path = external_path.ToString();
}
PAIMON_ASSIGN_OR_RAISE(int64_t local_micro,
DateTimeUtils::GetCurrentLocalTimeUs());
+ const FileIndexWriteResult& file_index = GetFileIndexWriteResult();
return std::make_shared<DataFileMeta>(
PathUtil::GetName(path_), output_bytes_, RecordCount(), min_key,
max_key, key_stats,
value_stats, min_sequence_number_, max_sequence_number_, schema_id_,
level_,
- /*extra_files=*/std::vector<std::optional<std::string>>(),
+ file_index.extra_files,
Timestamp(/*millisecond=*/local_micro / 1000,
/*nano_of_millisecond=*/0), delete_row_count_,
- /*embedded_index=*/nullptr, file_source_,
- /*value_stats_cols=*/std::nullopt, final_path,
/*first_row_id=*/std::nullopt,
- /*write_cols=*/std::nullopt);
+ file_index.embedded_index, file_source_,
/*value_stats_cols=*/std::nullopt, final_path,
+ /*first_row_id=*/std::nullopt, /*write_cols=*/std::nullopt);
}
Status KeyValueDataFileWriter::GenerateMinMaxKey(BinaryRow* min_key,
BinaryRow* max_key) const {
diff --git a/src/paimon/core/io/key_value_data_file_writer.h
b/src/paimon/core/io/key_value_data_file_writer.h
index e1e3fd92..eb7a2efc 100644
--- a/src/paimon/core/io/key_value_data_file_writer.h
+++ b/src/paimon/core/io/key_value_data_file_writer.h
@@ -17,6 +17,7 @@
*/
#pragma once
+
#include <cstdint>
#include <functional>
#include <limits>
@@ -25,7 +26,7 @@
#include <vector>
#include "paimon/core/io/data_file_meta.h"
-#include "paimon/core/io/single_file_writer.h"
+#include "paimon/core/io/data_file_writer_base.h"
#include "paimon/core/key_value.h"
#include "paimon/core/manifest/file_source.h"
#include "paimon/result.h"
@@ -44,14 +45,8 @@ class InternalRow;
class MemoryPool;
class SimpleStats;
-class KeyValueDataFileWriter
- : public SingleFileWriter<KeyValueBatch, std::shared_ptr<DataFileMeta>> {
+class KeyValueDataFileWriter : public DataFileWriterBase<KeyValueBatch> {
public:
- /// Callback invoked during BeforeFinish() to finalize file metadata.
- /// Produces an updated schema with per-field metadata (e.g. shredding
metadata)
- /// and may perform other finalization work (e.g. reporting stats to
cross-file context).
- using MetadataFinalizer =
std::function<Result<std::shared_ptr<arrow::Schema>>()>;
-
KeyValueDataFileWriter(const std::string& compression,
std::function<Status(KeyValueBatch&&,
::ArrowArray*)> converter,
int64_t schema_id, int32_t level, FileSource
file_source,
@@ -60,17 +55,10 @@ class KeyValueDataFileWriter
const std::shared_ptr<arrow::Schema>& write_schema,
bool is_external_path, const
std::shared_ptr<MemoryPool>& pool);
- /// Sets the metadata finalizer. Called during BeforeFinish() to produce
an updated
- /// schema and perform finalization callbacks. Must be set before Close().
- void SetMetadataFinalizer(MetadataFinalizer finalizer);
-
Status Write(KeyValueBatch batch) override;
Result<std::shared_ptr<DataFileMeta>> GetResult() override;
- protected:
- Status BeforeFinish() override;
-
private:
Result<std::vector<std::shared_ptr<ColumnStats>>> GetFieldStats();
@@ -96,7 +84,6 @@ class KeyValueDataFileWriter
int64_t max_sequence_number_ = std::numeric_limits<int64_t>::min();
std::shared_ptr<InternalRow> min_key_;
std::shared_ptr<InternalRow> max_key_;
- MetadataFinalizer metadata_finalizer_;
};
} // namespace paimon
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 07d50b98..8f388559 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
@@ -24,6 +24,7 @@
#include "arrow/c/helpers.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/key_value_data_file_writer.h"
#include "paimon/format/file_format.h"
@@ -60,6 +61,11 @@ KeyValueDataFileWriterFactory::CreateWriter() const {
options_.GetWriteFileCompression(level_), std::move(converter),
schema_id_, level_,
file_source_, primary_keys_, resources.stats_extractor, write_schema_,
path_factory_->IsExternalPath(), pool_);
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter>
file_index_writer,
+ CreateFileIndexWriter(write_schema_,
path_factory_));
+ if (file_index_writer) {
+ writer->SetFileIndexWriter(std::move(file_index_writer),
write_schema_);
+ }
PAIMON_RETURN_NOT_OK(
writer->Init(options_.GetFileSystem(), path_factory_->NewPath(),
resources.writer_builder));
return std::unique_ptr<SingleFileWriter<KeyValueBatch,
std::shared_ptr<DataFileMeta>>>(
diff --git a/src/paimon/core/io/shredding_append_data_file_writer_factory.cpp
b/src/paimon/core/io/shredding_append_data_file_writer_factory.cpp
index 6e4843bb..0e4e8219 100644
--- a/src/paimon/core/io/shredding_append_data_file_writer_factory.cpp
+++ b/src/paimon/core/io/shredding_append_data_file_writer_factory.cpp
@@ -23,6 +23,7 @@
#include "arrow/c/helpers.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/data_file_writer.h"
#include "paimon/core/io/infer_shredding_file_writer.h"
@@ -89,6 +90,11 @@ ShreddingAppendDataFileWriterFactory::CreateShreddedWriter(
options_.GetFileCompression(), std::move(batch_converter), schema_id_,
seq_num_counter,
file_source_, resources.stats_extractor,
path_factory_->IsExternalPath(), write_cols_,
pool_);
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter>
file_index_writer,
+ CreateFileIndexWriter(write_schema_,
path_factory_));
+ if (file_index_writer) {
+ writer->SetFileIndexWriter(std::move(file_index_writer),
write_schema_);
+ }
PAIMON_RETURN_NOT_OK(
writer->Init(options_.GetFileSystem(), path_factory_->NewPath(),
resources.writer_builder));
ShreddingWritePlanFactory::MetadataFinalizer finalizer =
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 8ac583ee..30d4c9fc 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
@@ -23,6 +23,7 @@
#include "arrow/c/helpers.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/infer_shredding_file_writer.h"
#include "paimon/core/io/key_value_data_file_writer.h"
@@ -88,6 +89,11 @@ ShreddingKeyValueDataFileWriterFactory::CreateShreddedWriter(
options_.GetWriteFileCompression(level_), std::move(batch_converter),
schema_id_, level_,
file_source_, primary_keys_, resources.stats_extractor, file_schema,
path_factory_->IsExternalPath(), pool_);
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DataFileIndexWriter>
file_index_writer,
+ CreateFileIndexWriter(write_schema_,
path_factory_));
+ if (file_index_writer) {
+ writer->SetFileIndexWriter(std::move(file_index_writer),
write_schema_);
+ }
PAIMON_RETURN_NOT_OK(
writer->Init(options_.GetFileSystem(), path_factory_->NewPath(),
resources.writer_builder));
ShreddingWritePlanFactory::MetadataFinalizer finalizer =
diff --git a/src/paimon/core/io/single_file_writer.h
b/src/paimon/core/io/single_file_writer.h
index 99507b57..6db3a699 100644
--- a/src/paimon/core/io/single_file_writer.h
+++ b/src/paimon/core/io/single_file_writer.h
@@ -25,6 +25,7 @@
#include <memory>
#include <string>
#include <utility>
+#include <vector>
#include "arrow/c/abi.h"
#include "arrow/c/bridge.h"
@@ -64,21 +65,27 @@ class SingleFileWriter : public FileWriter<T, R> {
class AbortExecutor {
public:
AbortExecutor(const std::shared_ptr<FileSystem>& fs, const
std::string& path)
- : fs_(fs), path_(path),
logger_(Logger::GetLogger("AbortExecutor")) {}
+ : paths_({{fs, path}}),
logger_(Logger::GetLogger("AbortExecutor")) {}
+
+ void Add(const std::shared_ptr<FileSystem>& fs, const std::string&
path) {
+ paths_.emplace_back(fs, path);
+ }
void Abort() {
- if (fs_) {
- auto status = fs_->Delete(path_);
+ for (const auto& [fs, path] : paths_) {
+ if (!fs) {
+ continue;
+ }
+ auto status = fs->Delete(path);
if (!status.ok()) {
- PAIMON_LOG_WARN(logger_, "Exception occurs when deleting
%s: %s", path_.c_str(),
+ PAIMON_LOG_WARN(logger_, "Exception occurs when deleting
%s: %s", path.c_str(),
status.ToString().c_str());
}
}
}
private:
- std::shared_ptr<FileSystem> fs_;
- std::string path_;
+ std::vector<std::pair<std::shared_ptr<FileSystem>, std::string>>
paths_;
std::shared_ptr<Logger> logger_;
};
@@ -132,6 +139,11 @@ class SingleFileWriter : public FileWriter<T, R> {
return Status::OK();
}
+ /// Hook called after the data file is closed and before its completion
callback is published.
+ virtual Status BeforeCompletion() {
+ return Status::OK();
+ }
+
/// Serializes schema and forwards it as file metadata to FormatWriter.
Status UpdateSchema(const std::shared_ptr<arrow::Schema>& schema);
@@ -239,6 +251,7 @@ Status SingleFileWriter<T, R>::Close() {
// guard still removes the file on a callback error, while a repeated
Close() does not publish
// the same file again.
closed_ = true;
+ PAIMON_RETURN_NOT_OK(BeforeCompletion());
if (completion_callback_) {
PAIMON_RETURN_NOT_OK(completion_callback_());
}
diff --git a/test/inte/write_and_read_inte_test.cpp
b/test/inte/write_and_read_inte_test.cpp
index 82eb5257..5916fc97 100644
--- a/test/inte/write_and_read_inte_test.cpp
+++ b/test/inte/write_and_read_inte_test.cpp
@@ -486,6 +486,89 @@ TEST_P(WriteAndReadInteTest,
TestAppendVectorWithPredicate) {
ASSERT_TRUE(expected->Equals(actual)) << actual->ToString();
}
+// TODO(jinli.zjw): move to a single file for a file index inte test
+TEST_P(WriteAndReadInteTest,
TestAppendWithExternalBitmapAndRangeBitmapIndexes) {
+ arrow::FieldVector fields = {arrow::field("name", arrow::utf8()),
+ arrow::field("score", arrow::int32())};
+ auto [file_format, file_system] = GetParam();
+ std::map<std::string, std::string> options = {
+ {Options::MANIFEST_FORMAT, "avro"},
+ {Options::FILE_FORMAT, file_format},
+ {Options::TARGET_FILE_SIZE, "1MB"},
+ {Options::BUCKET, "-1"},
+ {Options::FILE_SYSTEM, file_system},
+ {"file-index.bitmap.columns", "name"},
+ {"file-index.range-bitmap.columns", "score"},
+ {"file-index.range-bitmap.score.chunk-size", "1KB"},
+ {Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1B"},
+ };
+ if (file_system == "jindo") {
+ options = AddOptionsForJindo(options);
+ }
+
+ auto schema = arrow::schema(fields);
+ ASSERT_OK_AND_ASSIGN(auto helper, TestHelper::Create(test_dir_, schema,
/*partition_keys=*/{},
+ /*primary_keys=*/{},
options,
+
/*is_streaming_mode=*/false));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields),
R"([
+ ["Alice", 10],
+ ["Bob", 20],
+ ["Alice", 30],
+ ["Lucy", 40]
+ ])",
+ /*partition_map=*/{},
/*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
+
/*expected_commit_messages=*/std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(auto data_files, CurrentDataFiles(options));
+ ASSERT_EQ(1, data_files.size());
+ const auto& [bucket_path, data_file] = data_files[0];
+ ASSERT_FALSE(data_file->embedded_index);
+ ASSERT_EQ(1, data_file->extra_files.size());
+ ASSERT_TRUE(data_file->extra_files[0]);
+ ASSERT_EQ(data_file->file_name + ".index",
data_file->extra_files[0].value());
+ std::string index_path = PathUtil::JoinPath(bucket_path,
data_file->extra_files[0].value());
+ ASSERT_OK_AND_ASSIGN(bool index_exists,
dir_->GetFileSystem()->Exists(index_path));
+ ASSERT_TRUE(index_exists);
+
+ std::string indexed_name = "Alice";
+ auto name_predicate = PredicateBuilder::Equal(
+ /*field_index=*/0, /*field_name=*/"name", FieldType::STRING,
+ Literal(FieldType::STRING, indexed_name.data(), indexed_name.size()));
+ auto score_predicate = PredicateBuilder::GreaterThan(
+ /*field_index=*/1, /*field_name=*/"score", FieldType::INT,
Literal(20));
+ ASSERT_OK_AND_ASSIGN(auto predicate,
PredicateBuilder::And({name_predicate, score_predicate}));
+
+ std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar");
+ ScanContextBuilder scan_context_builder(table_path);
+ scan_context_builder.SetOptions(options)
+ .AddOption(Options::SCAN_MODE, StartupMode::LatestFull().ToString())
+ .SetPredicate(predicate);
+ ASSERT_OK_AND_ASSIGN(auto scan_context, scan_context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_scan,
TableScan::Create(std::move(scan_context)));
+ ASSERT_OK_AND_ASSIGN(auto plan, table_scan->CreatePlan());
+ ASSERT_EQ(1, plan->Splits().size());
+
+ // Keep precise post-read filtering disabled. The exact result therefore
verifies that the
+ // bitmap and range-bitmap indexes produced by the write path are consumed
by the read path.
+ ReadContextBuilder read_context_builder(table_path);
+ read_context_builder.SetOptions(options).SetPredicate(predicate);
+ ASSERT_OK_AND_ASSIGN(auto read_context, read_context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
+ ASSERT_OK_AND_ASSIGN(auto batch_reader,
table_read->CreateReader(plan->Splits()));
+ ASSERT_OK_AND_ASSIGN(auto actual,
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ arrow::FieldVector fields_with_row_kind = fields;
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+ auto expected_result = arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_(fields_with_row_kind), R"([[0, "Alice", 30]])");
+ ASSERT_TRUE(expected_result.ok()) << expected_result.status().ToString();
+ auto expected =
std::make_shared<arrow::ChunkedArray>(expected_result.ValueOrDie());
+ ASSERT_TRUE(expected->Equals(actual)) << actual->ToString();
+}
+
TEST_P(WriteAndReadInteTest, TestPKSimple) {
arrow::FieldVector fields = {
arrow::field("pk", arrow::utf8()),