This is an automated email from the ASF dual-hosted git repository.
suxiaogang223 pushed a commit to branch feature/paimon-jni-write-v1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/feature/paimon-jni-write-v1 by
this push:
new 1341369a91e [improvement](paimon) Optimize PaimonJniWriter and cleanup
dead code
1341369a91e is described below
commit 1341369a91ea790769182f9cd60622780596251d
Author: Socrates <[email protected]>
AuthorDate: Mon Jul 13 15:42:21 2026 +0800
[improvement](paimon) Optimize PaimonJniWriter and cleanup dead code
### What problem does this PR solve?
Problem Summary:
1. PaimonJniWriter.writeBatch() had per-cell instanceof dispatch via
readArrowValue(), causing ~819200 instanceof checks per batch
(4096 rows × 20 columns × ~10 instanceof per cell).
2. Dead code accumulated after the single-writer architecture refactor:
- paimon_bucket.h/.cpp (BE-side MurmurHash routing, now delegated to SDK)
- PhysicalPaimonTableSink bucked-shuffle logic (unused GATHER override)
- PaimonTableSink bucketNum/bucketKeyIndices Thrift payload
- Thrift TPaimonTableSink.bucket_num / bucket_key_indices fields
- Thrift TPaimonCommitMessage.partition / bucket fields
3. Missing DECLARE_OPERATOR(PaimonTableSinkLocalState) in operator.cpp
Root cause:
- writeBatch bottleneck: per-cell instanceof chain for every Arrow value
- cleanup: v0 per-bucket routing infrastructure no longer needed after
switching to SDK-internal routing
Fix:
- writeBatch: column-major typed extraction (1 instanceof per column),
group by SDK getPartition()/getBucket(), writeBundle() per group
- Remove paimon_bucket.h/.cpp (222+71 lines)
- Remove PhysicalPaimonTableSink bucket shuffle logic
- Remove PaimonTableSink bucketNum/bucketKeyIndices Thrift fields
- Simplify Thrift: TPaimonTableSink (remove fields 6,11),
TPaimonCommitMessage (remove fields 1,2, keep payload)
- Add DECLARE_OPERATOR(PaimonTableSinkLocalState)
- Stop tracking PAIMON_WRITE_DESIGN.md in git
### Release note
None
### Check List (For Author)
- Test: FE Maven compile + checkstyle verified
- Behavior changed: Yes (writeBatch optimized, dead code removed)
- Does this need documentation: No
---
be/src/exec/operator/operator.cpp | 1 +
be/src/exec/sink/writer/paimon/paimon_bucket.cpp | 222 --
be/src/exec/sink/writer/paimon/paimon_bucket.h | 71 -
.../exec/sink/writer/paimon/vpaimon_table_writer.h | 3 +-
.../paimon-scanner/PAIMON_WRITE_DESIGN.md | 2139 --------------------
.../org/apache/doris/paimon/PaimonJniWriter.java | 261 ++-
.../plans/physical/PhysicalPaimonTableSink.java | 31 +-
.../org/apache/doris/planner/PaimonTableSink.java | 29 +-
gensrc/thrift/DataSinks.thrift | 16 +-
9 files changed, 210 insertions(+), 2563 deletions(-)
diff --git a/be/src/exec/operator/operator.cpp
b/be/src/exec/operator/operator.cpp
index 1f255078b5d..d88f38b64fe 100644
--- a/be/src/exec/operator/operator.cpp
+++ b/be/src/exec/operator/operator.cpp
@@ -833,6 +833,7 @@ DECLARE_OPERATOR(OlapTableSinkV2LocalState)
DECLARE_OPERATOR(HiveTableSinkLocalState)
DECLARE_OPERATOR(TVFTableSinkLocalState)
DECLARE_OPERATOR(IcebergTableSinkLocalState)
+DECLARE_OPERATOR(PaimonTableSinkLocalState)
DECLARE_OPERATOR(SpillIcebergTableSinkLocalState)
DECLARE_OPERATOR(IcebergDeleteSinkLocalState)
DECLARE_OPERATOR(IcebergMergeSinkLocalState)
diff --git a/be/src/exec/sink/writer/paimon/paimon_bucket.cpp
b/be/src/exec/sink/writer/paimon/paimon_bucket.cpp
deleted file mode 100644
index ae07824aa53..00000000000
--- a/be/src/exec/sink/writer/paimon/paimon_bucket.cpp
+++ /dev/null
@@ -1,222 +0,0 @@
-// 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 "exec/sink/writer/paimon/paimon_bucket.h"
-
-#include <cstring>
-
-#include "core/column/column.h"
-#include "core/column/column_nullable.h"
-#include "core/column/column_vector.h"
-
-namespace doris {
-
-// ────────────────────────────────────────────────────────────
-// Murmur3-32 hash (seed=42, word-aligned input)
-// Matches Paimon's MurmurHashUtils.hashBytesByWords
-// ────────────────────────────────────────────────────────────
-
-static constexpr uint32_t MURMUR_C1 = 0xcc9e2d51;
-static constexpr uint32_t MURMUR_C2 = 0x1b873593;
-static constexpr uint32_t MURMUR_SEED = 42;
-
-static uint32_t murmur_mix_k1(uint32_t k1) {
- k1 *= MURMUR_C1;
- k1 = (k1 << 15) | (k1 >> 17); // rotate_left(15)
- k1 *= MURMUR_C2;
- return k1;
-}
-
-static uint32_t murmur_mix_h1(uint32_t h1, uint32_t k1) {
- h1 ^= k1;
- h1 = (h1 << 13) | (h1 >> 19); // rotate_left(13)
- h1 = h1 * 5 + 0xe6546b64;
- return h1;
-}
-
-static uint32_t murmur_fmix(uint32_t h) {
- h ^= h >> 16;
- h *= 0x85ebca6b;
- h ^= h >> 13;
- h *= 0xc2b2ae35;
- h ^= h >> 16;
- return h;
-}
-
-int32_t paimon_murmur_hash(const uint8_t* data, size_t len) {
- DCHECK(len % 4 == 0) << "Murmur hash requires word-aligned input, got " <<
len;
- uint32_t h1 = MURMUR_SEED;
- for (size_t i = 0; i < len; i += 4) {
- uint32_t word = static_cast<uint32_t>(data[i]) |
(static_cast<uint32_t>(data[i + 1]) << 8) |
- (static_cast<uint32_t>(data[i + 2]) << 16) |
- (static_cast<uint32_t>(data[i + 3]) << 24);
- uint32_t k1 = murmur_mix_k1(word);
- h1 = murmur_mix_h1(h1, k1);
- }
- return static_cast<int32_t>(murmur_fmix(h1 ^ static_cast<uint32_t>(len)));
-}
-
-// ────────────────────────────────────────────────────────────
-// Binary row construction for bucket key hashing
-// ────────────────────────────────────────────────────────────
-
-/// Compute Paimon BinaryRow null-bit-set width.
-/// Matches BinaryRow.calBitSetWidthInBytes(arity).
-static inline int32_t cal_bit_set_width(int32_t arity) {
- return ((arity + 63 + 8 /* HEADER_SIZE_IN_BYTES */) / 64) * 8;
-}
-
-/// Compute the fixed-size portion of a Paimon BinaryRow in bytes.
-static inline int32_t fixed_row_size(int32_t arity) {
- return cal_bit_set_width(arity) + arity * 8;
-}
-
-/// Write a null bit at the given position.
-/// Matches BinaryRowBuilder.setNullAt(pos).
-static void set_null_bit(uint8_t* data, int32_t pos, bool is_null) {
- int32_t bit_index = pos + 8; // HEADER_SIZE_IN_BYTES
- int32_t byte_idx = bit_index / 8;
- int32_t bit_off = bit_index % 8;
- if (is_null) {
- data[byte_idx] |= (1 << bit_off);
- }
-}
-
-// ────────────────────────────────────────────────────────────
-// Per-column value extraction helpers
-// ────────────────────────────────────────────────────────────
-
-namespace {
-
-/// Write a single column value at `row_idx` into the binary row at the
-/// correct fixed-field offset. Returns the number of bytes actually
-/// written (always writes into an 8-byte slot at field_offset).
-void write_col_to_row(uint8_t* row_data, int32_t null_bytes_size, int32_t
field_pos,
- const ColumnPtr& col, size_t row_idx) {
- int32_t field_offset = null_bytes_size + field_pos * 8;
-
- // Handle nullable column: unwrap the data column
- const IColumn* data_col = col.get();
- bool is_null = false;
- if (col->is_nullable()) {
- auto* nullable = assert_cast<const ColumnNullable*>(col.get());
- is_null = nullable->is_null_at(row_idx);
- data_col = &nullable->get_nested_column();
- }
-
- set_null_bit(row_data, field_pos, is_null);
- if (is_null) {
- return; // field already zeroed from initialization
- }
-
- // Write value in little-endian into the field slot
- PrimitiveType ptype = data_col->get_data_type();
-
- if (ptype == TYPE_TINYINT || ptype == TYPE_BOOLEAN) {
- auto val = assert_cast<const
ColumnVector<Int8>*>(data_col)->get_element(row_idx);
- row_data[field_offset] = static_cast<uint8_t>(val);
- } else if (ptype == TYPE_SMALLINT) {
- auto val = assert_cast<const
ColumnVector<Int16>*>(data_col)->get_element(row_idx);
- std::memcpy(row_data + field_offset, &val, sizeof(val));
- } else if (ptype == TYPE_INT || ptype == TYPE_DATE || ptype ==
TYPE_DATEV2) {
- auto val = assert_cast<const
ColumnVector<Int32>*>(data_col)->get_element(row_idx);
- std::memcpy(row_data + field_offset, &val, sizeof(val));
- } else if (ptype == TYPE_BIGINT || ptype == TYPE_DATETIME || ptype ==
TYPE_DATETIMEV2) {
- auto val = assert_cast<const
ColumnVector<Int64>*>(data_col)->get_element(row_idx);
- std::memcpy(row_data + field_offset, &val, sizeof(val));
- } else if (ptype == TYPE_FLOAT) {
- float f = assert_cast<const
ColumnVector<Float32>*>(data_col)->get_element(row_idx);
- int32_t bits;
- std::memcpy(&bits, &f, sizeof(bits));
- std::memcpy(row_data + field_offset, &bits, sizeof(bits));
- } else if (ptype == TYPE_DOUBLE) {
- double d = assert_cast<const
ColumnVector<Float64>*>(data_col)->get_element(row_idx);
- int64_t bits;
- std::memcpy(&bits, &d, sizeof(bits));
- std::memcpy(row_data + field_offset, &bits, sizeof(bits));
- } else if (ptype == TYPE_DECIMAL32) {
- auto val = assert_cast<const
ColumnVector<Int32>*>(data_col)->get_element(row_idx);
- std::memcpy(row_data + field_offset, &val, sizeof(val));
- } else if (ptype == TYPE_DECIMAL64) {
- auto val = assert_cast<const
ColumnVector<Int64>*>(data_col)->get_element(row_idx);
- std::memcpy(row_data + field_offset, &val, sizeof(val));
- } else if (ptype == TYPE_DECIMAL128 || ptype == TYPE_DECIMAL128I) {
- auto val = assert_cast<const
ColumnVector<Int128>*>(data_col)->get_element(row_idx);
- std::memcpy(row_data + field_offset, &val, sizeof(val));
- } else {
- // For complex types (STRING, ARRAY, MAP, etc.), we cannot compute
- // the correct binary row in BE. Fallback to single-bucket routing.
- // Callers should check this before calling.
- LOG(WARNING) << "Unsupported bucket key type for BE-side computation: "
- << static_cast<int>(ptype);
- }
-}
-
-} // anonymous namespace
-
-// ────────────────────────────────────────────────────────────
-// Public API
-// ────────────────────────────────────────────────────────────
-
-int32_t paimon_compute_bucket(const Block& block, size_t row_idx,
- const std::vector<int>& bucket_key_cols, int32_t
num_buckets) {
- if (num_buckets <= 1 || bucket_key_cols.empty()) {
- return 0;
- }
-
- int32_t arity = static_cast<int32_t>(bucket_key_cols.size());
- int32_t row_size = fixed_row_size(arity);
- std::vector<uint8_t> row_data(row_size, 0);
- int32_t null_bytes_size = cal_bit_set_width(arity);
-
- for (int32_t i = 0; i < arity; ++i) {
- const auto& col = block.get_by_position(bucket_key_cols[i]).column;
- write_col_to_row(row_data.data(), null_bytes_size, i, col, row_idx);
- }
-
- int32_t hash = paimon_murmur_hash(row_data.data(), row_size);
- int32_t bucket = hash % num_buckets;
- return bucket < 0 ? bucket + num_buckets : bucket;
-}
-
-std::vector<int32_t> paimon_compute_buckets(const Block& block,
- const std::vector<int>&
bucket_key_cols,
- int32_t num_buckets) {
- if (num_buckets <= 1 || bucket_key_cols.empty()) {
- return std::vector<int32_t>(block.rows(), 0);
- }
-
- std::vector<int32_t> buckets(block.rows());
- int32_t arity = static_cast<int32_t>(bucket_key_cols.size());
- int32_t row_size = fixed_row_size(arity);
- std::vector<uint8_t> row_data(row_size, 0);
- int32_t null_bytes_size = cal_bit_set_width(arity);
-
- for (size_t row = 0; row < block.rows(); ++row) {
- std::memset(row_data.data(), 0, row_size);
- for (int32_t i = 0; i < arity; ++i) {
- const auto& col = block.get_by_position(bucket_key_cols[i]).column;
- write_col_to_row(row_data.data(), null_bytes_size, i, col, row);
- }
- int32_t hash = paimon_murmur_hash(row_data.data(), row_size);
- int32_t bucket = hash % num_buckets;
- buckets[row] = bucket < 0 ? bucket + num_buckets : bucket;
- }
- return buckets;
-}
-
-} // namespace doris
diff --git a/be/src/exec/sink/writer/paimon/paimon_bucket.h
b/be/src/exec/sink/writer/paimon/paimon_bucket.h
deleted file mode 100644
index 3283b34c6f7..00000000000
--- a/be/src/exec/sink/writer/paimon/paimon_bucket.h
+++ /dev/null
@@ -1,71 +0,0 @@
-// 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 <string>
-#include <vector>
-
-#include "core/block/block.h"
-#include "core/column/column.h"
-
-namespace doris {
-
-/// Paimon-compatible bucket computation.
-///
-/// Implements the same bucket assignment logic as Paimon's Java SDK:
-/// Default: Math.abs(BinaryRow(murmur_hash) % numBuckets)
-/// Mod: Math.floorMod(bucket_key_value, numBuckets)
-///
-/// The binary row format matches Paimon's BinaryRow layout:
-/// - null_bits_size bytes of null bit set (always 8-byte aligned,
-/// includes conceptual 8-byte header)
-/// - arity * 8 bytes of fixed-length field slots
-///
-/// Each field occupies 8 bytes in the fixed part, with values
-/// written in little-endian order within their actual type width.
-
-/// Murmur3-32 hash over word-aligned data, matching Paimon's
-/// MurmurHashUtils.hashBytesByWords with seed=42.
-int32_t paimon_murmur_hash(const uint8_t* data, size_t len);
-
-/// Compute bucket id for a block row.
-///
-/// @param block input block containing all columns
-/// @param row_idx row index in the block
-/// @param bucket_key_cols indices of bucket key columns in the block
-/// @param num_buckets total number of buckets
-/// @return bucket id in [0, num_buckets)
-int32_t paimon_compute_bucket(const Block& block, size_t row_idx,
- const std::vector<int>& bucket_key_cols, int32_t
num_buckets);
-
-/// Compute bucket ids for all rows in a block.
-///
-/// @return vector of bucket ids, one per row
-std::vector<int32_t> paimon_compute_buckets(const Block& block,
- const std::vector<int>&
bucket_key_cols,
- int32_t num_buckets);
-
-/// Simple mod bucket for a single integral key.
-/// Matches Paimon's ModBucketFunction.
-inline int32_t paimon_bucket_mod(int64_t value, int32_t num_buckets) {
- int32_t r = static_cast<int32_t>(value % num_buckets);
- return r < 0 ? r + num_buckets : r;
-}
-
-} // namespace doris
diff --git a/be/src/exec/sink/writer/paimon/vpaimon_table_writer.h
b/be/src/exec/sink/writer/paimon/vpaimon_table_writer.h
index d4ce5b04bde..b54d254ac9a 100644
--- a/be/src/exec/sink/writer/paimon/vpaimon_table_writer.h
+++ b/be/src/exec/sink/writer/paimon/vpaimon_table_writer.h
@@ -85,8 +85,7 @@ private:
// Single writer managing all partitions and buckets (SDK-internal routing)
std::unique_ptr<IPaimonWriter> _writer;
- // Statistics
- int64_t _written_rows = 0;
+ // Statistics (note: _written_rows is inherited from ResultWriter)
int64_t _written_bytes = 0;
// Profile counters
diff --git a/fe/be-java-extensions/paimon-scanner/PAIMON_WRITE_DESIGN.md
b/fe/be-java-extensions/paimon-scanner/PAIMON_WRITE_DESIGN.md
deleted file mode 100644
index 324a38704be..00000000000
--- a/fe/be-java-extensions/paimon-scanner/PAIMON_WRITE_DESIGN.md
+++ /dev/null
@@ -1,2139 +0,0 @@
-# Doris Paimon 写入功能架构设计
-
-> **设计原则**:从第一性原理出发,基于 Paimon Java SDK(功能完备)和 Paimon Rust(性能优先)的差异,设计一个统一的写入框架。
->
-> **核心策略**:Rust writer 和 Java writer 是**互相替代**的关系,Rust 是 Java 的功能子集。优先使用
Rust(性能),功能缺失时回退 Java。首个版本仅引入 Java JNI writer,但在架构上必须提前为 Rust FFI writer 预留接口。
-
-## 目录
-
-1. [背景与目标](#1-背景与目标)
-2. [Paimon 写入模型分析](#2-paimon-写入模型分析)
-3. [Java SDK vs Rust 差异矩阵](#3-java-sdk-vs-rust-差异矩阵)
-4. [Doris 现有写入框架分析](#4-doris-现有写入框架分析)
-5. [架构设计](#5-架构设计)
-6. [接口定义](#6-接口定义)
-7. [数据流与交互时序](#7-数据流与交互时序)
-8. [FE 端设计](#8-fe-端设计)
-9. [BE 端设计](#9-be-端设计)
-10. [功能路由矩阵](#10-功能路由矩阵)
-11. [实施路线图](#11-实施路线图)
-
----
-
-## 1. 背景与目标
-
-### 1.1 背景
-
-Doris 需要支持向 Paimon 表写入数据。目前有两个可用的 Paimon 客户端库:
-
-- **Paimon Java SDK** (`paimon-core`):功能最完整的参考实现,支持全量的写入、提交、Compaction、Schema
Evolution、Lookup Changelog Producer、Deletion Vector 等功能。
-- **Paimon Rust** (`paimon-rust`):高性能实现,使用 Arrow 原生格式,零拷贝,异步 I/O。但目前功能较 Java
版本有差距(缺少 Compaction、Lookup/Full-compaction Changelog Producer、部分 MergeEngine 与
Deletion Vector 的组合等)。
-
-### 1.2 目标
-
-1. **v1(当前)**:基于 Java SDK 通过 JNI 实现功能完备的 Paimon 写入。同时建立 `IPaimonWriteBackend`
抽象接口,为 Rust 后端预留设计空间。
-2. **v2(中期)**:引入 Paimon Rust 通过 FFI 加速写入。Rust 覆盖 Append-only +
固定桶等高吞吐场景,功能缺失时自动回退 Java。
-3. **长期**:随着 Paimon Rust 功能完善,逐步扩大 Rust 后端的覆盖范围,最终在大多数场景下优先使用 Rust。
-4. **架构目标**:`IPaimonWriteBackend` 是核心抽象——Java 和 Rust 是平等的后端实现,可以互相替代,上层(Doris
Sink)不感知差异。
-
-### 1.3 关键约束
-
-- Paimon C++ native 版本(paimon-cpp)未来会被删除,不考虑
-- Java SDK 通过 JNI 调用,Rust 通过 C FFI 调用
-- Doris BE 是 C++ 代码,需要通过 JNI(调用 Java)或 FFI(调用 Rust)来桥接
-- 写入流程包括:数据写入 → CommitMessage 生成 → Snapshot 提交
-- FE 端需要感知表类型(Paimon)来做查询规划和写入协调
-
----
-
-## 2. Paimon 写入模型分析
-
-### 2.1 核心写入模型(两阶段提交)
-
-Paimon 的写入模型是一个**两阶段提交**过程,无论 Java 还是 Rust 都遵循相同的抽象:
-
-```
-阶段1:Write(分布式)
- WriteBuilder → newWrite() → TableWrite
- → write(row/batch) × N // 每个 worker 写自己的数据
- → prepareCommit() // 关闭文件,生成 CommitMessage[]
- → CommitMessage[] // 序列化传递给 Coordinator
-
-阶段2:Commit(集中式)
- WriteBuilder → newCommit() → TableCommit
- → commit(CommitMessage[]) // 原子提交:写 Manifest → 创建 Snapshot
- → abort(CommitMessage[]) // 失败时清理数据文件
-```
-
-### 2.2 Java SDK 核心接口
-
-#### WriteBuilder(工厂)
-
-```java
-// 批写入构建器
-public interface BatchWriteBuilder extends WriteBuilder {
- BatchWriteBuilder withOverwrite();
- BatchWriteBuilder withOverwrite(Map<String, String> staticPartition);
- BatchTableWrite newWrite();
- BatchTableCommit newCommit();
-}
-
-// 流写入构建器
-public interface StreamWriteBuilder extends WriteBuilder {
- StreamWriteBuilder withCommitUser(String commitUser);
- StreamTableWrite newWrite();
- StreamTableCommit newCommit();
-}
-```
-
-**关键点**:`WriteBuilder` 同时创建 `TableWrite` 和 `TableCommit`。同一个 `WriteBuilder`
实例产出的 write 和 commit 共享 `commitUser`(用于幂等提交)。
-
-#### TableWrite(数据写入)
-
-```java
-public interface TableWrite extends AutoCloseable {
- TableWrite withIOManager(IOManager ioManager);
- TableWrite withWriteType(RowType writeType);
- TableWrite withMemoryPoolFactory(MemoryPoolFactory factory);
-
- // 分区/桶计算
- BinaryRow getPartition(InternalRow row);
- int getBucket(InternalRow row);
-
- // 逐行写入
- void write(InternalRow row) throws Exception;
- void write(InternalRow row, int bucket) throws Exception;
-
- // 批量写入(高效路径)
- void writeBundle(BinaryRow partition, int bucket, BundleRecords bundle)
throws Exception;
-
- // 压缩
- void compact(BinaryRow partition, int bucket, boolean fullCompaction)
throws Exception;
-
- // 生成提交消息(在 TableWriteImpl 上,非接口方法)
- // List<CommitMessage> prepareCommit(boolean waitCompaction, long
commitIdentifier)
-}
-```
-
-**关键设计点**:
-- `writeBundle()` 是高性能批量写入路径:调用者预先计算好 partition 和 bucket,将一批行打包为
`BundleRecords` 直接写入
-- `compact()` 是显式的 bucket 级压缩触发
-- `prepareCommit()` 返回 `List<CommitMessage>`,每个 `CommitMessage` 对应一个
(partition, bucket) 的新文件列表
-- `CommitMessage` 是 `Serializable` 的,可以跨网络传输
-
-#### CommitMessage
-
-```java
-public interface CommitMessage extends Serializable {
- BinaryRow partition(); // 分区键(二进制编码)
- int bucket(); // 桶编号
- Integer totalBuckets(); // 该分区的总桶数
-}
-```
-
-> **注意**:Java 的 `CommitMessage` 接口只暴露了 partition 和 bucket。实际的文件列表(newFiles,
changelogFiles, indexFiles 等)在实现类 `CommitMessageImpl` 中,通过
`CommitMessageSerializer` 序列化。这是一种封装设计——调用者不需要知道内部文件细节。
-
-#### BatchTableCommit
-
-```java
-public interface BatchTableCommit extends TableCommit {
- void commit(List<CommitMessage> commitMessages);
- void truncateTable();
- void truncatePartitions(List<Map<String, String>> partitionSpecs);
- void updateStatistics(Statistics statistics);
- void compactManifests();
-}
-```
-
-### 2.3 Rust 核心接口
-
-Rust 采用更直接的 API 设计:
-
-```rust
-// 工厂
-pub struct WriteBuilder<'a> { table, commit_user, overwrite }
-impl WriteBuilder {
- pub fn new_write(&self) -> Result<TableWrite>;
- pub fn new_commit(&self) -> TableCommit;
- pub fn with_overwrite(self) -> Self;
- pub fn with_commit_user(self, user) -> Result<Self>;
-}
-
-// 写入
-pub struct TableWrite { ... }
-impl TableWrite {
- pub fn with_overwrite(self) -> Self;
- pub async fn write_arrow_batch(&mut self, batch: &RecordBatch) ->
Result<()>;
- pub async fn write_arrow(&mut self, batches: &[RecordBatch]) -> Result<()>;
- pub async fn prepare_commit(&mut self) -> Result<Vec<CommitMessage>>;
-}
-
-// 提交
-pub struct TableCommit { ... }
-impl TableCommit {
- pub async fn commit(&self, messages: Vec<CommitMessage>) -> Result<()>;
- pub async fn overwrite(&self, messages: Vec<CommitMessage>,
- static_partitions: Option<HashMap<String,
Option<Datum>>>) -> Result<()>;
- pub async fn truncate_table(&self) -> Result<()>;
- pub async fn truncate_partitions(&self, partitions: Vec<HashMap<String,
Option<Datum>>>) -> Result<()>;
- pub async fn abort(&self, messages: &[CommitMessage]) -> Result<()>;
-}
-
-// 提交消息(具体结构体,所有字段公开)
-pub struct CommitMessage {
- pub partition: Vec<u8>,
- pub bucket: i32,
- pub new_files: Vec<DataFileMeta>,
- pub new_changelog_files: Vec<DataFileMeta>,
- pub new_index_files: Vec<IndexFileMeta>,
- pub deleted_index_files: Vec<IndexFileMeta>,
- pub deleted_files: Vec<DataFileMeta>,
- pub check_from_snapshot: Option<i64>,
-}
-```
-
-**关键差异**:
-- Rust 的 `CommitMessage` 是**具体结构体**,所有字段直接暴露,而 Java 通过序列化器隐藏内部细节
-- Rust 使用 Arrow `RecordBatch` 作为数据载体,Java 使用 `InternalRow`
-- Rust 的写入是 async 的,Java 是同步的(但在 Flink 等框架中被异步调用)
-
-### 2.4 写入功能全景
-
-| 功能 | Java SDK | Rust | 说明 |
-|------|----------|------|------|
-| **Append-only 写入** | ✅ | ✅ | 无主键表的顺序写入 |
-| **主键表写入** | ✅ | ✅ | Sorted Merge Tree,按主键排序去重 |
-| **固定桶 (Fixed Bucket)** | ✅ | ✅ | hash(bucket_key) % N |
-| **动态桶 (Dynamic Bucket)** | ✅ | ✅ | bucket=-1,自动分配桶 |
-| **跨分区桶 (Cross Partition)** | ✅ | ✅ | 动态桶 + 跨分区主键 |
-| **Postpone 桶 (bucket=-2)** | ✅ | ✅ | 延迟桶分配,用于高吞吐写入 |
-| **MergeEngine: Normal** | ✅ | ✅ | 最后写入胜出 |
-| **MergeEngine: PartialUpdate** | ✅ | ✅ | 部分列更新(Rust 不支持 +DV) |
-| **MergeEngine: Aggregation** | ✅ | ✅ | 聚合函数(Rust 不支持 +DV) |
-| **MergeEngine: FirstRow** | ✅ | ✅ | 保留第一行 |
-| **MergeEngine: Deduplicate** | ✅ | ✅ | 按主键去重 |
-| **ChangelogProducer: None** | ✅ | ✅ | 不产生 changelog |
-| **ChangelogProducer: Input** | ✅ | ✅ | 透传 changelog |
-| **ChangelogProducer: Lookup** | ✅ | ❌ | 提交时查找旧值生成 changelog |
-| **ChangelogProducer: FullCompaction** | ✅ | ❌ | 全量压缩时生成 changelog |
-| **Compaction** | ✅ | ❌ | 自动/手动合并,层级压缩 |
-| **Deletion Vector** | ✅ | 部分 | Rust: 不支持 PartialUpdate+DV、Aggregation+DV |
-| **Schema Evolution** | ✅ | ✅ | 列添加/删除/重命名/类型变更 |
-| **Row Tracking** | ✅ | ✅ | DataEvolution 的行 ID 追踪 |
-| **DataEvolution (MERGE INTO)** | ✅ | ✅ | 基于行 ID 的部分列更新 |
-| **CoW MERGE INTO** | ✅ | ✅ | 无 PK 表的 Copy-on-Write 合并 |
-| **分区覆盖 (Overwrite)** | ✅ | ✅ | 静态/动态分区覆盖 |
-| **Truncate** | ✅ | ✅ | 清空表/分区 |
-| **Blob 类型** | ✅ | ✅ | 大对象列分离存储 |
-| **全局索引 (Global Index)** | ✅ | 部分 | Rust 仅支持 DropPartitionIndex |
-| **二次索引 (BTree Index)** | ✅ | ✅ | |
-| **文件格式: Parquet** | ✅ | ✅ | 默认 |
-| **文件格式: ORC** | ✅ | ✅ | |
-| **文件格式: Avro** | ✅ | ✅ | |
-| **文件格式: Row** | ✅ | ✅ | Arrow IPC 格式 |
-| **文件格式: Vortex** | ❌ | ✅ (feature) | Rust 特有 |
-| **Stream Writer (持续写入)** | ✅ | ❌ | Flink 流式写入模式 |
-| **Tag/Branch** | ✅ | 部分 | Rust 有 Tag 读取器但无创建 API |
-| **提交重试与幂等** | ✅ | ✅ | commit_user + commitIdentifier |
-
----
-
-## 3. Java SDK vs Rust 差异矩阵
-
-### 3.1 架构差异
-
-| 维度 | Java SDK | Rust |
-|------|----------|------|
-| **数据载体** | `InternalRow` (自定义行格式) | Arrow `RecordBatch` (列式) |
-| **API 粒度** | 逐行 `write(InternalRow)` + 批量 `writeBundle()` | 批量
`write_arrow_batch(RecordBatch)` |
-| **CommitMessage** | 接口 + 序列化器(封装实现) | 具体结构体(所有字段公开) |
-| **I/O 模型** | 同步(阻塞 I/O) | 异步(tokio + opendal) |
-| **内存管理** | Java GC + 直接内存(spillable) | Rust 所有权系统 |
-| **压缩** | 内置 `CompactManager` | 未实现 |
-| **可扩展性** | 通过 SPI 插件 | 通过 feature flags 和 traits |
-
-### 3.2 性能差异
-
-| 维度 | Java SDK | Rust |
-|------|----------|------|
-| **写入吞吐** | 良好 | 优秀(零拷贝 Arrow,异步 I/O) |
-| **内存效率** | 依赖 GC,可能 FGC | 确定性的所有权管理 |
-| **启动时间** | JVM 预热开销 | 原生性能,无预热 |
-| **CPU 效率** | JIT 编译优化 | 编译期优化 |
-| **跨语言调用** | JNI 开销 | FFI 开销(更低) |
-
-### 3.3 功能完备度
-
-**Java SDK 功能完备度**:⭐⭐⭐⭐⭐(参考实现,100% 功能)
-**Rust 功能完备度**:⭐⭐⭐⭐(约 75%,核心缺失:Compaction, Lookup/FullCompaction
ChangelogProducer)
-
----
-
-## 4. Doris 现有写入框架分析
-
-### 4.1 BE 端写入框架
-
-Doris BE 端的 Table Writer 继承体系:
-
-```
-ResultWriter (result_writer.h)
- └── AsyncResultWriter (async_result_writer.h)
- ├── VTabletWriter (vtablet_writer.h) // 内部表写入
- ├── VHiveTableWriter (vhive_table_writer.h) // Hive 外部表
- ├── VJdbcTableWriter (vjdbc_table_writer.h) // JDBC 外部表
- ├── VTVFTableWriter (vtvf_table_writer.h) // TVF 表函数
- ├── VIcebergTableWriter // Iceberg 外部表
- └── VMaxComputeTableWriter // MaxCompute 外部表
-```
-
-**AsyncResultWriter** 的核心接口:
-```cpp
-class AsyncResultWriter : public ResultWriter {
-public:
- virtual Status open(RuntimeState* state, RuntimeProfile* profile) = 0;
- virtual Status write(RuntimeState* state, Block& input_block) = 0; // 实际
I/O
- Status sink(Block* block, bool eos); // 异步入队
- Status start_writer(RuntimeState* state, RuntimeProfile* profile); // 启动
I/O 线程
-};
-```
-
-**关键设计**:
-- `sink()` → 异步队列 → `write()`:解耦计算和 I/O
-- `Block` 是 Doris 的列式数据批次
-- 每个 writer 实例对应一个 BE worker 线程
-
-### 4.2 Iceberg Table Writer 参考
-
-`VIcebergTableWriter` 是最接近 Paimon 写入需求的外部表写入器:
-
-```
-写入流程:
- write(RuntimeState, Block)
- → 分区计算(PartitionTransform)
- → 确定目标 partition path
- → 获取/创建 VIcebergPartitionWriter(每个 partition 一个)
- → 写入 Parquet/ORC 文件
- → 文件滚动(按 target_file_size)
- close()
- → 关闭所有 partition writers
- → 收集产生的 DataFile 列表
- → 通过 Iceberg Catalog API 提交(AddFiles)
-```
-
-**关键差异(vs Paimon)**:
-
-Iceberg 和 Paimon 的写入模型存在根本性差异,这直接影响 Doris 的写入架构设计。
-
-**Iceberg:Commit-on-Close 模型**
-
-```
-BE Worker 1 BE Worker 2 FE Coordinator
- │ │ │
- │ write(block) × N │ write(block) × N │
- │ → 写入 Parquet 文件 │ → 写入 Parquet 文件 │
- │ → 文件滚动 │ → 文件滚动 │
- │ │ │
- │ close() │ close() │
- │ → 收集 DataFile 列表 │ → 收集 DataFile 列表 │
- │ → 生成 TIcebergCommitData │ → 生成 TIcebergCommitData │
- │ {file_path, row_count, │ {file_path, row_count, │
- │ file_size, partition} │ file_size, partition} │
- │ │ │
- │──── TIcebergCommitData ──────│──── TIcebergCommitData ──────→│
- │ │ │
- │ │ 聚合所有 DataFile 列表 │
- │ │ table.newAppend() │
- │ │ .appendFile(files...) │
- │ │ .commit() ← 原子提交 │
-```
-
-Iceberg writer 的输出就是**最终产物**:一堆已经写好的数据文件(Parquet/ORC)+ 文件的元信息(路径、行数、大小、分区值)。FE
Coordinator 收集这些文件列表后,直接调用 Iceberg 的 `appendFile()` + `commit()`
即可完成提交。文件列表是**透明的、可直接使用的**。
-
-**Paimon:Prepare-then-Commit 模型**
-
-```
-BE Worker 1 BE Worker 2 FE Coordinator
- │ │ │
- │ write(row) × N │ write(row) × N │
- │ → Paimon 内部: │ → Paimon 内部: │
- │ 分区/桶路由 │ 分区/桶路由 │
- │ KeyValueFileWriter │ AppendOnlyWriter │
- │ 排序、去重、序列号分配 │ 文件滚动 │
- │ 写入数据文件 + changelog │ │
- │ │ │
- │ prepareCommit() │ prepareCommit() │
- │ → CommitMessage[] │ → CommitMessage[] │
- │ { │ { │
- │ partition: bytes, │ partition: bytes, │
- │ bucket: 3, │ bucket: 0, │
- │ newFiles: [DataFileMeta │ newFiles: [DataFileMeta │
- │ {fileName, level, │ {fileName, level, │
- │ minSeq, maxSeq, │ minSeq, maxSeq, │
- │ minKey, maxKey, │ minKey, maxKey, │
- │ rowCount, ...}], │ rowCount, ...}], │
- │ changelogFiles: [...], │ changelogFiles: [...], │
- │ indexFiles: [...] │ indexFiles: [...] │
- │ } │ } │
- │ │ │
- │── CommitMessage[] (序列化) ──│── CommitMessage[] (序列化) ──→│
- │ │ │
- │ │ 聚合所有 CommitMessage │
- │ │ TableCommit.commit( │
- │ │ allMessages) │
- │ │ ├─ 合并 Manifest │
- │ │ ├─ 冲突检测 │
- │ │ ├─ 写入新 Manifest │
- │ │ └─ 原子创建 Snapshot │
-```
-
-Paimon writer 的输出是**中间产物**:`CommitMessage` 不是简单的"文件路径列表",而是一个包含内部元数据的结构化消息:
-
-- **`newFiles` vs `deletedFiles`**:同一个 CommitMessage 可能同时包含新增文件和要删除的文件(例如
Compaction 后:新文件是合并结果,旧文件标记删除)
-- **`minSequenceNumber` / `maxSequenceNumber`**:每个文件的序列号范围,Commit 引擎用它来检测冲突和保证
LSM-tree 的一致性
-- **`changelogFiles`**:如果启用了 changelog producer,会产生单独的 changelog 文件,需要在 Commit
时写入独立的 changelog manifest
-- **`indexFiles` / `deletedIndexFiles`**:动态桶模式下的哈希索引文件,需要合并到 IndexManifest
-- **`minKey` / `maxKey`**:用于 Manifest 的分区裁剪优化
-
-这些元数据**必须由 Paimon 的 Commit
引擎来消费和处理**,外部不能简单地"收集文件列表然后提交"。`TableCommit.commit()` 做的事情远比 Iceberg 的
`appendFile()` 复杂——它需要合并 manifest、检测冲突、分配 row ID、维护索引、写 changelog manifest 等。
-
-**对 Doris 架构的影响**:
-
-Doris 侧采用与 Iceberg 相同的单 Writer 架构——每次 `write()` 将完整 Block 交给 Paimon SDK,SDK
内部完成分区路由和文件写入。与 Iceberg 的关键区别在于 Commit:
-
-| 维度 | Iceberg 模式 | Paimon 模式 |
-|------|-------------|-------------|
-| Doris Writer 架构 | 单一 `AsyncResultWriter` + 内部多 partition writer | 单一
`AsyncResultWriter` + 单一 `IPaimonWriter` |
-| Writer 输出 | DataFile 列表(透明) | CommitMessage(不透明内部结构) |
-| Commit 执行者 | FE 直接调 Iceberg API | 需要通过 Paimon `TableCommit` 执行 |
-| Commit 的关键输入 | 文件路径 + 行数 + 分区 | 完整 CommitMessage(含序列号、键范围、索引文件等) |
-| 提交失败处理 | 重试即可(文件已存在) | 需 abort() 清理数据文件,然后重试 |
-| 跨 BE 聚合 | 简单合并文件列表 | 相同 (partition,bucket) 的 messages 需保持原样传递 |
-| FE 需要理解的内容 | 文件元信息 | 无需理解(透传),但不能丢失任何字段 |
-| 分区/桶路由 | Writer 内部计算 Iceberg partition value | Paimon SDK 内部计算,Doris 不感知 |
-
-**为什么 Paimon 不能像 Iceberg 一样 Commit-on-Close?**
-
-根本原因是 Paimon 的 LSM-tree 架构。Paimon 的表不是简单的"文件集合",而是一个分层的有序结构:
-
-1. **序列号(Sequence Number)**:LSM-tree 依赖全局递增的序列号来决定记录的可见性和冲突解决。每个 writer
写入时分配序列号,但 Commit 时 Commit 引擎需要全局地验证序列号的连续性,合并增量到 base manifest。
-2. **Compaction**:Writer 在 `prepareCommit()` 时可能已经执行了
Compaction,产生了新旧文件的替换关系(`compactBefore` / `compactAfter`),这些必须在 Commit 时原子地反映到
manifest 中。如果多个 writer 各自独立 commit,会导致 manifest 冲突。
-3. **Manifest 的版本一致性**:Paimon 的 Snapshot 是一个完整的 manifest 树(base manifest list
+ delta manifest list + changelog manifest list + index
manifest)。这棵树必须在**一个原子操作**中创建,不能由多个 writer 各自添加叶子节点。
-
-因此,Paimon 的 Commit **必须是一个集中的、原子的操作**——这就是 "Prepare-then-Commit" 的含义:Writer
负责准备(产生 CommitMessage),Coordinator 负责提交(消费 CommitMessage 创建 Snapshot)。
-
-### 4.3 FE 端规划
-
-```
-BaseExternalTableDataSink
- └── IcebergTableSink // 生成 TIcebergTableSink (Thrift)
- // 包含:table location, schema, partition spec, file format, output
expressions
-
-DataSink (Thrift)
- ├── TIcebergTableSink
- ├── THiveTableSink
- ├── TMaxComputeTableSink
- └── TPaimonTableSink ← 需要新增
-```
-
-### 4.4 现有 JNI 框架
-
-Doris 已有 JNI 调用框架用于**读取**外部表:
-
-```
-be/src/format_v2/jni/
- ├── jni_table_reader.{h,cpp} // JNI 读取器框架
- ├── paimon_jni_reader.{h,cpp} // Paimon JNI 读取器
- ├── hudi_jni_reader.{h,cpp} // Hudi JNI 读取器
- └── ...
-
-be/src/format/jni/
- ├── jni_data_bridge.{h,cpp} // C++ ↔ Java 数据桥接
- └── jni_reader.{h,cpp}
-```
-
-**JNI 模式的写路径参考**:目前没有 JNI 写路径(只有读取),需要新建。
-
----
-
-## 5. 架构设计
-
-### 5.1 总体架构
-
-核心设计:**一个 `VPaimonTableWriter` = 一个 `AsyncResultWriter` 实例 = 一个 IO 线程**(对标
Iceberg 的 `VIcebergTableWriter`)。分区路由和桶路由由 **Paimon SDK 内部完成**,Doris
侧不参与路由计算,直接将完整 Block 交给 Paimon SDK 写入。
-
-```
-┌─────────────────────────────────────────────────────────────────────┐
-│ Doris FE │
-│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │
-│ │ Nereids │ │ PaimonTableSink │ │ Thrift: │ │
-│ │ Planner │──▶│ (FE Planner) │──▶│ TPaimonTableSink │ │
-│ └──────────────┘ └──────────────────┘ └─────────┬───────────┘ │
-│ │ │
-│ ★ 无特殊 Shuffle:正常 Exchange 分发数据到各 BE │ │
-│ 分区/桶路由由 Paimon SDK 在写入时内部处理 │ │
-└───────────────────────────────────────────────────────┼─────────────┘
- │ TDataSink
- ▼
-┌───────────────────────────────────────────────────────┴─────────────┐
-│ Doris BE │
-│ │
-│ ┌──────────────────────────────────────────────────────────────┐ │
-│ │ Pipeline Operator: PaimonTableSinkOperatorX │ │
-│ │ │ │
-│ │ sink_impl(block, eos): │ │
-│ │ 直接调用 AsyncWriterSink::sink(block, eos) │ │
-│ │ ★ 无分区/桶路由逻辑,不做 Block 拆分 │ │
-│ └──────────────────────┬───────────────────────────────────────┘ │
-│ │ │
-│ ★ 仅一个 AsyncWriterSink<VPaimonTableWriter> │
-│ │ │
-│ ▼ │
-│ ┌────────────────────────────────────────────────────────────┐ │
-│ │ VPaimonTableWriter (AsyncResultWriter, 单实例) │ │
-│ │ │ │
-│ │ sink(block) → 入队列 (_data_queue, QUEUE_SIZE=3) │ │
-│ │ │ │
-│ │ process_block() ← 单一 IO 线程 │ │
-│ │ write(block): │ │
-│ │ Block → Arrow IPC → IPaimonWriter::write() │ │
-│ │ → Paimon SDK 内部完成: │ │
-│ │ · 分区计算 (BinaryRow partition) │ │
-│ │ · 桶计算 (hash % numBuckets) │ │
-│ │ · 按 (partition, bucket) 路由 │ │
-│ │ · 文件写入 (AppendOnlyWriter / KeyValueFileWriter) │ │
-│ │ · Compaction 自动触发 │ │
-│ │ │ │
-│ │ close(): │ │
-│ │ prepareCommit() → CommitMessage[] │ │
-│ └────────────────────────────────────────────────────────────┘ │
-│ │
-│ ┌──────────────────────────────────────────────────────────────┐ │
-│ │ IPaimonWriteBackend (抽象层) │ │
-│ │ │ │
-│ │ + createWriter() → IPaimonWriter (整个表共享一个 writer) │ │
-│ │ + createCommitter() → IPaimonCommitter │ │
-│ │ │ │
-│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
-│ │ │ JniPaimonWriteBackend│ │ FfiPaimonWriteBackend │ │ │
-│ │ │ (Java SDK via JNI) │ │ (Rust SDK via C FFI) │ │ │
-│ │ └──────────────────────┘ └──────────────────────┘ │ │
-│ └──────────────────────────────────────────────────────────────┘ │
-│ │
-│ ┌──────────────────────────────────────────────────────────────┐ │
-│ │ CommitMessage (统一格式,跨后端) │ │
-│ │ 单 writer produce → RuntimeState 汇总 → RPC to FE 统一提交 │ │
-│ └──────────────────────────────────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────────────┘
-```
-
-### 5.2 核心设计决策
-
-#### 决策 1:统一 CommitMessage 格式
-
-**设计**:在 BE 层定义一个 C++ 版本的 `CommitMessage` 结构体,同时能被 JNI 后端(Java 序列化反序列化)和 FFI
后端(Rust 结构体)填充。
-
-```cpp
-// be/src/exec/sink/writer/paimon/paimon_commit_message.h
-struct PaimonDataFileMeta {
- std::string file_name;
- int64_t file_size;
- int64_t row_count;
- int64_t delete_row_count;
- int64_t min_sequence_number;
- int64_t max_sequence_number;
- int32_t level;
- int32_t schema_id;
- std::string min_key; // serialized BinaryRow
- std::string max_key;
- // ... 更多元数据字段
-};
-
-struct PaimonCommitMessage {
- std::string partition; // serialized BinaryRow bytes
- int32_t bucket;
- std::optional<int32_t> total_buckets;
-
- std::vector<PaimonDataFileMeta> new_files;
- std::vector<PaimonDataFileMeta> new_changelog_files;
- std::vector<PaimonIndexFileMeta> new_index_files;
- std::vector<PaimonIndexFileMeta> deleted_index_files;
- std::vector<PaimonDataFileMeta> deleted_files;
- std::optional<int64_t> check_from_snapshot;
-
- // 序列化为 Thrift 格式,传递给 FE Coordinator
- void to_thrift(TPaimonCommitMessage* t_msg) const;
-};
-```
-
-**理由**:
-- 解耦写入后端和提交逻辑
-- FE Coordinator 需要聚合所有 BE 的 CommitMessage,因此需要一个统一的序列化格式
-- 使用 Thrift(Doris 的标准 RPC 序列化)来传输 CommitMessage 到 FE
-
-#### 决策 2:IPaimonWriteBackend 抽象
-
-**设计**:定义一个 C++ 抽象接口,屏蔽 JNI 和 FFI 的差异。
-
-```cpp
-// be/src/exec/sink/writer/paimon/paimon_write_backend.h
-class IPaimonWriter {
-public:
- virtual ~IPaimonWriter() = default;
- // 写入一个 Block(列式批次)
- virtual Status write(RuntimeState* state, Block& block) = 0;
- // 关闭当前 writer,产生 CommitMessage
- virtual Status prepareCommit(std::vector<PaimonCommitMessage>& messages) =
0;
- // 强制 compaction(可选,仅 Java 后端支持)
- virtual Status compact(bool fullCompaction) = 0;
- // 丢弃当前写入
- virtual Status abort() = 0;
-};
-
-class IPaimonCommitter {
-public:
- virtual ~IPaimonCommitter() = default;
- // 提交所有 messages
- virtual Status commit(const std::vector<PaimonCommitMessage>& messages) =
0;
- // 分区覆盖提交
- virtual Status overwrite(const std::vector<PaimonCommitMessage>& messages,
- const std::optional<std::map<std::string,
std::string>>& staticPartition) = 0;
- // 清空表
- virtual Status truncateTable() = 0;
- // 清空分区
- virtual Status truncatePartitions(const std::vector<std::map<std::string,
std::string>>& partitions) = 0;
- // 中止(删除数据文件)
- virtual Status abort(const std::vector<PaimonCommitMessage>& messages) = 0;
-};
-
-class IPaimonWriteBackend {
-public:
- virtual ~IPaimonWriteBackend() = default;
-
- // 初始化
- virtual Status open(const TPaimonTableSink& sink) = 0;
-
- // 为特定 partition+bucket 创建 writer
- virtual Status createWriter(const std::string& partition,
- int32_t bucket,
- std::unique_ptr<IPaimonWriter>* writer) = 0;
-
- // 获取 committer(整个 sink 共享一个 committer)
- virtual Status createCommitter(std::unique_ptr<IPaimonCommitter>*
committer) = 0;
-
- // 后端类型标识
- virtual PaimonBackendType type() const = 0;
-
- // 功能查询
- virtual bool supportsCompaction() const = 0;
- virtual bool supportsLookupChangelog() const = 0;
- virtual bool supportsFullCompactionChangelog() const = 0;
-};
-```
-
-#### 决策 3:双后端,可互相替代
-
-Java writer 和 Rust writer 是**互相替代**的关系,不是互补关系。Rust 是 Java 的功能子集:
-
-```
-功能全集 (Java SDK)
-┌─────────────────────────────────────────┐
-│ ┌─────────────────────────────────┐ │
-│ │ 功能子集 (Rust) │ │
-│ │ · Append-only 写入 │ │
-│ │ · 固定桶 PK 写入 │ │
-│ │ · 动态桶 PK 写入 │ │
-│ │ · MergeEngine: Normal/ │ │
-│ │ PartialUpdate/Aggregation/ │ │
-│ │ FirstRow/Deduplicate │ │
-│ │ · ChangelogProducer: None/Input│ │
-│ │ · Overwrite/Truncate │ │
-│ └─────────────────────────────────┘ │
-│ Rust 缺失(需回退 Java): │
-│ · Compaction │
-│ · Lookup/FullCompaction Changelog │
-│ · DeletionVector + PartialUpdate/ │
-│ Aggregation 组合 │
-│ · MERGE INTO / DataEvolution │
-└─────────────────────────────────────────┘
-```
-
-**选择策略:优先 Rust,功能不足时回退 Java**
-
-```
-IF table options 包含 Rust 不支持的功能:
- → Java JNI Backend ← 功能完备保证
-ELSE:
- → Rust FFI Backend ← 优先选择,性能更优
-```
-
-**v1 实现策略**:
-
-```
-v1: 仅实现 Java JNI Backend,但:
- - IPaimonWriteBackend 接口从第一天就存在
- - VPaimonTableWriter 通过 IPaimonWriteBackend 调用
- - Java 后端是实现细节,不是架构主体
- - 新增 Rust 后端 = 新增一个 IPaimonWriteBackend 实现类,不碰上层代码
-
-v2: 实现 Rust FFI Backend
- - 扩展 paimon-rust C binding
- - 新增 FfiPaimonWriteBackend(<1000行新代码)
- - 上层 VPaimonTableWriter 无需修改
-```
-
-| 场景 | v1 (仅 Java) | v2 (Java + Rust) |
-|------|-------------|-------------------|
-| Append-only 表 | JNI Backend | **FFI Backend** (优先) |
-| PK 表,固定桶,No Compaction | JNI Backend | **FFI Backend** (优先) |
-| PK 表,需要 Compaction | JNI Backend | JNI Backend (Rust 不支持) |
-| Lookup Changelog Producer | JNI Backend | JNI Backend (Rust 不支持) |
-| MERGE INTO | JNI Backend | JNI Backend (Rust 不支持) |
-
-#### 决策 4:Single Writer — 对标 Iceberg 的单 Writer 架构
-
-**问题**:v0 (初始实现) 采用了 "一个 `(partition, bucket)` = 一个 `AsyncResultWriter`"
的设计。每个 `AsyncResultWriter` 在 `fragment_mgr` 线程池中创建一个独立的 IO 线程。当 Paimon 表的
bucket 数量较大(例如 128 个 bucket × 10 个分区 = 1280 个 writer 实例),会导致线程数爆炸,严重浪费系统资源。
-
-**根因分析**:`AsyncResultWriter::start_writer()` 调用
`fragment_mgr()->get_thread_pool()->submit_func()` 为每个实例提交一个 long-running 的
`process_block()` 任务。这个任务在 writer 的整个生命周期中持续占用一个线程。当 writer 数量远超 CPU
核数时,大量线程空转等待数据,造成无效的上下文切换和内存开销。
-
-**解决方案**:改为 **一个 `VPaimonTableWriter` = 一个 `AsyncResultWriter` 实例 = 一个 IO
线程**,对标 Iceberg 的 `VIcebergTableWriter`。分区路由和桶路由完全交给 Paimon SDK 内部处理。
-
-Doris 侧不再:
-- 在 BE 端计算 Paimon partition bytes
-- 在 BE 端计算 bucket id
-- 在 Pipeline Operator 层做 Block 拆分和路由
-- 为每个 (partition, bucket) 创建独立的 writer
-
-Doris 侧只负责:
-- 将完整 Block 通过 `IPaimonWriter::write()` 交给 Paimon SDK
-- Paimon SDK 内部完成分区/桶路由、文件写入、Compaction
-
-```
-Doris Side Paimon SDK Side
-═══════════ ════════════════
-
-Block (完整批次) TableWrite.write(row × N)
- │ │
- ├── Arrow IPC 转换 ├── getPartition(row)
- │ ↓ ├── getBucket(row)
- ├── JNI/FFI 传递 ├── writeBundle(p, b, records)
- │ ↓ │ → AppendOnlyWriter
- └── IPaimonWriter::write() │ → KeyValueFileWriter
- │ → CompactManager
- │
- └── prepareCommit()
- → CommitMessage[]
-```
-
-**参考**:Iceberg 的 `VIcebergTableWriter` 采用相同架构(单一 `AsyncResultWriter` + 内部管理多个
`VIcebergPartitionWriter`),已在生产环境验证。
-
-**架构优势**:
-1. **线程数可控**:每个 BE worker 仅 1 个 IO 线程,不受 bucket/partition 数量影响
-2. **简化 Doris 侧代码**:移除 `paimon_bucket.h/cpp`、移除 operator 层的路由/拆分逻辑
-3. **复用 Paimon SDK 成熟逻辑**:分区/桶计算与 Paimon 原生语义完全一致,避免兼容性问题
-4. **减少内存开销**:不再需要为每个 (p,b) 维护独立的 AsyncResultWriter 队列和 block pool
-5. **简化 FE 端**:无需特殊的 shuffle key,正常 Exchange 即可
-
-**线程数对比**:
-
-| 场景 | v0 (per-bucket writer) | v1 (single writer) |
-|------|----------------------|---------------------|
-| 1 分区 × 128 bucket | 128 个 IO 线程 | 1 个 IO 线程 |
-| 10 分区 × 64 bucket | 640 个 IO 线程 | 1 个 IO 线程 |
-| Unaware bucket | 1 个 IO 线程 | 1 个 IO 线程 |
-
-**潜在风险**:
-- 单线程写入可能成为吞吐瓶颈 → Paimon SDK 内部使用异步 I/O (Rust) 或线程池 (Java),单线程调用非瓶颈
-- 如需更高并发,可在 FE 层增加 `parallelism` 参数,创建多个 BE fragment 实例(每个持有独立 writer),这是标准的
Doris 水平扩展方式
-
-#### 决策 5:Bucket Affinity — 不再由 Doris 保证
-
-**背景变化**:由于分区/桶路由已移交 Paimon SDK 内部处理,Doris 不再需要感知 bucket affinity。
-
-多个 BE worker 可能会写入同一个 (partition, bucket),每个 worker 的 Paimon TableWrite
实例独立产生文件。这与 Paimon Flink connector 的默认行为一致——多个 Subtask 各自写各自的文件,最终由 Commit 引擎合并
Manifest。
-
-**文件数量控制**:由 Paimon SDK 内部处理:
-- `target-file-size` 控制单个文件大小
-- Compaction 自动合并小文件
-- 对于 Append-only 表,可通过 `write-only` 模式 + `compaction` 任务定期合并
-
-**长期优化(可选)**:如需减少小文件,可后续在 FE Planner 层引入 shuffle by
hash(bucket_key),但不影响当前架构设计。
-
-### 5.3 提交流程
-
-Paimon 的两阶段提交对 Doris 的分布式架构提出了特殊挑战:
-
-```
-时间线:分布式写入 + 集中式提交
-
-BE1: write() → prepareCommit() → CommitMessage[]
-BE2: write() → prepareCommit() → CommitMessage[]
-BE3: write() → prepareCommit() → CommitMessage[]
- │ │ │
- └───────────────┼────────────────────┘
- │ (通过 Thrift RPC 发送到 FE Coordinator)
- ▼
-FE Coordinator: 聚合所有 CommitMessage[]
- │
- ▼
- TableCommit.commit(allMessages)
- │
- ▼
- 新的 Snapshot 创建
-```
-
-**关键问题**:Commit 应该在哪发生?
-
-**选择 1:FE Coordinator 执行 Commit**
-- 优点:自然的两阶段提交,FE 已有 Iceberg 的集中式提交模式
-- 缺点:FE 需要持有 Java SDK 依赖(或通过 BE 代理)
-
-**选择 2:选出一个 BE 执行 Commit(Leader BE)**
-- 优点:BE 端直接调用 JNI/FFI
-- 缺点:需要选举逻辑,增加复杂度
-
-**选择 3:混合方案** ✅ **推荐**
-
-```
-BE 端:
- - 数据写入(全并行)
- - prepareCommit() → CommitMessage[](序列化为 Thrift)
-
-FE Coordinator 端:
- - 聚合所有 BE 的 CommitMessage[]
- - 通过专用的 "Commit BE" 执行 TableCommit.commit()
-
-Commit BE(可以是任意 BE 或专用 BE):
- - 接收 FE 发来的聚合后的 CommitMessage[]
- - 调用 JNI Backend 的 TableCommit.commit()
- - 返回结果给 FE
-```
-
-对于 Append-only 表(简单场景),可以直接在 FE 端通过 Java SDK 执行提交。对于需要 Compaction 的表(复杂场景),通过
Commit BE 执行。
-
-### 5.4 架构图总览(分场景)
-
-#### 场景 1:Append-only 表写入
-
-```
-Doris Sink
- │
- ▼
-BE: PaimonTableSinkOperatorX (Pipeline Operator)
- │ sink_impl(block, eos) → 直接调用 AsyncWriterSink::sink()
- │ ★ 无分区/桶路由,Block 原样传递
- │
- ▼
-VPaimonTableWriter (单一 AsyncResultWriter)
- │ process_block() → write(block):
- │ Block → Arrow IPC → IPaimonWriter::write()
- │
- ▼
-IPaimonWriter (Rust FFI preferred)
- │ Paimon SDK 内部:
- │ · 分区/桶计算
- │ · write_arrow_batch() → AppendOnlyWriter
- │ · 文件写入
- │
- ▼
-prepareCommit() → CommitMessage[]
- │
- ▼ (Thrift RPC)
-FE Coordinator → TableCommit.commit()
- ▼
-New Snapshot ✓
-```
-
-#### 场景 2:主键表写入
-
-```
-Doris Sink
- │
- ▼
-BE: PaimonTableSinkOperatorX (Pipeline Operator)
- │ sink_impl(block, eos) → 直接调用 AsyncWriterSink::sink()
- │
- ▼
-VPaimonTableWriter (单一 AsyncResultWriter)
- │ process_block() → write(block):
- │ Block → Arrow IPC → JNI → Java
- │
- ▼
-JniPaimonWriter (Java SDK)
- │ Paimon BatchTableWrite:
- │ · 分区/桶计算 + writeBundle(p,b,records)
- │ · KeyValueFileWriter (排序、去重、序列号分配)
- │ · CompactManager 自动 compaction
- │ · 写入数据文件 + changelog
- │
- ▼
-prepareCommit() → CommitMessage[]
- │
- ▼ (Thrift RPC)
-FE Coordinator → TableCommit.commit()
- ▼
-New Snapshot ✓
-```
-
----
-
-## 6. 接口定义
-
-### 6.1 Thrift 定义
-
-```thrift
-// gensrc/thrift/DataSinks.thrift
-
-enum TPaimonWriteBackendType {
- JNI = 0, // Java SDK via JNI
- FFI = 1, // Rust via C FFI
-}
-
-enum TPaimonWriteMode {
- APPEND = 0, // INSERT INTO
- OVERWRITE = 1, // INSERT OVERWRITE
- DYNAMIC_OVERWRITE = 2, // 动态分区覆盖
- MERGE_INTO = 3, // DataEvolution MERGE INTO
-}
-
-struct TPaimonTableSink {
- // 表标识
- 1: required string table_location; // Paimon 表路径
- 2: required string paimon_catalog_type; // filesystem / hive / rest
- 3: optional map<string, string> catalog_options;
- 4: optional map<string, string> table_options;
-
- // Schema
- 5: required string schema_json; // Paimon TableSchema JSON
- 6: required list<string> partition_keys;
- 7: required list<string> primary_keys;
- 8: required list<i32> primary_key_indices;
-
- // 写入配置
- 9: required TPaimonWriteMode write_mode;
- 10: optional map<string, string> static_partition; // overwrite 分区 spec
- 11: required TPaimonWriteBackendType backend_type;
-
- // 输出表达式(Doris Block → Paimon Row 映射)
- 12: required list<TExpr> output_exprs;
-
- // 文件配置
- 13: optional string file_format; // parquet / orc / avro
- 14: optional string file_compression; // zstd / snappy / lz4 / none
- 15: optional i64 target_file_size; // bytes
-
- // 提交配置
- 16: optional string commit_user;
- 17: optional i32 commit_max_retries;
- 18: optional i64 commit_timeout_ms;
-
- // Hadoop 配置(对于 Hive Metastore catalog)
- 19: optional map<string, string> hadoop_config;
-}
-
-struct TPaimonCommitMessage {
- 1: required binary partition; // serialized BinaryRow
- 2: required i32 bucket;
- 3: optional i32 total_buckets;
- 4: required list<TPaimonDataFileMeta> new_files;
- 5: optional list<TPaimonDataFileMeta> new_changelog_files;
- 6: optional list<TPaimonIndexFileMeta> new_index_files;
- 7: optional list<TPaimonIndexFileMeta> deleted_index_files;
- 8: optional list<TPaimonDataFileMeta> deleted_files;
- 9: optional i64 check_from_snapshot;
-}
-
-struct TPaimonDataFileMeta {
- 1: required string file_name;
- 2: required i64 file_size;
- 3: required i64 row_count;
- 4: optional i64 delete_row_count;
- 5: required i64 min_sequence_number;
- 6: required i64 max_sequence_number;
- 7: required i32 level;
- 8: required i32 schema_id;
- 9: required binary min_key;
- 10: required binary max_key;
- 11: optional binary min_partition_key;
- 12: optional binary max_partition_key;
- 13: optional list<string> write_cols;
- 14: optional i64 first_row_id;
- 15: optional i32 file_source;
- // ... 其他元数据
-}
-
-struct TPaimonIndexFileMeta {
- 1: required string file_name;
- 2: required i64 file_size;
- 3: required i64 row_count;
- 4: required string index_type; // "HASH" / "BTREE" / "GLOBAL"
- 5: optional TPaimonGlobalIndexMeta global_index_meta;
-}
-
-struct TPaimonCommitResult {
- 1: required bool success;
- 2: optional i64 new_snapshot_id;
- 3: optional string error_message;
-}
-
-// 添加到 TDataSink
-struct TDataSink {
- // ... existing fields ...
- 16: optional TPaimonTableSink paimon_table_sink
-}
-```
-
-### 6.2 C++ 抽象接口(完整版)
-
-```cpp
-// be/src/exec/sink/writer/paimon/paimon_write_backend.h
-
-namespace doris {
-
-// ──── 枚举 ────────────────────────────────────────────────
-
-enum class PaimonBackendType {
- JNI, // Java SDK
- FFI // Rust C FFI
-};
-
-enum class PaimonWriteMode {
- APPEND,
- OVERWRITE,
- DYNAMIC_OVERWRITE,
- MERGE_INTO
-};
-
-// ──── 数据结构 ────────────────────────────────────────────
-
-struct PaimonWriteOptions {
- std::string table_location;
- std::string commit_user;
- PaimonWriteMode write_mode;
- std::optional<std::unordered_map<std::string, std::string>>
static_partition;
- std::string file_format; // "parquet", "orc", "avro"
- std::string file_compression; // "zstd", "snappy", "lz4", "none"
- int64_t target_file_size_bytes = 128 * 1024 * 1024; // 128MB default
- int32_t commit_max_retries = 5;
- int64_t commit_timeout_ms = 300000; // 5min
-};
-
-// ──── IPaimonWriter ───────────────────────────────────────
-
-class IPaimonWriter {
-public:
- virtual ~IPaimonWriter() = default;
-
- /// 写入一个 Doris Block(完整批次,可能包含多分区、多桶的数据)
- /// Paimon SDK 内部完成:分区计算、桶计算、按 (partition,bucket) 路由、
- /// 文件写入(AppendOnlyWriter / KeyValueFileWriter)、Compaction 触发。
- ///
- /// @param state 运行时状态
- /// @param block 输入数据(Doris 列式格式,未按分区/桶拆分)
- virtual Status write(RuntimeState* state, Block& block) = 0;
-
- /// 准备提交:关闭所有内部 writer,生成 CommitMessage
- /// 对所有 (partition, bucket) 内部 writer 调用 prepareCommit()
- /// @param messages 输出:本次写入产生的所有 CommitMessage 列表
- virtual Status prepareCommit(
- std::vector<PaimonCommitMessage>& messages) = 0;
-
- /// 强制 compaction(仅部分后端支持)
- /// @param fullCompaction true=全量压缩, false=增量压缩
- virtual Status compact(bool fullCompaction) {
- return Status::NotSupported("compact not supported by this backend");
- }
-
- /// 丢弃本次写入的所有数据
- virtual Status abort() = 0;
-};
-
-// ──── IPaimonCommitter ────────────────────────────────────
-
-class IPaimonCommitter {
-public:
- virtual ~IPaimonCommitter() = default;
-
- /// 追加提交:将所有 CommitMessage 中的数据文件原子提交
- virtual Status commit(
- const std::vector<PaimonCommitMessage>& messages) = 0;
-
- /// 覆盖提交:删除旧数据,写入新数据
- virtual Status overwrite(
- const std::vector<PaimonCommitMessage>& messages,
- const std::optional<std::unordered_map<std::string, std::string>>&
- static_partition) = 0;
-
- /// 清空整张表
- virtual Status truncateTable() = 0;
-
- /// 清空指定分区
- virtual Status truncatePartitions(
- const std::vector<std::unordered_map<std::string, std::string>>&
- partitions) = 0;
-
- /// 中止:删除已写入但未提交的数据文件
- virtual Status abort(
- const std::vector<PaimonCommitMessage>& messages) = 0;
-};
-
-// ──── IPaimonWriteBackend ─────────────────────────────────
-
-class IPaimonWriteBackend {
-public:
- virtual ~IPaimonWriteBackend() = default;
-
- /// 初始化后端
- virtual Status open(const TPaimonTableSink& sink,
- RuntimeState* state) = 0;
-
- /// 为整个表创建一个 writer(管理所有 partition + bucket)
- /// 分区/桶路由由 Paimon SDK 在 write() 内部处理
- /// @param writer 输出:创建的 writer
- virtual Status createWriter(
- std::unique_ptr<IPaimonWriter>* writer) = 0;
-
- /// 创建一个 committer(每个 sink 只需要一个)
- virtual Status createCommitter(
- std::unique_ptr<IPaimonCommitter>* committer) = 0;
-
- /// 获取后端类型
- virtual PaimonBackendType type() const = 0;
-
- // ──── 功能查询 ────────────────────────────────────────
-
- virtual bool supportsCompaction() const = 0;
- virtual bool supportsLookupChangelogProducer() const = 0;
- virtual bool supportsFullCompactionChangelogProducer() const = 0;
- virtual bool supportsPartialUpdateWithDV() const = 0;
- virtual bool supportsAggregationWithDV() const = 0;
-};
-
-// ──── 后端工厂 ────────────────────────────────────────────
-
-class PaimonWriteBackendFactory {
-public:
- /// 根据配置创建合适的后端
- static Status create(
- const TPaimonTableSink& sink,
- std::unique_ptr<IPaimonWriteBackend>* backend);
-
- /// 根据表属性和用户偏好选择最佳后端类型
- static PaimonBackendType selectBackendType(
- const TPaimonTableSink& sink);
-};
-
-} // namespace doris
-```
-
----
-
-## 7. 数据流与交互时序
-
-### 7.1 完整写入流程(时序图)
-
-```
- FE Coordinator BE Worker 1 BE Worker 2 Commit BE
- │ │ │ │
- │ TDataSink │ │ │
- │ (TPaimonTableSink) │ │ │
- ├──────────────────────┤ │ │
- │ │ │ │
- │ open() open() │
- │ IPaimonWriteBackend IPaimonWriteBackend │
- │ → createWriter() → createWriter() │
- │ → IPaimonWriter → IPaimonWriter │
- │ │ │ │
- │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
- │ │ 写入阶段(并行) │ │
- │ │ │ │
- │ │ Block1[rows] │ │
- │ ├─────────────────────────────────────────┤ │
- │ │ write() → IPaimonWriter │ │
- │ │ → Paimon SDK 内部: │ │
- │ │ · 分区计算 + 桶计算 │ │
- │ │ · writeBundle(p,b,records) │ │
- │ │ · Compaction 自动触发 │ │
- │ │ │ │
- │ │ Block2[rows] │ │
- │ ├─────────────────────────────────────────┤ │
- │ │ write() → Paimon SDK │ │
- │ │ │ │
- │ │ ... (更多数据) ... │ │
- │ │ │ │
- │ │ EOS (End of Stream) │ │
- │ ├─────────────────────────────────────────┤ │
- │ │ prepareCommit() │ │
- │ │ → CommitMessage[] │ │
- │ │ (所有 partition+bucket 的消息) │ │
- │ │ │ │
- │ │ prepareCommit() │ │
- │ ├─────────────────────────────────────────┤ │
- │ │ → CommitMessage[] │ │
- │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
- │ │ │ │
- │ CommitMessage[] │ CommitMessage[] │ │
- │ (Thrift RPC) │ (Thrift RPC) │ │
- │◄─────────────────────┼─────────────────────┤ │
- │ │ │ │
- │ 聚合 & 去重 │ │ │
- │ allMessages[] │ │ │
- │ │ │ │
- │ │ commit(allMessages) │
- ├──────────────────────────────────────────────────────────────────┤
- │ │ │ TableCommit │
- │ │ │ .commit() │
- │ │ │ ├─ write manifests│
- │ │ │ ├─ write snapshot │
- │ │ │ └─ atomic rename │
- │ │ │ │
- │ CommitResult │ │ │
- │◄──────────────────────────────────────────────────────────────────┤
- │ │ │ │
- │ (on success) │ │ │
- │ close() writers │ │ │
- ├──────────────────────┤ │ │
- │ │ │ │
- │ (on failure) │ │ │
- │ abort(messages) │ │ │
- ├──────────────────────┼─────────────────────┤ │
-```
-
-### 7.2 错误处理流程
-
-```
-写入阶段可能的错误:
- 1. 网络/存储 I/O 错误
- → 重试(with exponential backoff)
- → 超过重试次数 → 标记 task 失败 → FE 重新调度
-
- 2. 内存不足
- → spill to disk (if spillable enabled)
- → 触发 memory limiter → 减少并发度
-
- 3. Schema mismatch
- → 立即失败,不回滚已写入的文件
- → 依赖 Snapshot 的隔离性,未提交的数据不会被读到
-
-提交阶段可能的错误:
- 1. Commit conflict(并发提交)
- → 自动重试(commit_max_retries times)
- → 检查 idempotency (commit_user + commit_identifier)
-
- 2. 提交超时
- → 检查是否已经提交成功(idempotency check)
- → 如果未成功 → 根据 Snapshot 状态决定重试还是放弃
-
- 3. 写入成功但提交失败 = 孤儿文件
- → 调用 abort(messages) 清理数据文件
- → 下次 Snapshot expiration 时也会清理未引用的文件
-```
-
----
-
-## 8. FE 端设计
-
-### 8.1 PaimonTableSink(FE Planner)
-
-```java
-// fe/fe-core/src/main/java/org/apache/doris/planner/PaimonTableSink.java
-
-public class PaimonTableSink extends BaseExternalTableDataSink {
-
- private final PaimonExternalTable targetTable;
- private List<Expr> outputExprs;
- private PaimonWriteMode writeMode;
- private Map<String, String> staticPartition;
-
- public PaimonTableSink(PaimonExternalTable targetTable,
InsertCommandContext ctx) {
- this.targetTable = targetTable;
- // 从 ctx 确定写入模式
- if (ctx.isOverwrite()) {
- this.writeMode = PaimonWriteMode.OVERWRITE;
- this.staticPartition = ctx.getStaticPartition();
- } else {
- this.writeMode = PaimonWriteMode.APPEND;
- }
- }
-
- @Override
- public TDataSink toThrift() {
- TDataSink result = new TDataSink(TDataSinkType.PAIMON_TABLE_SINK);
-
- TPaimonTableSink paimonSink = new TPaimonTableSink();
- paimonSink.setTableLocation(targetTable.getTableLocation());
- paimonSink.setPaimonCatalogType(targetTable.getCatalogType());
- paimonSink.setSchemaJson(targetTable.getPaimonSchemaJson());
- paimonSink.setPartitionKeys(targetTable.getPartitionKeys());
- paimonSink.setPrimaryKeys(targetTable.getPrimaryKeys());
- paimonSink.setPrimaryKeyIndices(targetTable.getPrimaryKeyIndices());
- paimonSink.setWriteMode(writeMode.toThrift());
- paimonSink.setOutputExprs(Expr.treesToThrift(outputExprs));
- paimonSink.setFileFormat(targetTable.getFileFormat());
- paimonSink.setFileCompression(targetTable.getFileCompression());
- paimonSink.setTargetFileSize(targetTable.getTargetFileSize());
-
- // 选择后端类型
- paimonSink.setBackendType(selectBackendType().toThrift());
-
- // 生成 commit_user
- paimonSink.setCommitUser(UUID.randomUUID().toString());
-
- result.setPaimonTableSink(paimonSink);
- return result;
- }
-
- /**
- * 智能选择写入后端:
- * - 如果需要 Compaction 或 Lookup/FullCompaction Changelog,必须用 JNI
- * - 如果用户指定了 preferred backend,遵循用户选择
- * - 默认:JNI(功能优先)
- */
- private TPaimonWriteBackendType selectBackendType() {
- // 检查表是否配置了需要 Java SDK 才支持的功能
- if (targetTable.requiresCompaction() ||
- targetTable.getChangelogProducer().requiresJava()) {
- return TPaimonWriteBackendType.JNI;
- }
-
- // 检查用户偏好
- String configuredBackend = targetTable.getTableProperties()
- .get("doris.paimon.write.backend");
- if ("ffi".equalsIgnoreCase(configuredBACKend) ||
- "rust".equalsIgnoreCase(configuredBACKend)) {
- return TPaimonWriteBackendType.FFI;
- }
-
- // 默认使用 JNI(功能最完整)
- return TPaimonWriteBackendType.JNI;
- }
-}
-```
-
-### 8.2 PaimonWriteCommitCoordinator(FE Coordinator)
-
-```java
-//
fe/fe-core/src/main/java/org/apache/doris/transaction/PaimonWriteCommitCoordinator.java
-
-public class PaimonWriteCommitCoordinator {
-
- /**
- * 协调分布式写入的提交:
- * 1. 收集所有 BE 的 CommitMessage[]
- * 2. 聚合去重
- * 3. 选择一个 BE 执行提交(或 FE 本地执行)
- * 4. 处理提交结果
- */
- public TPaimonCommitResult commit(
- Database db,
- PaimonExternalTable table,
- List<List<TPaimonCommitMessage>> allBackendMessages) {
-
- // Step 1: 扁平化所有 BE 的 messages
- List<TPaimonCommitMessage> allMessages = allBackendMessages.stream()
- .flatMap(List::stream)
- .collect(Collectors.toList());
-
- if (allMessages.isEmpty()) {
- return new TPaimonCommitResult(true); // nothing to commit
- }
-
- // Step 2: 聚合相同 (partition, bucket) 的 messages
- Map<Pair<byte[], Integer>, List<TPaimonCommitMessage>> grouped =
- groupByPartitionBucket(allMessages);
-
- // Step 3: 选择 Commit BE
- TNetworkAddress commitBE = selectCommitBE(allBackendMessages);
-
- // Step 4: 通过 RPC 调用 Commit BE 执行提交
- TPaimonCommitRequest request = new TPaimonCommitRequest();
- request.setTableLocation(table.getTableLocation());
- request.setCommitMessages(allMessages);
- request.setCommitUser(generateCommitUser());
-
- try {
- TPaimonCommitResult result = BackendServiceProxy.getInstance()
- .paimonCommit(commitBE, request);
- return result;
- } catch (Exception e) {
- // 提交失败处理
- return handleCommitFailure(allMessages, e);
- }
- }
-}
-```
-
----
-
-## 9. BE 端设计
-
-### 9.1 VPaimonTableWriter
-
-对标 Iceberg 的 `VIcebergTableWriter`:单一 `AsyncResultWriter` 实例,在 `write()` 中将完整
Block 交给 Paimon SDK 处理。分区/桶路由、文件写入、Compaction 全部由 Paimon SDK 内部完成。
-
-```cpp
-// be/src/exec/sink/writer/paimon/vpaimon_table_writer.h
-
-class VPaimonTableWriter final : public AsyncResultWriter {
-public:
- VPaimonTableWriter(const TDataSink& t_sink,
- const VExprContextSPtrs& output_exprs,
- std::shared_ptr<Dependency> dep,
- std::shared_ptr<Dependency> fin_dep);
-
- Status open(RuntimeState* state, RuntimeProfile* profile) override;
- Status write(RuntimeState* state, Block& block) override;
- Status close(Status status) override;
-
-private:
- TDataSink _t_sink;
- RuntimeState* _state = nullptr;
-
- // 抽象后端 — 整个 writer 持有一个 IPaimonWriteBackend 实例
- std::unique_ptr<IPaimonWriteBackend> _backend;
-
- // 单一 IPaimonWriter(管理所有 partition + bucket,对标 Iceberg 的
- // _partitions_to_writers map,但路由逻辑在 Paimon SDK 内部)
- std::unique_ptr<IPaimonWriter> _writer;
-
- // 统计
- int64_t _written_rows = 0;
- int64_t _written_bytes = 0;
-
- // Profile counters
- RuntimeProfile::Counter* _written_rows_counter = nullptr;
- RuntimeProfile::Counter* _written_bytes_counter = nullptr;
- RuntimeProfile::Counter* _send_data_timer = nullptr;
- RuntimeProfile::Counter* _project_timer = nullptr;
- RuntimeProfile::Counter* _arrow_convert_timer = nullptr;
- RuntimeProfile::Counter* _file_store_write_timer = nullptr;
- RuntimeProfile::Counter* _open_timer = nullptr;
- RuntimeProfile::Counter* _close_timer = nullptr;
- RuntimeProfile::Counter* _prepare_commit_timer = nullptr;
- RuntimeProfile::Counter* _serialize_commit_messages_timer = nullptr;
- RuntimeProfile::Counter* _commit_payload_count = nullptr;
- RuntimeProfile::Counter* _commit_payload_bytes_counter = nullptr;
-};
-```
-
-**`write()` 实现逻辑**(伪代码):
-
-```cpp
-Status VPaimonTableWriter::write(RuntimeState* state, Block& block) {
- if (block.rows() == 0) return Status::OK();
-
- SCOPED_TIMER(_send_data_timer);
-
- // 1. Project: 应用 output expressions
- Block output_block;
- {
- SCOPED_TIMER(_project_timer);
- RETURN_IF_ERROR(_projection_block(block, &output_block));
- }
-
- // 2. 写入 Paimon SDK(分区/桶路由由 SDK 内部处理)
- {
- SCOPED_TIMER(_file_store_write_timer);
- RETURN_IF_ERROR(_writer->write(state, output_block));
- }
-
- COUNTER_UPDATE(_written_rows_counter, block.rows());
- _written_rows += block.rows();
- return Status::OK();
-}
-```
-
-**`open()` 实现逻辑**:
-
-```cpp
-Status VPaimonTableWriter::open(RuntimeState* state, RuntimeProfile* profile) {
- _state = state;
- _operator_profile = profile;
-
- // 注册 counters
- // ...
-
- // 创建后端(JNI 或 FFI)
-
RETURN_IF_ERROR(PaimonWriteBackendFactory::create(_t_sink.paimon_table_sink,
&_backend));
- RETURN_IF_ERROR(_backend->open(_t_sink.paimon_table_sink, state));
-
- // 创建一个 writer(管理全部分区和桶)
- RETURN_IF_ERROR(_backend->createWriter(&_writer));
-
- return Status::OK();
-}
-```
-
-**`close()` 实现逻辑**:
-
-```cpp
-Status VPaimonTableWriter::close(Status status) {
- SCOPED_TIMER(_close_timer);
-
- std::vector<TPaimonCommitMessage> messages;
- if (status.ok() && _writer) {
- {
- SCOPED_TIMER(_prepare_commit_timer);
- // prepareCommit 对所有内部 (partition, bucket) writer 调用
- Status prep_st = _writer->prepare_commit(messages);
- if (!prep_st.ok()) status = prep_st;
- }
- if (status.ok() && !messages.empty()) {
- _state->add_paimon_commit_messages(messages);
- }
- }
-
- if (!status.ok() && _writer) {
- _writer->abort(); // 清理所有未提交的数据文件
- }
-
- _writer.reset();
- _backend.reset();
- return status;
-}
-```
-
-**与 Iceberg VIcebergTableWriter 的对应关系**:
-
-| 维度 | VIcebergTableWriter | VPaimonTableWriter (新) |
-|------|-------------------|------------------------|
-| AsyncResultWriter 个数 | 1 | 1 |
-| IO 线程数 | 1 | 1 |
-| 分区路由位置 | `write()` 内部 `_write_prepared_block()` | `IPaimonWriter::write()`
内部(SDK) |
-| 多分区管理 | `_partitions_to_writers` map (C++ 侧) | Paimon SDK 内部 map |
-| 文件轮转 | 按 `target_file_size_bytes` | Paimon SDK 内部处理 |
-| close 行为 | 遍历关闭所有 partition writer | `prepareCommit()` 一次性收拢所有消息 |
-
-Pipeline Operator 层简化为最简单的透传,不再做路由:
-
-```cpp
-// PaimonTableSinkOperatorX 不再需要 _get_or_create_writer()
-// 不再需要 _compute_routing()
-// 不再需要 _split_by_key()
-// 不再需要 _writers map
-// 不再需要 _bucket_key_indices / _partition_col_indices / _total_buckets
-
-// **sink_impl() 简化为:**
-// 直接调用 AsyncWriterSink::sink() → VPaimonTableWriter::sink() → 入队列
-```
-
-### 9.1.1 移除的 BE 端代码
-
-以下文件和函数在本次架构调整中**不再需要**,将由后续 PR 清理:
-
-| 文件/函数 | 说明 |
-|----------|------|
-| `be/src/exec/sink/writer/paimon/paimon_bucket.h` | BE 端 MurmurHash 桶计算 |
-| `be/src/exec/sink/writer/paimon/paimon_bucket.cpp` | 同上 |
-| `PaimonTableSinkOperatorX::_compute_routing()` | Pipeline 层路由计算 |
-| `PaimonTableSinkOperatorX::_split_by_key()` | Block 按 (p,b) 拆分 |
-| `PaimonTableSinkOperatorX::_get_or_create_writer()` | 懒创建 per-(p,b) writer |
-| `PaimonTableSinkOperatorX::_writers` map | per-(p,b) writer 注册表 |
-| `PaimonTableSinkOperatorX::_bucket_key_indices` | bucket key 列索引 |
-| `PaimonTableSinkOperatorX::_partition_col_indices` | partition 列索引 |
-| `PaimonTableSinkOperatorX::_total_buckets` | 总桶数配置 |
-| `PaimonTableSinkOperatorX::_routing_enabled` | 路由开关 |
-| `PaimonTableSinkOperatorX::_eos_sent` | 多 writer EOS 跟踪 |
-
-### 9.2 JNI Backend 实现
-
-```cpp
-// be/src/exec/sink/writer/paimon/jni_paimon_write_backend.h
-
-class JniPaimonWriteBackend : public IPaimonWriteBackend {
-public:
- Status open(const TPaimonTableSink& sink, RuntimeState* state) override;
-
- // 创建 writer——管理所有 partition+bucket,SDK 内部路由
- Status createWriter(std::unique_ptr<IPaimonWriter>* writer) override;
-
- Status createCommitter(
- std::unique_ptr<IPaimonCommitter>* committer) override;
-
- PaimonBackendType type() const override { return PaimonBackendType::JNI; }
-
- bool supportsCompaction() const override { return true; }
- bool supportsLookupChangelogProducer() const override { return true; }
- bool supportsFullCompactionChangelogProducer() const override { return
true; }
- bool supportsPartialUpdateWithDV() const override { return true; }
- bool supportsAggregationWithDV() const override { return true; }
-
-private:
- // JNI 全局引用
- jobject _write_builder; // org.apache.paimon.table.sink.BatchWriteBuilder
- jobject _jni_env_context; // JNI 执行环境
-
- // 桥接方法
- Status _callJavaWrite(jobject writer, Block& block);
- Status _callJavaPrepareCommit(jobject writer,
- std::vector<PaimonCommitMessage>& messages);
- Status _serializeCommitMessages(jobject javaMessages,
- std::vector<PaimonCommitMessage>&
cppMessages);
-};
-
-// JniPaimonWriter 内部持有单个 Java BatchTableWrite 实例,
-// 每次 write() 将 Block 逐行或批量传递给 Java SDK,
-// SDK 内部完成分区/桶路由和文件写入。
-class JniPaimonWriter : public IPaimonWriter {
-public:
- Status write(RuntimeState* state, Block& block) override;
- Status prepareCommit(std::vector<PaimonCommitMessage>& messages) override;
- Status compact(bool fullCompaction) override;
- Status abort() override;
-
-private:
- jobject _table_write; // org.apache.paimon.table.sink.BatchTableWrite
- std::unique_ptr<JniPaimonWriteBackend> _backend; // 非拥有引用,仅用于 JNI 调用
-};
-```
-```
-
-### 9.3 FFI (Rust) Backend 实现
-
-```cpp
-// be/src/exec/sink/writer/paimon/ffi_paimon_write_backend.h
-
-class FfiPaimonWriteBackend : public IPaimonWriteBackend {
-public:
- Status open(const TPaimonTableSink& sink, RuntimeState* state) override;
-
- Status createWriter(std::unique_ptr<IPaimonWriter>* writer) override;
-
- Status createCommitter(
- std::unique_ptr<IPaimonCommitter>* committer) override;
-
- PaimonBackendType type() const override { return PaimonBackendType::FFI; }
-
- // 功能查询 — Rust 目前的能力边界
- bool supportsCompaction() const override { return false; }
- bool supportsLookupChangelogProducer() const override { return false; }
- bool supportsFullCompactionChangelogProducer() const override { return
false; }
- bool supportsPartialUpdateWithDV() const override { return false; }
- bool supportsAggregationWithDV() const override { return false; }
-
-private:
- // 动态加载的 Rust 共享库
- void* _lib_handle;
-
- // C FFI 函数指针
- // 通过 paimon-rust bindings/c 提供
- using PaimonWriteBuilderNew = void* (*)(/* args */);
- using PaimonTableWriteNew = void* (*)(void* builder);
- using PaimonTableWriteArrowBatch = int32_t (*)(void* writer,
- FFI_ArrowArray* array, FFI_ArrowSchema* schema);
- using PaimonTableWritePrepareCommit = void* (*)(void* writer); // returns
CommitMessage[]
- using PaimonTableCommitNew = void* (*)(void* builder);
- using PaimonTableCommitCommit = int32_t (*)(void* commit, void* messages);
- // ... 等等
-
- PaimonWriteBuilderNew _ffi_write_builder_new;
- PaimonTableWriteNew _ffi_table_write_new;
- PaimonTableWriteArrowBatch _ffi_write_arrow_batch;
- PaimonTableWritePrepareCommit _ffi_prepare_commit;
- PaimonTableCommitNew _ffi_table_commit_new;
- PaimonTableCommitCommit _ffi_commit;
-};
-```
-
-**注意**:`paimon-rust/bindings/c/` 目前只暴露了**读取** API。写入 API 需要新增。这是实施路线图中的第一步。
-
----
-
-## 10. 功能路由矩阵
-
-### 10.1 写入模式路由
-
-原则:**优先 Rust,功能不足回退 Java**。Rust 是 Java 的功能子集。
-
-| 表类型 | v1 (仅Java) | v2 首选 | v2 回退 | 回退原因 |
-|--------|------------|---------|---------|----------|
-| Append-only(任意分区) | JNI | **FFI** | JNI | Rust 覆盖最充分 |
-| Append-only + Blob 列 | JNI | **FFI** | JNI | Rust 支持 |
-| PK 表,固定桶 | JNI | **FFI** | JNI | KeyValueFileWriter |
-| PK 表,动态桶 | JNI | **FFI** | JNI | DynamicBucketAssigner |
-| PK 表,Postpone 桶 | JNI | **FFI** | JNI | PostponeFileWriter |
-| ChangelogProducer=Input | JNI | **FFI** | JNI | Rust 支持 |
-| INSERT OVERWRITE | JNI | **FFI** | JNI | Rust 支持 |
-| Truncate table/partition | JNI | **FFI** | JNI | Rust 支持 |
-| ChangelogProducer=Lookup | JNI | JNI | - | Rust **不支持** |
-| ChangelogProducer=FullCompaction | JNI | JNI | - | Rust **不支持** |
-| PartialUpdate + DeletionVector | JNI | JNI | - | Rust **不支持** |
-| Aggregation + DeletionVector | JNI | JNI | - | Rust **不支持** |
-| MERGE INTO (有 PK) | JNI | JNI | - | Rust **不支持** |
-| MERGE INTO (无 PK) | JNI | JNI | - | Rust **不支持** |
-| 需要 Compaction | JNI | JNI | - | Rust **不支持** |
-
-**v2 回退规则**:一旦表的 options 中包含任何 Rust 不支持的功能,整个写入退回到 Java Backend。
-
-### 10.2 功能对齐路线
-
-```
-Phase 1 (v1) — Java JNI Backend + 抽象层:
- ✅ 建立 IPaimonWriteBackend / IPaimonWriter / IPaimonCommitter 接口
- ✅ JniPaimonWriteBackend(完整实现)
- ✅ FfiPaimonWriteBackend(stub,所有方法返回 NotSupported)
- ✅ 结构化 Thrift CommitMessage
- ✅ 单 Writer 架构(对标 Iceberg),Paimon SDK 内部路由
- ✅ Append-only + 固定桶 PK 写入
- ✅ INSERT OVERWRITE / Truncate
- ✅ ChangelogProducer: None / Input
- ✅ 所有 MergeEngine
- ⬜ Compaction(Java 后端)
- ⬜ ChangelogProducer: Lookup / FullCompaction(Java 后端)
-
-Phase 2 (v2) — Rust FFI Backend:
- ⬜ 扩展 paimon-rust C binding(写入 API)
- ⬜ FfiPaimonWriteBackend 从 stub 变为完整实现
- ⬜ 零拷贝 Arrow C Data Interface
- ⬜ Rust tokio runtime 集成
- ⬜ PaimonWriteBackendFactory 自动后端选择
- ⬜ 上层代码零改动(VPaimonTableWriter 不变)
-
-Phase 3(高级功能补齐):
- ⬜ Compaction 触发与执行(Java 后端)
- ⬜ Lookup / FullCompaction Changelog Producer(Java 后端)
- ⬜ MERGE INTO / DataEvolution(Java 后端)
- ⬜ 动态桶 + 跨分区桶
-
-Phase 4(Rust 功能追赶):
- ⬜ Rust Compaction 实现
- ⬜ Rust Lookup Changelog Producer
- ⬜ Rust DV + PartialUpdate/Aggregation
-```
-
----
-
-## 11. 实施路线图
-
-### Phase 1:v1 — Java JNI Backend + 预设计 Rust 接口(约 6-8 周)
-
-**目标**:基于 Java SDK 实现功能完备的 Paimon 写入,同时建立 `IPaimonWriteBackend` 抽象为 Rust
后端预留空间。
-
-**核心原则**:虽然只实现 Java 后端,但**架构代码从第一天起就通过抽象接口调用**。新增 Rust 后端 = 新增一个
`IPaimonWriteBackend` 的实现类(<1000行),不动上层代码。
-
-1. **Thrift 定义**
- - 新增 `TPaimonTableSink`(含 `backend_type` 字段,v1 固定为 JNI)
- - 新增结构化 `TPaimonCommitMessage`(含完整 DataFileMeta、IndexFileMeta)
- - 新增 `TPaimonCommitResult`
- - 在 `TDataSink` 中添加 `paimon_table_sink` 字段
-
-2. **BE 端 — 抽象层(关键)**
- - **`IPaimonWriteBackend`** 接口:`open()` / `createWriter()` /
`createCommitter()` / 能力查询
- - **`IPaimonWriter`** 接口:`write()` / `prepareCommit()` / `compact()` /
`abort()`
- - **`IPaimonCommitter`** 接口:`commit()` / `overwrite()` / `truncateTable()`
/ `truncatePartitions()` / `abort()`
- - **`PaimonWriteBackendFactory`**:`selectBackendType()` + `create()`
- - 这些接口从第一天起就存在,Java 后端只是第一个实现
-
-3. **BE 端 — JNI 后端实现**
- - **`JniPaimonWriteBackend`**:实现 `IPaimonWriteBackend`
- - **`VPaimonTableWriter`**:继承 `AsyncResultWriter`,通过 `IPaimonWriteBackend`
接口调用(不直接依赖 JNI)
- - Block → Arrow → Java 的数据桥接(Paimon SDK 内部处理分区/桶路由)
-
-4. **BE 端 — FFI 后端预留**
- - **`FfiPaimonWriteBackend`**:stub 实现,所有方法返回 `Status::NotSupported("FFI
backend not yet implemented")`
- - 后端选择逻辑:`PaimonWriteBackendFactory` 根据 `TPaimonTableSink.backend_type`
创建对应后端
- - v1 中 `backend_type` 固定为 JNI,但代码路径已支持切换
-
-5. **Java 端 (be-java-extensions)**
- - `PaimonJniWriteBackend` — JNI 入口,接收调用并转发给 Paimon SDK
- - `BatchTableWrite` / `StreamTableWrite` 的 JNI wrapper
- - `CommitMessage` 的序列化(Paimon 原生格式),同时转换为结构化 Thrift 格式
-
-6. **FE 端**
- - `PaimonTableSink` — 生成 Thrift sink 描述,v1 固定 `backend_type = JNI`
- - `PaimonWriteCommitCoordinator` / `PaimonTransaction` — 聚合
CommitMessages,协调提交
- - Nereids planner 集成(INSERT INTO paimon_table SELECT ...)
-
-7. **测试**
- - 单元测试:`IPaimonWriteBackend` 接口的 mock 测试
- - 集成测试:Doris → Paimon → Doris 读写闭环
- - 性能测试:与直接使用 Paimon Flink 写入的对比
-
-### Phase 2:v2 — Rust FFI Backend(约 4-6 周)
-
-**目标**:引入 Rust 后端作为优先选择,加速覆盖大部分写入场景。
-
-**关键**:由于 Phase 1 已经建立了 `IPaimonWriteBackend`
抽象,此阶段**上层代码(VPaimonTableWriter、FE Planner)无需修改**。仅需:
-
-1. **Rust 端(核心工作)**
- - 扩展 `paimon-rust/bindings/c/`:新增写入相关的 C FFI 函数
- - `paimon_write_builder_new()` / `paimon_table_write_new()` /
`paimon_write_arrow_batch()`
- - `paimon_prepare_commit()` / `paimon_table_commit_new()` /
`paimon_commit()` / `paimon_commit_abort()`
- - Arrow C Data Interface 支持(ArrowArray/ArrowSchema FFI,零拷贝)
-
-2. **BE 端(替换 stub)**
- - 将 `FfiPaimonWriteBackend` 从 stub 替换为真正的实现(<1000行新代码)
- - 动态加载 Rust 共享库(`dlopen`)
- - Doris Block → Arrow FFI 的零拷贝转换
- - Tokio runtime 集成(异步 I/O)
-
-3. **功能路由(自动切换)**
- - `PaimonWriteBackendFactory::selectBackendType()` 实现自动判断:
- - 检查表 options 是否包含 Rust 不支持的功能
- - 是 → JNI Backend;否 → FFI Backend
- - FE 端 `TPaimonTableSink.backend_type` 从固定 JNI 变为**自动选择**
-
-### Phase 3:高级写入功能(约 6-8 周)
-
-**目标**:对齐 Paimon 的全部写入能力
-
-1. **Compaction 支持**
- - 触发策略配置
- - 显式 `compact()` 调用
- - Compaction 结果的 CommitMessage 处理
-
-2. **Changelog Producer 全支持**
- - Lookup Changelog Producer
- - FullCompaction Changelog Producer
-
-3. **MERGE INTO 支持**
- - DataEvolution 表的 RowTracking 集成
- - Copy-on-Write 合并
-
-4. **Schema Evolution**
- - 写入时的 Schema 兼容性检查
- - 自动 Schema Update
-
-### Phase 4:优化与 Rust 长期演进(持续)
-
-1. **性能优化**
- - BE 端数据批处理优化
- - 零拷贝数据路径
- - 自适应后端选择
-
-2. **Rust 功能跟进**
- - 与 Paimon Rust 社区协作,补齐 Compaction 等功能
- - 逐步将更多功能从 JNI 迁移到 FFI
-
-3. **稳定性**
- - 混沌测试
- - 大规模数据写入测试
- - 多版本兼容性测试
-
-
-## 附录 A:关键文件路径
-
-### Paimon Java SDK
-| 文件 | 说明 |
-|------|------|
-| `paimon-core/.../table/sink/TableWrite.java` | 写入接口 |
-| `paimon-core/.../table/sink/BatchTableWrite.java` | 批写入接口 |
-| `paimon-core/.../table/sink/BatchWriteBuilder.java` | 写入构建器 |
-| `paimon-core/.../table/sink/BatchTableCommit.java` | 批提交接口 |
-| `paimon-core/.../table/sink/StreamTableWrite.java` | 流写入接口 |
-| `paimon-core/.../table/sink/StreamWriteBuilder.java` | 流写入构建器 |
-| `paimon-core/.../table/sink/CommitMessage.java` | 提交消息接口 |
-| `paimon-core/.../table/sink/CommitMessageSerializer.java` | 提交消息序列化器 |
-
-### Paimon Rust
-| 文件 | 说明 |
-|------|------|
-| `crates/paimon/src/table/write_builder.rs` | WriteBuilder 实现 |
-| `crates/paimon/src/table/table_write.rs` | TableWrite 实现(~3000行) |
-| `crates/paimon/src/table/table_commit.rs` | TableCommit 实现(~4800行) |
-| `crates/paimon/src/table/commit_message.rs` | CommitMessage 结构体 |
-| `crates/paimon/src/table/data_file_writer.rs` | Append-only 文件写入器 |
-| `crates/paimon/src/table/kv_file_writer.rs` | PK 表 KeyValue 写入器 |
-| `crates/paimon/src/table/bucket_assigner*.rs` | 桶分配策略 |
-| `bindings/c/src/table.rs` | C FFI 绑定(当前只有读取) |
-| `bindings/c/Cargo.toml` | C 绑定 crate 配置 |
-
-### Doris
-| 文件 | 说明 |
-|------|------|
-| `be/src/exec/sink/writer/async_result_writer.h` | 异步写入器基类 |
-| `be/src/exec/sink/writer/iceberg/viceberg_table_writer.{h,cpp}` | Iceberg
写入参考 |
-| `be/src/format_v2/jni/paimon_jni_reader.{h,cpp}` | Paimon JNI 读取器(参考) |
-| `be/src/format/jni/jni_data_bridge.{h,cpp}` | JNI 数据桥接 |
-| `gensrc/thrift/DataSinks.thrift` | Thrift sink 定义 |
-| `fe/fe-core/.../planner/IcebergTableSink.java` | Iceberg Sink FE 参考 |
-| `fe/fe-core/.../datasource/paimon/` | Paimon 数据源相关 |
-
----
-
-## 附录 B:设计模式与交互规则
-
-### 关键抽象对应关系
-
-```
-Doris 概念 Paimon 概念 说明
-─────────────────────────────────────────────────────
-VPaimonTableWriter BatchWriteBuilder 整个表的写入器(单实例)
-IPaimonWriter BatchTableWrite 管理所有分区/桶的内部路由
-Block (batch) InternalRow / RecordBatch 数据批次
-prepareCommit() prepareCommit() 生成 CommitMessage
-close() + RPC TableCommit.commit() 提交
-```
-
-### CommitMessage 生命周期
-
-```
-创建:IPaimonWriter::prepareCommit()
- ↓
-序列化(Thrift):BE → FE RPC
- ↓
-聚合:FE Coordinator
- ↓
-去重(idempotency):对比 commit_user + commit_identifier
- ↓
-提交:Commit BE → IPaimonCommitter::commit()
- ↓
-清理(success):释放 writer
-清理(failure):IPaimonCommitter::abort() 删除数据文件
-```
-
----
-
-## 12. 功能矩阵审查:实现 vs 需求
-
-> 基于 `PAIMON_WRITE_FEATURE_MATRIX.md` 对当前 v1 实现的功能覆盖审查。
-
-### 12.1 已覆盖(✅)
-
-| 功能 | 矩阵优先级 | 实现方式 |
-|------|----------|----------|
-| Append-only 表写入 | P0 | JNI backend → Paimon BatchTableWrite (SDK 内部路由) |
-| Primary-key 表写入(full row) | P1 | JNI backend → KeyValueFileWriter,完整 row |
-| Fixed bucket 写入 | P0/P1 | Paimon SDK 内部 MurmurHash 桶计算,Doris 不感知 |
-| Unaware bucket 写入 | P0 | 单 writer,Paimon SDK 内部处理 |
-| Partition 表写入 | P0 | FE 下发 partition columns,Paimon SDK 内部路由 |
-| 非 partition 表写入 | P0 | 单 writer,正常并发写 |
-| `INSERT INTO` | P0 | 主路径,Nereids planner 完整支持 |
-| `INSERT OVERWRITE` | P1 | Thrift 下发 write_mode + static_partition,SDK 内
`withOverwrite()` |
-| Commit message 序列化 | P0 | 结构化 Thrift CommitMessage 传递 |
-| Stream commit 幂等 | P0 | commitUser + txnId |
-| Abort 未提交文件 | P0 | `_writer->abort()` → SDK 内清理 |
-| Changelog producer none | P0 | 默认模式,透明写入 |
-| 多 Catalog 类型 | P1 | FE 下发 Hadoop config + catalog options |
-| 对象存储 | P1 | Hadoop config 透传,支持 S3/OSS/HDFS |
-| 基础类型(Boolean/Integer/Float/String/Decimal/Date) | P0 | Arrow → Paimon 类型转换 |
-| 并发 writer(多 BE) | P0 | BE 天然多实例,FE 汇总 CommitMessage |
-| Nullability 校验 | P0 | Paimon SDK 写入层校验 |
-| 幂等提交 | P0 | commitIdentifier = Doris txnId |
-| 线程数可控 | **P0** | 单 Writer 架构,线程数 = BE worker 数,不随 bucket 数增长 |
-
-### 12.2 部分覆盖(⚠️)
-
-| 功能 | 矩阵优先级 | 现状 | 差距 |
-|------|----------|------|------|
-| Deduplicate merge engine | P1 | 完整 row 写入,理论支持 | 缺少专项测试验证 |
-| 复杂类型(ARRAY/MAP/STRUCT) | P1 | JNI writer 已实现转换逻辑 | 缺少专项测试覆盖 |
-| 小文件控制 | P1 | Paimon SDK 内部按 target-file-size 控制 | SDK 行为,Doris 侧无需干预 |
-| Partition 规范化 | P0 | 依赖 Paimon SDK 内部处理 | 与 Paimon 原生语义完全一致 |
-
-### 12.3 未覆盖 — v2 计划(❌)
-
-| 功能 | 矩阵优先级 | 说明 |
-|------|----------|------|
-| Dynamic hash bucket | P2 | 需要多 assigner 分片并发设计 |
-| Dynamic partition overwrite | P2 | 需要在 static overwrite 稳定后再做 |
-| Row-level delete/update | P3 | 需要 RowKind 映射和 merge engine 联动 |
-| Partial-update merge engine | P3 | Paimon PK writer 路径不支持 `withWriteType` |
-| Aggregation merge engine | P2/P3 | 需要确认聚合字段和 sequence field |
-| First-row merge engine | P3 | 需要明确重复 key 语义 |
-| Sequence field | P2 | 需要从表 option 读取并保证写入列完整性 |
-| Schema evolution | P2 | 需处理计划生成后 schema 变化 |
-| Compaction | P3 | 写入路径不应承担 compaction 调度 |
-| Changelog producer input | P2 | 需要 RowKind 输入支持 |
-| Changelog producer lookup/full-compaction | P3 | 依赖 lookup 或 compaction |
-
-### 12.4 未覆盖 — 暂缓(❌)
-
-| 功能 | 矩阵优先级 | 原因 |
-|------|----------|------|
-| Key dynamic bucket | P3 | 需要全局 key index,复杂度高 |
-| Postpone bucket | P3 | 对 Doris 分发拓扑依赖较强 |
-| Batch commit (`Long.MAX_VALUE`) | 不支持 | 与 Doris 外部事务模型不匹配 |
-
-### 12.5 关键差距总结
-
-当前实现覆盖了矩阵中 **P0 项目的 100%**,P1 项目的 **~80%**。
-
-与 v0(per-bucket writer)相比,v1 架构调整的核心变化:
-- **移除** BE 端 partition/bucket 计算与路由(`paimon_bucket.h/.cpp`、operator 层路由逻辑)
-- **移除** per-(partition, bucket) VPaimonTableWriter 创建与生命周期管理
-- **新增** 单一 VPaimonTableWriter,对标 Iceberg 的 VIcebergTableWriter 架构
-- **复用** Paimon SDK 的 `TableWrite::write()` 内部路由机制
-
-剩余差距集中在 P2/P3 级别:Dynamic bucket、Partial-update、Row-level delete/update、
-Compaction、Changelog producer (input/lookup/full-compaction) 等,按设计文档
-Phase 2-4 路线图推进。
-
----
-
-## 13. 测试计划
-
-### 13.1 功能测试矩阵
-
-#### 13.1.1 基础写入场景
-
-| 用例 ID | 场景 | 表配置 | 数据特征 | 验证点 |
-|---------|------|--------|---------|--------|
-| FT-001 | Append-only 无分区无 bucket | `bucket=1`, 无 PK, 无 partition | 1000 行,3
列 INT/VARCHAR/DOUBLE | commit 成功,Paimon 可读回全部行 |
-| FT-002 | Append-only 有分区 | 按 region 分区, 无 PK | 5 个分区,每分区 200 行 |
分区路径正确,manifest 正确 |
-| FT-003 | Append-only + fixed bucket | `bucket=4`, `bucket-key=id` | 10000 行
id 从 1-10000 | 4 个 bucket 均有数据文件 |
-| FT-004 | PK 表 + fixed bucket + full row | `bucket=2`, PK=id,
merge-engine=deduplicate | 1000 行含重复 id | 重复 key 保留最新值 |
-| FT-005 | 多 BE 并发写入 | bucket unaware, 4 个 BE | 每个 BE 写 5000 行 | commit
message 去重,总行数正确 |
-
-#### 13.1.2 事务与容错
-
-| 用例 ID | 场景 | 操作 | 验证点 |
-|---------|------|------|--------|
-| FT-010 | FE commit 成功 | INSERT INTO → commit | Snapshot 创建,可读 |
-| FT-011 | FE commit 失败 → rollback | 模拟 commit 异常 | abort 清理数据文件,无孤儿文件 |
-| FT-012 | BE 写入失败 → abort | BE 端 JNI 调用异常 | writer.abort() 被调用 |
-| FT-013 | Fragment retry 去重 | 同 txnId 多次上报 commit message | base64 去重生效 |
-| FT-014 | 空表写入 | 0 行数据 | prepareCommit 返回空,不创建 Snapshot |
-| FT-015 | 大事务提交 | 10 万行,4 BE | commit message 分块正确,payload ≤ 8MB |
-
-#### 13.1.3 类型覆盖
-
-| 用例 ID | Paimon 类型 | Doris 类型 | 特殊值 |
-|---------|-----------|-----------|--------|
-| FT-020 | BOOLEAN | BOOLEAN | true, false, NULL |
-| FT-021 | INT / BIGINT | INT / BIGINT | 0, -1, INT_MAX, INT_MIN, NULL |
-| FT-022 | FLOAT / DOUBLE | FLOAT / DOUBLE | 0.0, NaN, INF, -0.0, NULL |
-| FT-023 | DECIMAL(10,2) | DECIMAL(10,2) | 0, 99999999.99, -1.50, NULL |
-| FT-024 | VARCHAR(100) | VARCHAR(100) | "", "hello", 100-char string, NULL |
-| FT-025 | CHAR(10) | CHAR(10) | "abc", 10-char pad, NULL |
-| FT-026 | DATE | DATE | 1970-01-01, 2099-12-31, NULL |
-| FT-027 | TIMESTAMP(6) | DATETIME(6) | 微秒精度, NULL |
-| FT-028 | ARRAY<INT> | ARRAY<INT> | [], [1,2,3], NULL |
-| FT-029 | MAP<STRING,INT> | MAP<STRING,INT> | {}, {"a":1}, NULL |
-| FT-030 | ROW<name STRING, age INT> | STRUCT | 嵌套 NULL 字段 |
-
-#### 13.1.4 边界场景
-
-| 用例 ID | 场景 | 验证点 |
-|---------|------|--------|
-| FT-040 | NULL 列全部为 NULL | 写入正确,查询返回 NULL |
-| FT-041 | 主键全 NULL(如允许) | 行为符合 Paimon 规范 |
-| FT-042 | 超长 VARCHAR > 65535 | 写入失败有清晰错误信息 |
-| FT-043 | DECIMAL 精度溢出 | 写入失败有清晰错误信息 |
-| FT-044 | Bucket key 为复合键 | 路由正确 |
-
-#### 13.1.5 存储后端
-
-| 用例 ID | 存储 | 验证点 |
-|---------|------|--------|
-| FT-050 | Local FS | 基本功能 |
-| FT-051 | HDFS | rename 语义, commit |
-| FT-052 | S3 (MinIO 模拟) | 对象存储 rename 行为 |
-
-### 13.2 性能测试计划
-
-#### 13.2.1 吞吐量基准测试
-
-**目标**:确定 Doris → Paimon 写入的吞吐基线。
-
-| 测试场景 | 表类型 | 数据量 | BE 数 | 对比基准 |
-|---------|--------|--------|-------|----------|
-| PT-001 | Append-only, bucket unaware | 1 亿行, 100GB | 1/4/8 | Paimon Flink
Writer 同等条件 |
-| PT-002 | PK 表, fixed bucket=16 | 1 亿行, 100GB | 1/4/8 | Paimon Flink Writer
同等条件 |
-| PT-003 | Append-only, partition=10 | 1 亿行, 100GB | 4 | Paimon Flink Writer
同等条件 |
-
-**测量指标**:
-- 写入吞吐(rows/s, MB/s)
-- BE 内存占用(peak, avg)
-- Arrow IPC 序列化/反序列化耗时占比
-- JNI 调用耗时占比
-- Paimon FileStoreWrite 耗时占比
-- Commit 耗时(prepareCommit + FE commit)
-
-#### 13.2.2 扩展性测试
-
-| 测试场景 | 变量 | 期望 |
-|---------|------|------|
-| PT-010 | BE 数 1→2→4→8 | 吞吐接近线性增长 |
-| PT-011 | 分区数 1→10→100 | 吞吐稳定,文件数可控 |
-| PT-012 | Bucket 数 1→4→16→64 | 并发 writer 数增长,吞吐改善 |
-
-#### 13.2.3 大数据量压力测试
-
-| 测试场景 | 数据量 | 关注点 |
-|---------|--------|--------|
-| PT-020 | 10 亿行, 1TB Append-only | 长时间运行稳定性,内存无泄漏 |
-| PT-021 | 10 亿行, 1TB PK 表 | merge engine 排序内存,spill 行为 |
-| PT-022 | 1000 分区, 每分区 100 万行 | manifest 膨胀,commit 性能 |
-
-#### 13.2.4 资源限制测试
-
-| 测试场景 | 限制 | 验证点 |
-|---------|------|--------|
-| PT-030 | BE 内存限制 4GB | spill 正常触发,不 OOM |
-| PT-031 | 限流场景(网络带宽 100Mbps) | 吞吐自适应,无超时重试雪崩 |
-| PT-032 | 并发 10 个 INSERT 任务 | 资源隔离,无互相影响 |
-
-### 13.3 测试基础设施
-
-#### 13.3.1 回归测试框架
-
-```
-regression-test/suites/paimon_write_p0/
-├── test_insert_append_only.groovy # FT-001
-├── test_insert_partitioned.groovy # FT-002
-├── test_insert_fixed_bucket.groovy # FT-003
-├── test_insert_pk_dedup.groovy # FT-004
-├── test_insert_multi_be.groovy # FT-005
-├── test_transaction_commit.groovy # FT-010
-├── test_transaction_rollback.groovy # FT-011
-├── test_fragment_retry_dedup.groovy # FT-013
-├── test_empty_write.groovy # FT-014
-├── test_types_basic.groovy # FT-020..027
-├── test_types_complex.groovy # FT-028..030
-├── test_nullability.groovy # FT-040
-├── test_boundary.groovy # FT-041..044
-├── test_storage_hdfs.groovy # FT-051
-└── test_storage_s3.groovy # FT-052
-```
-
-#### 13.3.2 性能测试脚本
-
-```
-tools/paimon_perf/
-├── benchmark.sh # 一键启动性能测试
-├── schema/
-│ ├── append_only_ddl.sql # Append-only 表 DDL
-│ ├── pk_table_ddl.sql # PK 表 DDL
-│ └── partitioned_table_ddl.sql # 分区表 DDL
-├── workload/
-│ ├── gen_data.sh # 数据生成(TPC-H / TPC-DS)
-│ └── run_benchmark.sh # 执行基准测试
-└── report/
- └── analyze.sh # 结果汇总和分析
-```
-
-### 13.4 推荐里程碑
-
-与 `PAIMON_WRITE_FEATURE_MATRIX.md` Section 13 对齐:
-
-| 里程碑 | 覆盖范围 | 验收标准 | 测试用例 |
-|--------|---------|---------|----------|
-| **M1** | append-only + bucket unaware + INSERT INTO | 多 BE 并发写成功,FE commit
成功,失败可 abort | FT-001, FT-010..014, PT-001 |
-| **M2** | partition + fixed bucket | 按 partition/bucket 并发写,Paimon 正确读取 |
FT-002..003, FT-005, PT-003 |
-| **M3** | primary-key fixed bucket full row | PK upsert 语义,重复 key 符合 merge
engine | FT-004, PT-002 |
-| **M4** | 类型全覆盖 + 复杂类型 | 全部基础类型 + ARRAY/MAP/STRUCT | FT-020..030 |
-| **M5** | 存储后端全覆盖 | HDFS + S3 通过回归测试 | FT-050..052 |
-| **M6** | INSERT OVERWRITE + dynamic bucket | v2 功能 | 待 v2 定义 |
-
-### 13.5 M1 准入标准(当前 v1 应达到)
-
-1. ✅ FE 编译通过,checkstyle 0 违规
-2. ⬜ BE 编译通过(CI 验证)
-3. ⬜ FT-001 ~ FT-005 全部通过的回归测试
-4. ⬜ FT-010 ~ FT-015 事务容错测试通过
-5. ⬜ PT-001 吞吐不低于 Flink Writer 的 70%
-6. ⬜ 10 小时连续写入无内存泄漏
-
----
-
-> **文档版本**:v8.0
-> **作者**:Doris Paimon Write Team
-> **日期**:2026-07-13
-> **状态**:架构重构:Single Writer(对标 Iceberg)+ Paimon SDK 内部路由。移除 BE 端
partition/bucket 路由计算。
-> **变更摘要**:
-> - 从 "One Writer per (Partition,Bucket)" 改为 "Single Writer" 架构
-> - 分区路由由 Paimon SDK 内部处理,Doris 不再参与路由计算
-> - 移除 `paimon_bucket.h/.cpp`、operator 层路由/拆分/多 writer 管理逻辑
-> - 线程数从 O(partition_count × bucket_count) 降为 O(BE_worker_count)
-> - `IPaimonWriter::write()` 接收完整 Block,SDK 内部路由
-> - `IPaimonWriteBackend::createWriter()` 不再需要 partition/bucket 参数
diff --git
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java
index dbb5ead0dc5..2eac6e95f56 100644
---
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java
+++
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java
@@ -47,14 +47,17 @@ import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.CatalogContext;
import org.apache.paimon.catalog.CatalogFactory;
import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.Decimal;
import org.apache.paimon.data.GenericArray;
import org.apache.paimon.data.GenericMap;
import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
import org.apache.paimon.data.Timestamp;
import org.apache.paimon.disk.IOManager;
import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.io.BundleRecords;
import org.apache.paimon.io.DataOutputSerializer;
import org.apache.paimon.memory.HeapMemorySegmentPool;
import org.apache.paimon.options.Options;
@@ -86,6 +89,8 @@ import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -98,8 +103,9 @@ import java.util.stream.Collectors;
* C++ Block → Arrow IPC Stream → direct memory address
* → PaimonJniWriter.write(address, length)
* → ArrowStreamReader → VectorSchemaRoot
- * → convert row-by-row → Paimon GenericRow
- * → BatchTableWrite.write(row) [SDK handles partition+bucket routing]
+ * → extractColumnValues (column-major typed extraction)
+ * → group by writer.getPartition(row) + writer.getBucket(row)
+ * → writer.writeBundle(partition, bucket, bundle) [batch write]
*
* Commit path:
*
@@ -395,94 +401,170 @@ public class PaimonJniWriter {
List<FieldVector> vectors = root.getFieldVectors();
int colCount = fields.size();
DataType[] currentTypes = resolveTargetTypes(fields);
- GenericRow reusedRow = new GenericRow(colCount);
- for (int i = 0; i < rowCount; i++) {
- for (int col = 0; col < colCount; col++) {
- try {
- reusedRow.setField(col,
- readArrowValue(vectors.get(col), i,
fields.get(col), currentTypes[col]));
- } catch (Throwable t) {
- throw new RuntimeException("PaimonJniWriter row conversion
failed: row=" + i
- + ", col=" + col + ", field=" +
fields.get(col).getName()
- + ", targetType=" + (currentTypes[col] != null
- ? currentTypes[col].asSQLString() :
"null"), t);
- }
- }
- try {
- // Generic write — Paimon SDK handles partition computation
- // (getPartition) and bucket computation (getBucket)
internally.
- writer.write(reusedRow);
- } catch (Throwable t) {
- throw new RuntimeException("PaimonJniWriter write failed:
row=" + i
- + ", totalRows=" + rowCount, t);
+ // Column-major extraction: one instanceof per column, N direct getter
calls.
+ Object[][] columnValues = new Object[colCount][];
+ for (int col = 0; col < colCount; col++) {
+ columnValues[col] = extractColumnValues(vectors.get(col),
fields.get(col),
+ currentTypes[col], rowCount);
+ }
+
+ // Group rows by (partition, bucket) using the Paimon SDK's routing
methods,
+ // then write each group via writeBundle for batch-level I/O
optimization.
+ Map<PartitionBucketKey, List<InternalRow>> groups = new
LinkedHashMap<>();
+ for (int r = 0; r < rowCount; r++) {
+ GenericRow row = new GenericRow(colCount);
+ for (int c = 0; c < colCount; c++) {
+ row.setField(c, columnValues[c][r]);
}
+ BinaryRow partition = writer.getPartition(row);
+ int bucket = writer.getBucket(row);
+ groups.computeIfAbsent(new PartitionBucketKey(partition, bucket),
+ k -> new ArrayList<>()).add(row);
+ }
+
+ for (Map.Entry<PartitionBucketKey, List<InternalRow>> entry :
groups.entrySet()) {
+ PartitionBucketKey key = entry.getKey();
+ writer.writeBundle(key.partition, key.bucket, new
ListBundleRecords(entry.getValue()));
}
}
- // ────────────────────────────────────────────────────────────
- // Arrow → Paimon type conversion
- // ────────────────────────────────────────────────────────────
+ /**
+ * Extract all values for one column using a type-specific fast path.
+ * Single instanceof per column — no per-cell type dispatch.
+ */
+ private Object[] extractColumnValues(FieldVector vector, Field arrowField,
+ DataType targetType, int rowCount) {
+ Object[] values = new Object[rowCount];
- private Object readArrowValue(FieldVector vector, int row, Field
arrowField,
- DataType targetType) {
- if (vector == null || vector.isNull(row)) {
- return null;
- }
- if (targetType instanceof BinaryType || targetType instanceof
VarBinaryType) {
- if (vector instanceof VarBinaryVector) {
- return ((VarBinaryVector) vector).get(row);
- }
- if (vector instanceof VarCharVector) {
- return ((VarCharVector) vector).get(row);
- }
- } else {
- if (vector instanceof VarCharVector) {
- return BinaryString.fromBytes(((VarCharVector)
vector).get(row));
+ // ── Primitive int types ──────────────────────────────
+ if (vector instanceof IntVector) {
+ IntVector v = (IntVector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
}
+ return values;
}
- if (vector instanceof BitVector) {
- return ((BitVector) vector).get(row) == 1;
- }
- if (vector instanceof TinyIntVector) {
- return ((TinyIntVector) vector).get(row);
+ if (vector instanceof BigIntVector) {
+ BigIntVector v = (BigIntVector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ return values;
}
if (vector instanceof SmallIntVector) {
- return ((SmallIntVector) vector).get(row);
- }
- if (vector instanceof IntVector) {
- return ((IntVector) vector).get(row);
+ SmallIntVector v = (SmallIntVector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ return values;
}
- if (vector instanceof BigIntVector) {
- return ((BigIntVector) vector).get(row);
+ if (vector instanceof TinyIntVector) {
+ TinyIntVector v = (TinyIntVector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ return values;
}
if (vector instanceof Float4Vector) {
- return ((Float4Vector) vector).get(row);
+ Float4Vector v = (Float4Vector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ return values;
}
if (vector instanceof Float8Vector) {
- return ((Float8Vector) vector).get(row);
+ Float8Vector v = (Float8Vector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ return values;
+ }
+ if (vector instanceof BitVector) {
+ BitVector v = (BitVector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i) == 1;
+ }
+ return values;
}
if (vector instanceof DateDayVector) {
- return ((DateDayVector) vector).get(row);
+ DateDayVector v = (DateDayVector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ return values;
+ }
+
+ // ── String / Binary ──────────────────────────────────
+ if (vector instanceof VarCharVector) {
+ VarCharVector v = (VarCharVector) vector;
+ if (targetType instanceof BinaryType || targetType instanceof
VarBinaryType) {
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ } else {
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null :
BinaryString.fromBytes(v.get(i));
+ }
+ }
+ return values;
}
+ if (vector instanceof VarBinaryVector) {
+ VarBinaryVector v = (VarBinaryVector) vector;
+ for (int i = 0; i < rowCount; i++) {
+ values[i] = v.isNull(i) ? null : v.get(i);
+ }
+ return values;
+ }
+
+ // ── Timestamp ────────────────────────────────────────
if (vector instanceof TimeStampMicroVector) {
+ TimeStampMicroVector v = (TimeStampMicroVector) vector;
NullableTimeStampMicroHolder holder = new
NullableTimeStampMicroHolder();
- ((TimeStampMicroVector) vector).get(row, holder);
- return Timestamp.fromMicros(holder.value);
+ for (int i = 0; i < rowCount; i++) {
+ if (v.isNull(i)) {
+ values[i] = null;
+ } else {
+ v.get(i, holder);
+ values[i] = Timestamp.fromMicros(holder.value);
+ }
+ }
+ return values;
}
+
+ // ── Decimal ──────────────────────────────────────────
if (vector instanceof DecimalVector) {
- return readDecimal((DecimalVector) vector, row);
+ DecimalVector v = (DecimalVector) vector;
+ int precision = v.getPrecision();
+ int scale = v.getScale();
+ org.apache.arrow.memory.ArrowBuf dataBuf = v.getDataBuffer();
+ for (int i = 0; i < rowCount; i++) {
+ if (v.isNull(i)) {
+ values[i] = null;
+ } else {
+ BigDecimal bd = getBigDecimalFromArrowBuf(dataBuf, i,
scale,
+ DecimalVector.TYPE_WIDTH);
+ values[i] = Decimal.fromBigDecimal(bd, precision, scale);
+ }
+ }
+ return values;
}
- Object val = vector.getObject(row);
- return convertToPaimonType(val, arrowField, targetType);
- }
- private Decimal readDecimal(DecimalVector vector, int row) {
- BigDecimal bd = getBigDecimalFromArrowBuf(vector.getDataBuffer(), row,
- vector.getScale(), DecimalVector.TYPE_WIDTH);
- return Decimal.fromBigDecimal(bd, vector.getPrecision(),
vector.getScale());
+ // ── Complex types (Array / Map / Struct) — fallback ──
+ for (int i = 0; i < rowCount; i++) {
+ if (vector.isNull(i)) {
+ values[i] = null;
+ } else {
+ values[i] = convertToPaimonType(vector.getObject(i),
arrowField, targetType);
+ }
+ }
+ return values;
}
+ // ────────────────────────────────────────────────────────────
+ // Arrow → Paimon type conversion (used by extractColumnValues fallback)
+ // ────────────────────────────────────────────────────────────
+
@SuppressWarnings({"unchecked", "rawtypes"})
private Object convertToPaimonType(Object val, Field arrowField, DataType
targetType) {
if (val == null) {
@@ -820,6 +902,57 @@ public class PaimonJniWriter {
+ ", writerNull=" + (writer == null);
}
+ // ────────────────────────────────────────────────────────────
+ // Bundle helpers
+ // ────────────────────────────────────────────────────────────
+
+ /** Compound key for grouping rows by (partition, bucket). */
+ private static final class PartitionBucketKey {
+ final BinaryRow partition;
+ final int bucket;
+
+ PartitionBucketKey(BinaryRow partition, int bucket) {
+ this.partition = partition;
+ this.bucket = bucket;
+ }
+
+ @Override
+ public int hashCode() {
+ return partition.hashCode() * 31 + bucket;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof PartitionBucketKey)) {
+ return false;
+ }
+ PartitionBucketKey other = (PartitionBucketKey) obj;
+ return bucket == other.bucket && partition.equals(other.partition);
+ }
+ }
+
+ /** BundleRecords backed by a List of pre-built InternalRows. */
+ private static final class ListBundleRecords implements BundleRecords {
+ private final List<InternalRow> rows;
+
+ ListBundleRecords(List<InternalRow> rows) {
+ this.rows = rows;
+ }
+
+ @Override
+ public long rowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public Iterator<InternalRow> iterator() {
+ return rows.iterator();
+ }
+ }
+
/** InputStream over a direct ByteBuffer (no copy). */
private static class DirectBufInputStream extends InputStream {
private final ByteBuffer buf;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPaimonTableSink.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPaimonTableSink.java
index 2b98c2a4618..f8d53ba3e23 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPaimonTableSink.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPaimonTableSink.java
@@ -21,10 +21,8 @@ import org.apache.doris.catalog.Column;
import org.apache.doris.datasource.paimon.PaimonExternalDatabase;
import org.apache.doris.datasource.paimon.PaimonExternalTable;
import org.apache.doris.nereids.memo.GroupExpression;
-import org.apache.doris.nereids.properties.DistributionSpecHash;
import org.apache.doris.nereids.properties.LogicalProperties;
import org.apache.doris.nereids.properties.PhysicalProperties;
-import org.apache.doris.nereids.trees.expressions.ExprId;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.plans.AbstractPlan;
import org.apache.doris.nereids.trees.plans.Plan;
@@ -32,10 +30,8 @@ import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.statistics.Statistics;
-import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
-import java.util.stream.Collectors;
/**
* Physical Paimon table sink.
@@ -103,31 +99,8 @@ public class PhysicalPaimonTableSink<CHILD_TYPE extends
Plan>
@Override
public PhysicalProperties getRequirePhysicalProperties() {
- if (physicalProperties != null) {
- return physicalProperties;
- }
- // For HASH_FIXED tables, require hash distribution on (partition cols
+ bucket key cols)
- // so that the same (partition, bucket) data is routed to the same BE.
- // This allows one AsyncResultWriter per (partition, bucket) with no
cross-BE conflicts.
- PaimonExternalTable table = (PaimonExternalTable) targetTable;
- List<String> partitionKeys = table.getPartitionColumnNames();
- List<String> bucketKeys = table.getBucketKeys();
-
- List<Integer> shuffleColIdx = new ArrayList<>();
- List<Column> fullSchema = table.getFullSchema();
- for (int i = 0; i < fullSchema.size(); i++) {
- String colName = fullSchema.get(i).getName();
- if (partitionKeys.contains(colName) ||
bucketKeys.contains(colName)) {
- shuffleColIdx.add(i);
- }
- }
-
- if (!shuffleColIdx.isEmpty()) {
- List<ExprId> exprIds = shuffleColIdx.stream()
- .map(idx -> child().getOutput().get(idx).getExprId())
- .collect(Collectors.toList());
- return new PhysicalProperties(new DistributionSpecHash(exprIds,
DistributionSpecHash.ShuffleType.REQUIRE));
- }
+ // Partition and bucket routing is handled by the Paimon SDK
internally.
+ // Doris does not need to shuffle data by partition/bucket keys.
return PhysicalProperties.SINK_RANDOM_PARTITIONED;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/planner/PaimonTableSink.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/PaimonTableSink.java
index 0c83f41e366..0fb6d392a09 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/PaimonTableSink.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/PaimonTableSink.java
@@ -51,9 +51,9 @@ import java.util.Set;
* Paimon table sink.
*
* Generates TPaimonTableSink payload consumed by BE, including table location,
- * Paimon options, Hadoop config, bucket metadata, and sink column names.
+ * Paimon options, Hadoop config, write mode, and sink column names.
*
- * v1: always sets backend_type = JNI.
+ * v1: single-writer architecture; partition/bucket routing delegated to SDK.
*/
public class PaimonTableSink extends BaseExternalTableDataSink {
private static final Logger LOG =
LogManager.getLogger(PaimonTableSink.class);
@@ -162,30 +162,7 @@ public class PaimonTableSink extends
BaseExternalTableDataSink {
tSink.setSerializedTable(encodeObjectToString(paimonTable));
}
- // Bucket info
- if (paimonTable instanceof org.apache.paimon.table.FileStoreTable) {
- org.apache.paimon.table.FileStoreTable fst =
- (org.apache.paimon.table.FileStoreTable) paimonTable;
- int bucketNum = fst.schema().numBuckets();
- if (bucketNum > 0) {
- tSink.setBucketNum(bucketNum);
- }
- // Extract bucket key column indices for BE-side routing
- List<String> bucketKeys = fst.schema().bucketKeys();
- if (!bucketKeys.isEmpty() && cols != null) {
- List<Integer> bucketKeyIndices = new ArrayList<>();
- for (int i = 0; i < cols.size(); i++) {
- if (bucketKeys.contains(cols.get(i).getName())) {
- bucketKeyIndices.add(i);
- }
- }
- if (!bucketKeyIndices.isEmpty()) {
- tSink.setBucketKeyIndices(bucketKeyIndices);
- }
- }
- }
-
- // Write mode: APPEND or OVERWRITE
+ // Serialize table for BE-side fast loading
tSink.setBackendType(TPaimonWriteBackendType.JNI);
if (insertCtx.isPresent() && insertCtx.get() instanceof
PaimonInsertCommandContext) {
PaimonInsertCommandContext ctx =
diff --git a/gensrc/thrift/DataSinks.thrift b/gensrc/thrift/DataSinks.thrift
index a4078c96e42..3f6eeb438c9 100644
--- a/gensrc/thrift/DataSinks.thrift
+++ b/gensrc/thrift/DataSinks.thrift
@@ -630,9 +630,7 @@ enum TPaimonWriteMode {
}
struct TPaimonCommitMessage {
- 1: optional binary partition // serialized BinaryRow bytes
- 2: optional i32 bucket
- 3: optional binary payload // Paimon native
CommitMessageSerializer bytes
+ 1: optional binary payload // Paimon native
CommitMessageSerializer bytes (DPCM-framed)
}
struct TPaimonTableSink {
@@ -641,13 +639,11 @@ struct TPaimonTableSink {
3: optional string table_location
4: optional map<string, string> paimon_options
5: optional map<string, string> hadoop_config
- 6: optional i32 bucket_num
- 7: optional list<string> column_names
- 8: optional string serialized_table // serialized Paimon Table
object (base64)
- 9: optional TPaimonWriteBackendType backend_type // v1 fixed to JNI
- 10: optional TPaimonWriteMode write_mode // APPEND or OVERWRITE
- 11: optional list<i32> bucket_key_indices // indices of
bucket-key columns in column_names
- 12: optional map<string, string> static_partition // static partition
spec for INSERT OVERWRITE
+ 6: optional list<string> column_names
+ 7: optional string serialized_table // serialized Paimon Table
object (base64)
+ 8: optional TPaimonWriteBackendType backend_type
+ 9: optional TPaimonWriteMode write_mode
+ 10: optional map<string, string> static_partition // static partition
spec for INSERT OVERWRITE
}
struct TDataSink {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]