924060929 commented on code in PR #65381:
URL: https://github.com/apache/doris/pull/65381#discussion_r3549497387


##########
gensrc/thrift/FrontendService.thrift:
##########
@@ -325,14 +325,16 @@ struct TReportExecStatusParams {
 
   28: optional list<DataSinks.TIcebergCommitData> iceberg_commit_datas
 
-  29: optional i64 txn_id
-  30: optional string label
+  29: optional list<DataSinks.TPaimonCommitMessage> paimon_commit_messages

Review Comment:
   ๐Ÿ”ด ไธ่ƒฝๆ”นๅŠจๅทฒๆœ‰ Thrift ๅญ—ๆฎต็š„ field idใ€‚่ฟ™้‡ŒๆŠŠ `paimon_commit_messages` ๆ”พๅˆฐไบ† 29๏ผŒ็ญ‰ไบŽๆŠŠ 
`txn_id`(29โ†’30)ใ€`label`(30โ†’31)ใ€`fragment_instance_reports`(31โ†’32)ใ€`mc_commit_datas`(32โ†’33)ใ€`first_error_msg`(33โ†’34)
 ๆ•ดไฝ“ๅŽ็งปใ€‚
   
   field id ๆ˜ฏ Thrift ็š„็บฟไธŠ่บซไปฝใ€‚`TReportExecStatusParams` ๆฏๆฌก fragment ไธŠๆŠฅ้ƒฝไผš็”จๅˆฐ๏ผŒๆปšๅŠจๅ‡็บง / 
ๆทท็‰ˆๆœฌๆ—ถๆ—ง BE ๅ‘็š„ `txn_id`(29) ไผš่ขซๆ–ฐ FE ๅฝ“ๆˆ `paimon_commit_messages` ่งฃๆž๏ผˆ็ฑปๅž‹ไธ็ฌฆ่ขซไธขๅผƒ๏ผ‰๏ผŒ็›ดๆŽฅ็ ดๅๅŒ…ๆ‹ฌ 
Iceberg / Hive / MaxCompute ๅœจๅ†…็š„ๆ‰€ๆœ‰ๅค–่กจๅฏผๅ…ฅ็Šถๆ€ไธŠๆŠฅใ€‚
   
   ่ฏทไฟๆŒ 29โ€“33 ๅŽŸๆ ทไธๅŠจ๏ผŒๆ–ฐๅญ—ๆฎต็”จไธ‹ไธ€ไธช็ฉบ้—ฒ id๏ผˆ่ฟ™้‡Œๅบ”ไธบ 34๏ผ‰ใ€‚



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java:
##########
@@ -591,6 +594,21 @@ public PlanFragment 
visitPhysicalIcebergTableSink(PhysicalIcebergTableSink<? ext
         return rootFragment;
     }
 
+    @Override
+    public PlanFragment visitPhysicalPaimonTableSink(PhysicalPaimonTableSink<? 
extends Plan> paimonTableSink,
+                                                      PlanTranslatorContext 
context) {
+        PlanFragment rootFragment = paimonTableSink.child().accept(this, 
context);
+        rootFragment.setOutputPartition(DataPartition.UNPARTITIONED);
+        List<Expr> outputExprs = Lists.newArrayList();
+        paimonTableSink.getOutput().stream().map(Slot::getExprId)
+                .forEach(exprId -> 
outputExprs.add(context.findSlotRef(exprId)));
+        PaimonTableSink sink = new PaimonTableSink(
+                (PaimonExternalTable) paimonTableSink.getTargetTable());
+        rootFragment.setSink(sink);
+        sink.setOutputExprs(outputExprs);

Review Comment:
   ๐Ÿ”ด ่ฟ™้‡Œๅช `setOutputExprs`๏ผŒ็ผบ `sink.setCols(paimonTableSink.getCols())`ใ€‚
   
   `PaimonTableSink.bindDataSink()` ้‡Œ `for (Column col : cols)` ไผšๆ— ๆกไปถ้ๅކ `cols`๏ผŒ่€Œ 
`cols` ไธ€็›ดๆ˜ฏ null โ†’ ๆฏๆฌก Paimon insert ๅฟ… NPE๏ผ›ๅŒๆ—ถ `bucket_key_indices` ๅ› ไธบ `cols != 
null` ๅˆค็ฉบไนŸๆฐธ่ฟœไธไผšไธ‹ๅ‘ใ€‚ๅ‚่€ƒ Iceberg / Hive sink ็š„ translator ่กฅไธŠ setColsใ€‚



##########
be/src/exec/sink/writer/paimon/vpaimon_table_writer.cpp:
##########
@@ -0,0 +1,299 @@
+// 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/vpaimon_table_writer.h"
+
+#include "common/logging.h"
+#include "core/block/block.h"
+#include "exec/sink/writer/paimon/paimon_bucket.h"
+#include "runtime/runtime_state.h"
+#include "vec/data_types/data_type_factory.hpp"
+
+namespace doris {
+
+static constexpr const char* PAIMON_OUTPUT_COLUMN_NAMES_KEY = 
"doris.output_column_names";
+static constexpr char PAIMON_COLUMN_NAME_SEPARATOR = '\x01';
+
+VPaimonTableWriter::VPaimonTableWriter(const TDataSink& t_sink,
+                                       const VExprContextSPtrs& output_exprs,
+                                       std::shared_ptr<Dependency> dep,
+                                       std::shared_ptr<Dependency> fin_dep)
+        : AsyncResultWriter(output_exprs, std::move(dep), std::move(fin_dep)), 
_t_sink(t_sink) {
+    DCHECK(_t_sink.__isset.paimon_table_sink);
+}
+
+Status VPaimonTableWriter::open(RuntimeState* state, RuntimeProfile* profile) {
+    _state = state;
+    _operator_profile = profile;
+
+    // Register profile counters
+    _written_rows_counter = ADD_COUNTER(_operator_profile, "WrittenRows", 
TUnit::UNIT);
+    _written_bytes_counter = ADD_COUNTER(_operator_profile, "WrittenBytes", 
TUnit::BYTES);
+    _send_data_timer = ADD_TIMER(_operator_profile, "SendDataTime");
+    _project_timer = ADD_CHILD_TIMER(_operator_profile, "ProjectTime", 
"SendDataTime");
+    _bucket_compute_timer = ADD_CHILD_TIMER(_operator_profile, 
"BucketComputeTime", "SendDataTime");
+    _arrow_convert_timer = ADD_CHILD_TIMER(_operator_profile, 
"ArrowConvertTime", "SendDataTime");
+    _file_store_write_timer =
+            ADD_CHILD_TIMER(_operator_profile, "FileStoreWriteTime", 
"SendDataTime");
+    _open_timer = ADD_TIMER(_operator_profile, "OpenTime");
+    _close_timer = ADD_TIMER(_operator_profile, "CloseTime");
+    _prepare_commit_timer = ADD_TIMER(_operator_profile, "PrepareCommitTime");
+    _serialize_commit_messages_timer = ADD_TIMER(_operator_profile, 
"SerializeCommitMessagesTime");
+    _commit_payload_count = ADD_COUNTER(_operator_profile, 
"CommitPayloadCount", TUnit::UNIT);
+    _commit_payload_bytes_counter =
+            ADD_COUNTER(_operator_profile, "CommitPayloadBytes", TUnit::BYTES);
+    _bucket_writer_count = ADD_COUNTER(_operator_profile, "BucketWriterCount", 
TUnit::UNIT);
+
+    SCOPED_TIMER(_open_timer);
+
+    // Create the backend via factory
+    
RETURN_IF_ERROR(PaimonWriteBackendFactory::create(_t_sink.paimon_table_sink, 
&_backend));
+    RETURN_IF_ERROR(_backend->open(_t_sink.paimon_table_sink, state));
+
+    // Determine bucket routing strategy
+    const auto& paimon_sink = _t_sink.paimon_table_sink;
+    _total_buckets = paimon_sink.__isset.bucket_num ? paimon_sink.bucket_num : 
1;
+
+    // Bucket routing is enabled when:
+    // 1. num_buckets > 1 (HASH_FIXED mode)
+    // 2. Bucket key columns are specified
+    _bucket_routing_enabled = _total_buckets > 1;
+
+    if (_bucket_routing_enabled && paimon_sink.__isset.column_names) {
+        // The bucket key is determined by Paimon options (bucket-key).
+        // FE passes column names; we need the indices of bucket key columns.
+        // For now, we use the primary key columns as bucket key (Paimon 
default).
+        // In production, FE should explicitly pass bucket_key_indices.
+        //
+        // When bucket-key is explicitly configured, FE should populate
+        // TPaimonTableSink.bucket_key_indices.
+        if (paimon_sink.__isset.bucket_key_indices && 
!paimon_sink.bucket_key_indices.empty()) {
+            _bucket_key_indices = 
std::vector<int>(paimon_sink.bucket_key_indices.begin(),
+                                                   
paimon_sink.bucket_key_indices.end());
+        }
+    }
+
+    LOG(INFO) << "VPaimonTableWriter opened: table=" << paimon_sink.tb_name
+              << ", backend=" << static_cast<int>(_backend->type())
+              << ", total_buckets=" << _total_buckets
+              << ", bucket_routing=" << _bucket_routing_enabled
+              << ", bucket_key_indices=" << _bucket_key_indices.size();
+    return Status::OK();
+}
+
+Status VPaimonTableWriter::write(RuntimeState* state, Block& block) {
+    if (block.rows() == 0) {
+        return Status::OK();
+    }
+
+    SCOPED_TIMER(_send_data_timer);
+
+    // Project: apply output expressions
+    Block output_block;
+    {
+        SCOPED_TIMER(_project_timer);
+        RETURN_IF_ERROR(_projection_block(block, &output_block));
+    }
+
+    // Rename columns using the output column names from the sink config
+    const auto& paimon_sink = _t_sink.paimon_table_sink;
+    if (paimon_sink.__isset.paimon_options) {
+        auto it = 
paimon_sink.paimon_options.find(PAIMON_OUTPUT_COLUMN_NAMES_KEY);
+        if (it != paimon_sink.paimon_options.end()) {
+            auto output_names = _split_column_names(it->second);

Review Comment:
   ๐Ÿ”ด ็ผ–่ฏ‘ไธ่ฟ‡๏ผš`_split_column_names` ๆ˜ฏๆœฌๆ–‡ไปถ็š„ `static` ๅ‡ฝๆ•ฐ๏ผŒๅฎšไน‰ๅœจๆ–‡ไปถๆœซๅฐพ๏ผŒไฝ†่ฟ™้‡Œๅœจๅฎšไน‰ไน‹ๅ‰ๅฐฑ่ฐƒ็”จไบ†ใ€ไธ”ๆฒกๆœ‰ๅ‰ๅ‘ๅฃฐๆ˜Ž 
โ†’ `use of undeclared identifier`ใ€‚่ฟ™ไนŸๅฐ่ฏไบ† BE ็กฎๅฎž่ฟ˜ๆฒก็ผ–่ฟ‡ใ€‚ๅŠ ไธชๅ‰ๅ‘ๅฃฐๆ˜Žๆˆ–ๆŠŠๅฎšไน‰ๅ‰็งปๅณๅฏใ€‚



##########
be/src/exec/sink/writer/paimon/vpaimon_table_writer.cpp:
##########
@@ -0,0 +1,299 @@
+// 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/vpaimon_table_writer.h"
+
+#include "common/logging.h"
+#include "core/block/block.h"
+#include "exec/sink/writer/paimon/paimon_bucket.h"
+#include "runtime/runtime_state.h"
+#include "vec/data_types/data_type_factory.hpp"
+
+namespace doris {
+
+static constexpr const char* PAIMON_OUTPUT_COLUMN_NAMES_KEY = 
"doris.output_column_names";
+static constexpr char PAIMON_COLUMN_NAME_SEPARATOR = '\x01';
+
+VPaimonTableWriter::VPaimonTableWriter(const TDataSink& t_sink,
+                                       const VExprContextSPtrs& output_exprs,
+                                       std::shared_ptr<Dependency> dep,
+                                       std::shared_ptr<Dependency> fin_dep)
+        : AsyncResultWriter(output_exprs, std::move(dep), std::move(fin_dep)), 
_t_sink(t_sink) {
+    DCHECK(_t_sink.__isset.paimon_table_sink);
+}
+
+Status VPaimonTableWriter::open(RuntimeState* state, RuntimeProfile* profile) {
+    _state = state;
+    _operator_profile = profile;
+
+    // Register profile counters
+    _written_rows_counter = ADD_COUNTER(_operator_profile, "WrittenRows", 
TUnit::UNIT);
+    _written_bytes_counter = ADD_COUNTER(_operator_profile, "WrittenBytes", 
TUnit::BYTES);
+    _send_data_timer = ADD_TIMER(_operator_profile, "SendDataTime");
+    _project_timer = ADD_CHILD_TIMER(_operator_profile, "ProjectTime", 
"SendDataTime");
+    _bucket_compute_timer = ADD_CHILD_TIMER(_operator_profile, 
"BucketComputeTime", "SendDataTime");
+    _arrow_convert_timer = ADD_CHILD_TIMER(_operator_profile, 
"ArrowConvertTime", "SendDataTime");
+    _file_store_write_timer =
+            ADD_CHILD_TIMER(_operator_profile, "FileStoreWriteTime", 
"SendDataTime");
+    _open_timer = ADD_TIMER(_operator_profile, "OpenTime");
+    _close_timer = ADD_TIMER(_operator_profile, "CloseTime");
+    _prepare_commit_timer = ADD_TIMER(_operator_profile, "PrepareCommitTime");
+    _serialize_commit_messages_timer = ADD_TIMER(_operator_profile, 
"SerializeCommitMessagesTime");
+    _commit_payload_count = ADD_COUNTER(_operator_profile, 
"CommitPayloadCount", TUnit::UNIT);
+    _commit_payload_bytes_counter =
+            ADD_COUNTER(_operator_profile, "CommitPayloadBytes", TUnit::BYTES);
+    _bucket_writer_count = ADD_COUNTER(_operator_profile, "BucketWriterCount", 
TUnit::UNIT);
+
+    SCOPED_TIMER(_open_timer);
+
+    // Create the backend via factory
+    
RETURN_IF_ERROR(PaimonWriteBackendFactory::create(_t_sink.paimon_table_sink, 
&_backend));
+    RETURN_IF_ERROR(_backend->open(_t_sink.paimon_table_sink, state));
+
+    // Determine bucket routing strategy
+    const auto& paimon_sink = _t_sink.paimon_table_sink;
+    _total_buckets = paimon_sink.__isset.bucket_num ? paimon_sink.bucket_num : 
1;
+
+    // Bucket routing is enabled when:
+    // 1. num_buckets > 1 (HASH_FIXED mode)
+    // 2. Bucket key columns are specified
+    _bucket_routing_enabled = _total_buckets > 1;
+
+    if (_bucket_routing_enabled && paimon_sink.__isset.column_names) {
+        // The bucket key is determined by Paimon options (bucket-key).
+        // FE passes column names; we need the indices of bucket key columns.
+        // For now, we use the primary key columns as bucket key (Paimon 
default).
+        // In production, FE should explicitly pass bucket_key_indices.
+        //
+        // When bucket-key is explicitly configured, FE should populate
+        // TPaimonTableSink.bucket_key_indices.
+        if (paimon_sink.__isset.bucket_key_indices && 
!paimon_sink.bucket_key_indices.empty()) {
+            _bucket_key_indices = 
std::vector<int>(paimon_sink.bucket_key_indices.begin(),
+                                                   
paimon_sink.bucket_key_indices.end());
+        }
+    }
+
+    LOG(INFO) << "VPaimonTableWriter opened: table=" << paimon_sink.tb_name
+              << ", backend=" << static_cast<int>(_backend->type())
+              << ", total_buckets=" << _total_buckets
+              << ", bucket_routing=" << _bucket_routing_enabled
+              << ", bucket_key_indices=" << _bucket_key_indices.size();
+    return Status::OK();
+}
+
+Status VPaimonTableWriter::write(RuntimeState* state, Block& block) {
+    if (block.rows() == 0) {
+        return Status::OK();
+    }
+
+    SCOPED_TIMER(_send_data_timer);
+
+    // Project: apply output expressions
+    Block output_block;
+    {
+        SCOPED_TIMER(_project_timer);
+        RETURN_IF_ERROR(_projection_block(block, &output_block));
+    }
+
+    // Rename columns using the output column names from the sink config
+    const auto& paimon_sink = _t_sink.paimon_table_sink;
+    if (paimon_sink.__isset.paimon_options) {
+        auto it = 
paimon_sink.paimon_options.find(PAIMON_OUTPUT_COLUMN_NAMES_KEY);
+        if (it != paimon_sink.paimon_options.end()) {
+            auto output_names = _split_column_names(it->second);
+            if (!output_names.empty() && output_names.size() == 
output_block.columns()) {
+                for (size_t i = 0; i < output_names.size(); ++i) {
+                    output_block.get_by_position(i).name = output_names[i];
+                }
+            }
+        }
+    }
+
+    // Route by bucket and dispatch to per-bucket writers
+    return _route_by_bucket(output_block);
+}
+
+Status VPaimonTableWriter::_route_by_bucket(Block& block) {
+    // Compute bucket ids for all rows
+    std::vector<int32_t> buckets;
+    {
+        SCOPED_TIMER(_bucket_compute_timer);
+        buckets = _compute_buckets(block);
+    }
+
+    // Split block into per-bucket sub-blocks
+    auto bucket_blocks = _split_by_bucket(block, buckets);
+
+    // Write each bucket's data to the corresponding writer
+    for (auto& [bucket, sub_block] : bucket_blocks) {
+        std::unique_ptr<IPaimonWriter> writer;
+        RETURN_IF_ERROR(_get_or_create_bucket_writer(bucket, &writer));
+
+        COUNTER_UPDATE(_written_rows_counter, sub_block->rows());
+        COUNTER_UPDATE(_written_bytes_counter, sub_block->bytes());
+        _state->update_num_rows_load_total(sub_block->rows());
+        _state->update_num_bytes_load_total(sub_block->bytes());
+
+        {
+            SCOPED_TIMER(_file_store_write_timer);
+            RETURN_IF_ERROR(writer->write(_state, *sub_block));
+        }
+        _written_rows += sub_block->rows();
+        _written_bytes += sub_block->bytes();
+    }
+
+    COUNTER_SET(_bucket_writer_count, 
static_cast<int64_t>(_bucket_writers.size()));
+    return Status::OK();
+}
+
+std::vector<int32_t> VPaimonTableWriter::_compute_buckets(const Block& block) {
+    if (!_bucket_routing_enabled) {
+        return std::vector<int32_t>(block.rows(), 0);
+    }
+    // Use BE-side Paimon murmur hash for bucket assignment
+    if (!_bucket_key_indices.empty()) {
+        return paimon_compute_buckets(block, _bucket_key_indices, 
_total_buckets);
+    }
+    // Fallback: use hash of first column
+    std::vector<int> first_col = {0};
+    if (block.columns() > 0) {
+        return paimon_compute_buckets(block, first_col, _total_buckets);
+    }
+    return std::vector<int32_t>(block.rows(), 0);
+}
+
+std::unordered_map<BucketKey, std::unique_ptr<Block>> 
VPaimonTableWriter::_split_by_bucket(
+        const Block& block, const std::vector<int32_t>& buckets) {
+    std::unordered_map<BucketKey, std::unique_ptr<Block>> result;
+
+    // Group row indices by bucket
+    std::unordered_map<BucketKey, std::vector<size_t>> bucket_rows;
+    for (size_t i = 0; i < buckets.size(); ++i) {
+        bucket_rows[buckets[i]].push_back(i);
+    }
+
+    // Build sub-blocks for each bucket
+    for (auto& [bucket, row_indices] : bucket_rows) {
+        auto sub_block = Block::create_unique(block.clone_empty());
+        auto columns = std::move(*sub_block).mutate_columns();
+        for (int col = 0; col < block.columns(); ++col) {
+            const auto& src_col = block.get_by_position(col).column;
+            for (size_t idx : row_indices) {
+                columns[col]->insert_from(*src_col, idx);
+            }
+        }
+        sub_block->set_columns(std::move(columns));
+        result[bucket] = std::move(sub_block);
+    }
+    return result;
+}
+
+Status VPaimonTableWriter::_get_or_create_bucket_writer(int32_t bucket,
+                                                        
std::unique_ptr<IPaimonWriter>* writer) {
+    auto it = _bucket_writers.find(bucket);
+    if (it != _bucket_writers.end()) {
+        // Return a raw pointer to the existing writer. The caller uses this
+        // temporarily and must not store it.
+        *writer = nullptr; // caller uses the returned pointer, not stored

Review Comment:
   ๐Ÿ”ด ่ฟ™ไธช helper ็š„ๅ‡บๅ‚ `*writer` ไธคไธชๅˆ†ๆ”ฏ้ƒฝๆ‹ฟไธๅˆฐๅฏ็”จๅฏน่ฑก๏ผšๅทฒๅญ˜ๅœจๅˆ†ๆ”ฏๆ˜พๅผ็ฝฎ 
`nullptr`๏ผ›ๆ–ฐๅปบๅˆ†ๆ”ฏ๏ผˆไธ‹้ข๏ผ‰`create_writer` ๅ†™็š„ๆ˜ฏ `_bucket_writers[bucket]`๏ผŒๅŒๆ ทๆฒก่ต‹ๅ€ผ 
`*writer`ใ€‚่ฐƒ็”จๆ–น `_route_by_bucket` ้šๅŽ `writer->write(...)` ๅฟ…็„ถๅฏน nullptr ่งฃๅผ•็”จ โ†’ 
ๆฏๆฌกๅ†™ๅ…ฅ้ƒฝ crashใ€‚
   
   ่€Œไธ”็”จ `unique_ptr*` ๅšๅ‡บๅ‚่ฏญไน‰ๆœฌ่บซๅฐฑไธๅฏน๏ผšwriter ๅฝ’ `_bucket_writers` ่ฟ™ไธช map 
ๆ‰€ๆœ‰๏ผŒไธ่ƒฝๅ†ไบคๅ‡บๆ‰€ๆœ‰ๆƒใ€‚ๅบ”ๆ”นๆˆ่ฟ”ๅ›ž่ฃธๆŒ‡้’ˆ `IPaimonWriter*`๏ผˆ`*out = 
_bucket_writers[bucket].get();`๏ผ‰๏ผŒ่ฐƒ็”จๆ–น็”จ่ฃธๆŒ‡้’ˆๅ†™ใ€‚



##########
be/src/exec/sink/writer/paimon/paimon_bucket.cpp:
##########
@@ -0,0 +1,222 @@
+// 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: "

Review Comment:
   ๐ŸŸ  ่ฟ™ๆ•ดๅฅ— BE ็ซฏ bucket ่ฎก็ฎ—ๅœจ v1 ๆ—ขๅ†—ไฝ™ๅˆไธๅฎ‰ๅ…จ๏ผŒๅปบ่ฎฎ็›ดๆŽฅๅˆ ๆމใ€‚
   
   **ๅ†—ไฝ™**๏ผšJNI ่ทฏๅพ„ไธ‹ `PaimonJniWriter.writeBatch` ๅทฒ็ป็”จ `rowKeyExtractor.bucket()` ๅœจ 
Java ไพง้‡็ฎ— bucket๏ผŒ่€Œ BE ็ฎ—ๅ‡บๆฅ็š„ bucket ไปŽๆฒกไผ ็ป™ Java๏ผˆๆ‰€ๆœ‰ per-bucket writer ้ƒฝๅŒ…ๅŒไธ€ไธช Java 
ๅฏน่ฑก๏ผ‰๏ผŒBE ่ฟ™้‡Œ็š„ๅˆ†ๆกถ + ๆ‹† block ๅฎŒๅ…จๆ˜ฏ็ฉบ่ฝฌใ€‚
   
   **ไธๅฎ‰ๅ…จ**๏ผšๆ‰‹ๅทฅ้‡ๅปบ Paimon BinaryRow ๅนถไธ่ƒฝไธŽ Paimon ๅšๅˆฐๅญ—่Š‚็บงไธ€่‡ด โ€”โ€”
   - `DATEV2` / `DATETIMEV2` ๅ†™็š„ๆ˜ฏ Doris ๅ†…้ƒจ packed ่กจ็คบ๏ผŒไธๆ˜ฏ Paimon ็š„ epoch-day int / 
micros long๏ผ›
   - `DECIMAL32` / `DECIMAL64` ๆฒกๆŠŠ็ฌฆๅทๆ‰ฉๅฑ•ๅˆฐ 8 ๅญ—่Š‚ๆงฝ๏ผŒ่ดŸๆ•ฐไผš็ฎ—้”™๏ผ›
   - ่ฟ™้‡Œ string / ๅคๆ‚็ฑปๅž‹็›ดๆŽฅ `LOG(WARNING)` ไธๅ†™ไปปไฝ•ๅญ—่Š‚ โ†’ ๆ‰€ๆœ‰่กŒ่ฝๅˆฐๅŒไธ€ไธช bucketใ€‚
   
   ไธ€ๆ—ฆไปฅๅŽ็œŸๆœ‰ไบบ็”จๅˆฐ BE ็ฎ—็š„ bucket๏ผŒๅฐฑไผš PK ๅŽป้‡้”™ไนฑใ€‚Java ไพงๅทฒ็ป็ฎ—ๅฏนไบ†๏ผŒv1 ๆฒกๅฟ…่ฆไฟ็•™่ฟ™ๆก่ทฏๅพ„ใ€‚



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to