Skip to content

feat(file-index): support writing file indexes - #210

Merged
SteNicholas merged 5 commits into
apache:mainfrom
zjw1111:agent/file-index-write
Aug 20, 2026
Merged

feat(file-index): support writing file indexes#210
SteNicholas merged 5 commits into
apache:mainfrom
zjw1111:agent/file-index-write

Conversation

@zjw1111

@zjw1111 zjw1111 commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Linked issue: #173

Add the File Index write path and initially support Bitmap and Range Bitmap indexes.

  • Add the FileIndexFormat::Writer API and Java-compatible V1 index serialization.
  • Build per-column indexes while writing append-only, key-value, and shredding data files.
  • Embed small indexes in DataFileMeta and publish larger indexes as extra .index files.
  • Share data-writer completion, metadata finalization, writer-level index publication, and abort bookkeeping through DataFileWriterBase.
  • Add a pool-backed ByteArrayOutputStream on top of MemorySegmentOutputStream.
  • Parse File Index options from core options and expose the manifest threshold through CoreOptions.
  • Add an end-to-end append-table test which writes external Bitmap and Range Bitmap indexes and consumes them during predicate reads.

Bloom Filter and BSI writers are intentionally deferred to a follow-up change.

This PR covers writer-level and rolling-writer abort cleanup. Cleanup of DataFileMeta.extra_files from snapshot expiration, commit abort, and append/merge-tree intermediate compaction paths is tracked as follow-up work.

Tests

  • cmake --build build --target unittest -j "$(nproc)" built all unit and integration test targets. The initial run passed 26 of 27 test executables and exposed a null-buffer validation issue in the new ByteArrayOutputStream test; the issue was fixed.
  • ./build/debug/paimon-common-test passed all 1439 tests after the fix.
  • ./build/debug/paimon-core-test --gtest_filter='DataFileIndexWriterTest.*:CoreOptionsTest.*:AppendOnlyWriterTest.*' passed all 56 selected tests after relinking the final library.
  • The latest review refinements rebuilt paimon-core-test and passed 10 focused tests covering append, key-value, DataFileIndexWriter, and FileIndexOptions.
  • Added WriteAndReadInteTest.TestAppendWithExternalBitmapAndRangeBitmapIndexes to the parameterized end-to-end suite.
  • pre-commit run --from-ref upstream/main --to-ref HEAD passed during implementation; the latest changed production files also passed clang-format --dry-run --Werror and git diff --check.

API and Format

Yes. This adds the public FileIndexFormat::Writer API and writes the Java-compatible V1 File Index container format. Existing read formats and existing public APIs are not changed incompatibly.

Documentation

No user-facing documentation changes in this PR.

Generative AI tooling

Generated-by: OpenAI Codex (GPT-5)

@zjw1111
zjw1111 force-pushed the agent/file-index-write branch from 580352e to 18fe9aa Compare August 17, 2026 07:49
@zjw1111
zjw1111 marked this pull request as ready for review August 17, 2026 07:50
Comment thread include/paimon/file_index/file_index_format.h
Comment thread src/paimon/common/file_index/file_index_format.cpp Outdated
Comment thread src/paimon/common/file_index/file_index_format.cpp
Comment thread src/paimon/common/file_index/file_index_format_test.cpp Outdated
Comment thread src/paimon/common/io/byte_array_output_stream.cpp Outdated
const std::vector<MemorySegment>& segments = output_.Segments();
result_ = std::shared_ptr<Bytes>(new Bytes(static_cast<size_t>(position_), pool_.get()),
[pool = pool_](Bytes* bytes) { delete bytes; });
MemorySegmentUtils::CopyToBytes(segments, /*offset=*/0, result_.get(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation feels a bit unusual. Would it make sense to add a helper on Bytes instead, for example:

static std::shared_ptr<Bytes> AllocateShared(
    size_t size, std::shared_ptr<MemoryPool> pool);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I removed the custom deleter instead of adding AllocateShared. ByteArrayOutputStream::Finish now receives a MemoryPool* and constructs the result with std::make_shared. The API documents that the caller must keep the pool alive until the returned Bytes is destroyed, and the upper layer guarantees that lifetime. With this ownership contract, a helper that captures shared_ptr is no longer needed.

Comment thread src/paimon/common/io/memory_segment_output_stream.cpp
int64_t in_manifest_threshold_;
std::shared_ptr<FileSystem> file_system_;
std::shared_ptr<DataFilePathFactory> path_factory_;
std::shared_ptr<MemoryPool> pool_;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is pool_ placed last because none of the other member variables in this class use it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is about destruction order. Members are destroyed in reverse declaration order. The earlier members do not rely on DataFileIndexWriter::pool_ during destruction: each current FileIndexWriter keeps its own shared_ptr. pool_ here is used directly only while serializing the container, so declaring it after writers_ is safe. If a future member borrows this pool instead of owning it, pool_ should be declared before that member.

int64_t record_count = batch->length;
PAIMON_RETURN_NOT_OK(SingleFileWriter::Write(batch));
PAIMON_RETURN_NOT_OK(WriteRecord(batch, batch));
seq_num_counter_->Add(record_count);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two parameters of WriteRecord are hard to understand in the current form.

return Status::Invalid(fmt::format("Invalid file index option {}", key));
}
for (std::string column_name : StringUtils::Split(value, ",", /*ignore_empty=*/false)) {
StringUtils::Trim(&column_name);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore_empty=true or false?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We intentionally keep ignore_empty=false so leading or middle empty column entries, such as f1,,f2, are rejected instead of silently ignored. Java String.split drops trailing empty tokens, so malformed values such as f1,f2,, currently behave differently. I reverted the ad-hoc special case and added a TODO to align this together with ConfigParser::ParseList, so list option parsing can be changed consistently.

@lxy-9602 lxy-9602 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@zjw1111
zjw1111 force-pushed the agent/file-index-write branch from 8ae9807 to e3fe5c7 Compare August 20, 2026 03:00

@SteNicholas SteNicholas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@SteNicholas
SteNicholas merged commit 8a110d4 into apache:main Aug 20, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants