This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.0 by this push:
     new 7047fb2cfa8 branch-4.0: [fix](paimon) preserve partition metadata 
across splits (#65583)
7047fb2cfa8 is described below

commit 7047fb2cfa8fc23fe03159ca6b787b5fca06dc9a
Author: Socrates <[email protected]>
AuthorDate: Wed Jul 15 17:55:46 2026 +0800

    branch-4.0: [fix](paimon) preserve partition metadata across splits (#65583)
    
    ### What problem does this PR solve?
    
    Paimon partition metadata was generated only when runtime partition
    pruning was enabled. Some count/native/JNI splits therefore reached BE
    without stable partition values. BE also reused a partition-prune block
    across splits but inserted replacement columns, corrupting the block
    layout.
    
    ### Changes
    
    - Always attach ordered Paimon partition metadata to every data split.
    - Generate aligned `columns_from_path_keys`, `columns_from_path`, and
    `columns_from_path_is_null` arrays in FE.
    - Pass explicit null markers from `FileScanner` to Parquet/ORC readers.
    - Preserve NULL, empty string, and literal `\N` semantics.
    - Replace partition columns in the reusable prune block instead of
    inserting new columns.
    - Preserve the partition-key case behavior already present on the latest
    branch-4.0 baseline.
    - Add FE/BE unit coverage and a three-partition static `IN` regression
    case.
    
    This is a focused backport of the relevant #62821 behavior adapted to
    the pre-#62306 reader architecture.
---
 be/src/vec/exec/format/generic_reader.h            |  3 +-
 be/src/vec/exec/format/orc/vorc_reader.cpp         | 28 +++----
 be/src/vec/exec/format/orc/vorc_reader.h           |  4 +-
 .../exec/format/parquet/vparquet_group_reader.cpp  | 24 ++----
 .../exec/format/parquet/vparquet_group_reader.h    |  3 +-
 be/src/vec/exec/format/parquet/vparquet_reader.cpp |  4 +-
 be/src/vec/exec/format/parquet/vparquet_reader.h   |  3 +-
 .../exec/format/table/partition_column_filler.h    | 79 +++++++++++++++++++
 be/src/vec/exec/format/table/table_format_reader.h |  6 +-
 be/src/vec/exec/scan/file_scanner.cpp              | 79 ++++++-------------
 .../format/table/partition_column_filler_test.cpp  | 88 ++++++++++++++++++++++
 .../apache/doris/datasource/paimon/PaimonUtil.java | 10 +++
 .../datasource/paimon/source/PaimonScanNode.java   | 60 +++++++++------
 .../datasource/paimon/source/PaimonSplit.java      |  6 +-
 .../doris/datasource/paimon/PaimonUtilTest.java    | 32 ++++++++
 .../paimon/source/PaimonScanNodeTest.java          | 29 ++++++-
 ...est_paimon_runtime_filter_partition_pruning.out |  7 +-
 ..._paimon_runtime_filter_partition_pruning.groovy |  6 +-
 18 files changed, 346 insertions(+), 125 deletions(-)

diff --git a/be/src/vec/exec/format/generic_reader.h 
b/be/src/vec/exec/format/generic_reader.h
index 620112a71e7..71893fa994f 100644
--- a/be/src/vec/exec/format/generic_reader.h
+++ b/be/src/vec/exec/format/generic_reader.h
@@ -75,7 +75,8 @@ public:
     virtual Status set_fill_columns(
             const std::unordered_map<std::string, std::tuple<std::string, 
const SlotDescriptor*>>&
                     partition_columns,
-            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns) {
+            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns,
+            const std::unordered_map<std::string, bool>& 
partition_value_is_null = {}) {
         return Status::OK();
     }
 
diff --git a/be/src/vec/exec/format/orc/vorc_reader.cpp 
b/be/src/vec/exec/format/orc/vorc_reader.cpp
index 55913981255..dd83dbb599a 100644
--- a/be/src/vec/exec/format/orc/vorc_reader.cpp
+++ b/be/src/vec/exec/format/orc/vorc_reader.cpp
@@ -84,6 +84,7 @@
 #include "vec/data_types/data_type_nullable.h"
 #include "vec/data_types/data_type_struct.h"
 #include "vec/exec/format/orc/orc_file_reader.h"
+#include "vec/exec/format/table/partition_column_filler.h"
 #include "vec/exec/format/table/transactional_hive_common.h"
 #include "vec/exec/scan/file_scanner.h"
 #include "vec/exprs/vbloom_predicate.h"
@@ -1064,8 +1065,10 @@ bool OrcReader::_init_search_argument(const VExprSPtrs& 
exprs) {
 Status OrcReader::set_fill_columns(
         const std::unordered_map<std::string, std::tuple<std::string, const 
SlotDescriptor*>>&
                 partition_columns,
-        const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns) {
+        const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns,
+        const std::unordered_map<std::string, bool>& partition_value_is_null) {
     SCOPED_RAW_TIMER(&_statistics.set_fill_column_time);
+    _lazy_read_ctx.partition_value_is_null = partition_value_is_null;
 
     // std::unordered_map<column_name, std::pair<col_id, slot_id>>
     std::unordered_map<std::string, std::pair<uint32_t, int>> 
predicate_table_columns;
@@ -1383,23 +1386,12 @@ Status OrcReader::_fill_partition_columns(
         auto col_ptr = 
block->get_by_position((*_col_name_to_block_idx)[kv.first])
                                .column->assume_mutable();
         const auto& [value, slot_desc] = kv.second;
-        auto _text_serde = slot_desc->get_data_type_ptr()->get_serde();
-        Slice slice(value.data(), value.size());
-        uint64_t num_deserialized = 0;
-        if (_text_serde->deserialize_column_from_fixed_json(*col_ptr, slice, 
rows,
-                                                            &num_deserialized,
-                                                            
_text_formatOptions) != Status::OK()) {
-            return Status::InternalError("Failed to fill partition column: 
{}={}",
-                                         slot_desc->col_name(), value);
-        }
-        if (num_deserialized != rows) {
-            return Status::InternalError(
-                    "Failed to fill partition column: {}={} ."
-                    "Number of rows expected to be written : {}, number of 
rows actually "
-                    "written : "
-                    "{}",
-                    slot_desc->col_name(), value, num_deserialized, rows);
-        }
+        auto null_it = _lazy_read_ctx.partition_value_is_null.find(kv.first);
+        RETURN_IF_ERROR(fill_partition_column_from_path_value(
+                *col_ptr, *slot_desc, value, rows,
+                null_it != _lazy_read_ctx.partition_value_is_null.end(),
+                null_it != _lazy_read_ctx.partition_value_is_null.end() && 
null_it->second,
+                _text_formatOptions));
     }
     return Status::OK();
 }
diff --git a/be/src/vec/exec/format/orc/vorc_reader.h 
b/be/src/vec/exec/format/orc/vorc_reader.h
index ef2eb7976b4..418c326664b 100644
--- a/be/src/vec/exec/format/orc/vorc_reader.h
+++ b/be/src/vec/exec/format/orc/vorc_reader.h
@@ -109,6 +109,7 @@ struct LazyReadContext {
     // lazy read partition columns or all partition columns
     std::unordered_map<std::string, std::tuple<std::string, const 
SlotDescriptor*>>
             partition_columns;
+    std::unordered_map<std::string, bool> partition_value_is_null;
     std::unordered_map<std::string, VExprContextSPtr> 
predicate_missing_columns;
     // lazy read missing columns or all missing columns
     std::unordered_map<std::string, VExprContextSPtr> missing_columns;
@@ -180,7 +181,8 @@ public:
     Status set_fill_columns(
             const std::unordered_map<std::string, std::tuple<std::string, 
const SlotDescriptor*>>&
                     partition_columns,
-            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns) override;
+            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns,
+            const std::unordered_map<std::string, bool>& 
partition_value_is_null = {}) override;
 
     Status get_next_block(Block* block, size_t* read_rows, bool* eof) override;
 
diff --git a/be/src/vec/exec/format/parquet/vparquet_group_reader.cpp 
b/be/src/vec/exec/format/parquet/vparquet_group_reader.cpp
index 2cec1061218..95f6c13a575 100644
--- a/be/src/vec/exec/format/parquet/vparquet_group_reader.cpp
+++ b/be/src/vec/exec/format/parquet/vparquet_group_reader.cpp
@@ -54,6 +54,7 @@
 #include "vec/data_types/data_type_nullable.h"
 #include "vec/data_types/data_type_number.h"
 #include "vec/data_types/data_type_string.h"
+#include "vec/exec/format/table/partition_column_filler.h"
 #include "vec/exprs/vdirect_in_predicate.h"
 #include "vec/exprs/vectorized_fn_call.h"
 #include "vec/exprs/vexpr.h"
@@ -725,23 +726,12 @@ Status RowGroupReader::_fill_partition_columns(
         // obtained from block*, it is a mutable object.
         auto* col_ptr = const_cast<IColumn*>(doris_column.get());
         const auto& [value, slot_desc] = kv.second;
-        auto _text_serde = slot_desc->get_data_type_ptr()->get_serde();
-        Slice slice(value.data(), value.size());
-        uint64_t num_deserialized = 0;
-        // Be careful when reading empty rows from parquet row groups.
-        if (_text_serde->deserialize_column_from_fixed_json(*col_ptr, slice, 
rows,
-                                                            &num_deserialized,
-                                                            
_text_formatOptions) != Status::OK()) {
-            return Status::InternalError("Failed to fill partition column: 
{}={}",
-                                         slot_desc->col_name(), value);
-        }
-        if (num_deserialized != rows) {
-            return Status::InternalError(
-                    "Failed to fill partition column: {}={} ."
-                    "Number of rows expected to be written : {}, number of 
rows actually written : "
-                    "{}",
-                    slot_desc->col_name(), value, num_deserialized, rows);
-        }
+        auto null_it = _lazy_read_ctx.partition_value_is_null.find(kv.first);
+        RETURN_IF_ERROR(fill_partition_column_from_path_value(
+                *col_ptr, *slot_desc, value, rows,
+                null_it != _lazy_read_ctx.partition_value_is_null.end(),
+                null_it != _lazy_read_ctx.partition_value_is_null.end() && 
null_it->second,
+                _text_formatOptions));
     }
     return Status::OK();
 }
diff --git a/be/src/vec/exec/format/parquet/vparquet_group_reader.h 
b/be/src/vec/exec/format/parquet/vparquet_group_reader.h
index 1e5259413a3..dab92d669e9 100644
--- a/be/src/vec/exec/format/parquet/vparquet_group_reader.h
+++ b/be/src/vec/exec/format/parquet/vparquet_group_reader.h
@@ -82,9 +82,10 @@ public:
         // all conjuncts: in sql, join runtime filter, topn runtime filter.
         VExprContextSPtrs conjuncts;
 
-        // ParquetReader::set_fill_columns(xxx, xxx) will set these two members
+        // ParquetReader::set_fill_columns(xxx, xxx) will set these members
         std::unordered_map<std::string, std::tuple<std::string, const 
SlotDescriptor*>>
                 fill_partition_columns;
+        std::unordered_map<std::string, bool> partition_value_is_null;
         std::unordered_map<std::string, VExprContextSPtr> fill_missing_columns;
 
         phmap::flat_hash_map<int, 
std::vector<std::shared_ptr<ColumnPredicate>>>
diff --git a/be/src/vec/exec/format/parquet/vparquet_reader.cpp 
b/be/src/vec/exec/format/parquet/vparquet_reader.cpp
index af1958aa7a1..e559aa43871 100644
--- a/be/src/vec/exec/format/parquet/vparquet_reader.cpp
+++ b/be/src/vec/exec/format/parquet/vparquet_reader.cpp
@@ -464,8 +464,10 @@ bool ParquetReader::_type_matches(const int cid) const {
 Status ParquetReader::set_fill_columns(
         const std::unordered_map<std::string, std::tuple<std::string, const 
SlotDescriptor*>>&
                 partition_columns,
-        const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns) {
+        const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns,
+        const std::unordered_map<std::string, bool>& partition_value_is_null) {
     _lazy_read_ctx.fill_partition_columns = partition_columns;
+    _lazy_read_ctx.partition_value_is_null = partition_value_is_null;
     _lazy_read_ctx.fill_missing_columns = missing_columns;
 
     // std::unordered_map<column_name, std::pair<col_id, slot_id>>
diff --git a/be/src/vec/exec/format/parquet/vparquet_reader.h 
b/be/src/vec/exec/format/parquet/vparquet_reader.h
index c67aab46b69..537691ff9ed 100644
--- a/be/src/vec/exec/format/parquet/vparquet_reader.h
+++ b/be/src/vec/exec/format/parquet/vparquet_reader.h
@@ -164,7 +164,8 @@ public:
     Status set_fill_columns(
             const std::unordered_map<std::string, std::tuple<std::string, 
const SlotDescriptor*>>&
                     partition_columns,
-            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns) override;
+            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns,
+            const std::unordered_map<std::string, bool>& 
partition_value_is_null = {}) override;
 
     Status get_file_metadata_schema(const FieldDescriptor** ptr);
 
diff --git a/be/src/vec/exec/format/table/partition_column_filler.h 
b/be/src/vec/exec/format/table/partition_column_filler.h
new file mode 100644
index 00000000000..48e4830ff1c
--- /dev/null
+++ b/be/src/vec/exec/format/table/partition_column_filler.h
@@ -0,0 +1,79 @@
+// 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 <glog/logging.h>
+
+#include <string>
+
+#include "common/status.h"
+#include "runtime/descriptors.h"
+#include "util/slice.h"
+#include "vec/columns/column_nullable.h"
+#include "vec/common/assert_cast.h"
+#include "vec/data_types/serde/data_type_serde.h"
+
+namespace doris::vectorized {
+
+inline Status fill_partition_column_from_path_value(
+        IColumn& column, const SlotDescriptor& slot_desc, const std::string& 
value, size_t rows,
+        bool has_explicit_null_marker, bool explicit_null_marker,
+        DataTypeSerDe::FormatOptions text_format_options = {}) {
+    auto data_type = slot_desc.get_data_type_ptr();
+    auto text_serde = data_type->get_serde();
+    uint64_t num_deserialized = 0;
+
+    if (has_explicit_null_marker && explicit_null_marker) {
+        DCHECK(data_type->is_nullable());
+        column.insert_many_defaults(rows);
+        return Status::OK();
+    }
+
+    IColumn* value_column = &column;
+    DataTypeSerDeSPtr value_serde = text_serde;
+    ColumnNullable* nullable_column = nullptr;
+    // Legacy path partitions encode null as "\\N" and rely on nullable serde. 
Sources with an
+    // explicit null marker deserialize the nested value so a literal "\\N" 
remains non-null.
+    if (data_type->is_nullable() && has_explicit_null_marker) {
+        nullable_column = assert_cast<ColumnNullable*>(&column);
+        value_column = &nullable_column->get_nested_column();
+        value_serde = text_serde->get_nested_serdes()[0];
+    }
+
+    const size_t old_size = value_column->size();
+    Slice slice(value.data(), value.size());
+    Status status = value_serde->deserialize_column_from_fixed_json(
+            *value_column, slice, rows, &num_deserialized, 
text_format_options);
+    if (!status.ok()) {
+        value_column->resize(old_size);
+        return Status::InternalError("Failed to fill partition column: {}={}", 
slot_desc.col_name(),
+                                     value);
+    }
+    if (num_deserialized != rows) {
+        value_column->resize(old_size);
+        return Status::InternalError(
+                "Failed to fill partition column: {}={}. Expected rows: {}, 
actual: {}",
+                slot_desc.col_name(), value, rows, num_deserialized);
+    }
+    if (nullable_column != nullptr) {
+        nullable_column->get_null_map_column().insert_many_vals(0, rows);
+    }
+    return Status::OK();
+}
+
+} // namespace doris::vectorized
diff --git a/be/src/vec/exec/format/table/table_format_reader.h 
b/be/src/vec/exec/format/table/table_format_reader.h
index d32170c0afa..c980d754ca1 100644
--- a/be/src/vec/exec/format/table/table_format_reader.h
+++ b/be/src/vec/exec/format/table/table_format_reader.h
@@ -96,8 +96,10 @@ public:
     Status set_fill_columns(
             const std::unordered_map<std::string, std::tuple<std::string, 
const SlotDescriptor*>>&
                     partition_columns,
-            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns) final {
-        return _file_format_reader->set_fill_columns(partition_columns, 
missing_columns);
+            const std::unordered_map<std::string, VExprContextSPtr>& 
missing_columns,
+            const std::unordered_map<std::string, bool>& 
partition_value_is_null = {}) final {
+        return _file_format_reader->set_fill_columns(partition_columns, 
missing_columns,
+                                                     partition_value_is_null);
     }
 
     bool fill_all_columns() const override { return 
_file_format_reader->fill_all_columns(); }
diff --git a/be/src/vec/exec/scan/file_scanner.cpp 
b/be/src/vec/exec/scan/file_scanner.cpp
index d164112c6a9..1a770a002b9 100644
--- a/be/src/vec/exec/scan/file_scanner.cpp
+++ b/be/src/vec/exec/scan/file_scanner.cpp
@@ -70,6 +70,7 @@
 #include "vec/exec/format/table/max_compute_jni_reader.h"
 #include "vec/exec/format/table/paimon_jni_reader.h"
 #include "vec/exec/format/table/paimon_reader.h"
+#include "vec/exec/format/table/partition_column_filler.h"
 #include "vec/exec/format/table/remote_doris_reader.h"
 #include "vec/exec/format/table/transactional_hive_reader.h"
 #include "vec/exec/format/table/trino_connector_jni_reader.h"
@@ -253,33 +254,12 @@ Status 
FileScanner::_process_runtime_filters_partition_prune(bool& can_filter_al
     for (auto const& partition_col_desc : _partition_col_descs) {
         const auto& [partition_value, partition_slot_desc] = 
partition_col_desc.second;
         auto data_type = partition_slot_desc->get_data_type_ptr();
-        auto test_serde = data_type->get_serde();
         auto partition_value_column = data_type->create_column();
-        auto* col_ptr = static_cast<IColumn*>(partition_value_column.get());
-        Slice slice(partition_value.data(), partition_value.size());
-        uint64_t num_deserialized = 0;
-        DataTypeSerDe::FormatOptions options {};
-        if 
(_partition_value_is_null.contains(partition_slot_desc->col_name())) {
-            // for iceberg/paimon table
-            // NOTICE: column is always be nullable for iceberg/paimon table 
now
-            DCHECK(data_type->is_nullable());
-            test_serde = test_serde->get_nested_serdes()[0];
-            auto* null_column = assert_cast<ColumnNullable*>(col_ptr);
-            if (_partition_value_is_null[partition_slot_desc->col_name()]) {
-                null_column->insert_many_defaults(partition_value_column_size);
-            } else {
-                // If the partition value is not null, we set null map to 0 
and deserialize it normally.
-                null_column->get_null_map_column().insert_many_vals(0, 
partition_value_column_size);
-                RETURN_IF_ERROR(test_serde->deserialize_column_from_fixed_json(
-                        null_column->get_nested_column(), slice, 
partition_value_column_size,
-                        &num_deserialized, options));
-            }
-        } else {
-            // for hive/hudi table, the null value is set as "\\N"
-            // TODO: this will be unified as iceberg/paimon table in the future
-            RETURN_IF_ERROR(test_serde->deserialize_column_from_fixed_json(
-                    *col_ptr, slice, partition_value_column_size, 
&num_deserialized, options));
-        }
+        auto null_it = 
_partition_value_is_null.find(partition_slot_desc->col_name());
+        RETURN_IF_ERROR(fill_partition_column_from_path_value(
+                *partition_value_column, *partition_slot_desc, partition_value,
+                partition_value_column_size, null_it != 
_partition_value_is_null.end(),
+                null_it != _partition_value_is_null.end() && null_it->second));
 
         partition_slot_id_to_column[partition_slot_desc->id()] = 
std::move(partition_value_column);
     }
@@ -291,20 +271,9 @@ Status 
FileScanner::_process_runtime_filters_partition_prune(bool& can_filter_al
     for (auto const* slot_desc : _real_tuple_desc->slots()) {
         if (partition_slot_id_to_column.find(slot_desc->id()) !=
             partition_slot_id_to_column.end()) {
-            auto data_type = slot_desc->get_data_type_ptr();
             auto partition_value_column = 
std::move(partition_slot_id_to_column[slot_desc->id()]);
-            if (data_type->is_nullable()) {
-                _runtime_filter_partition_prune_block.insert(
-                        index, ColumnWithTypeAndName(
-                                       ColumnNullable::create(
-                                               
std::move(partition_value_column),
-                                               
ColumnUInt8::create(partition_value_column_size, 0)),
-                                       data_type, slot_desc->col_name()));
-            } else {
-                _runtime_filter_partition_prune_block.insert(
-                        index, 
ColumnWithTypeAndName(std::move(partition_value_column), data_type,
-                                                     slot_desc->col_name()));
-            }
+            _runtime_filter_partition_prune_block.replace_by_position(
+                    index, std::move(partition_value_column));
             if (index == 0) {
                 first_column_filled = true;
             }
@@ -638,22 +607,10 @@ Status FileScanner::_fill_columns_from_path(size_t rows) {
         // _src_block_ptr points to a mutable block created by this class 
itself, so const_cast can be used here.
         IColumn* col_ptr = const_cast<IColumn*>(doris_column.get());
         auto& [value, slot_desc] = kv.second;
-        auto _text_serde = slot_desc->get_data_type_ptr()->get_serde();
-        Slice slice(value.data(), value.size());
-        uint64_t num_deserialized = 0;
-        if (_text_serde->deserialize_column_from_fixed_json(*col_ptr, slice, 
rows,
-                                                            &num_deserialized,
-                                                            
_text_formatOptions) != Status::OK()) {
-            return Status::InternalError("Failed to fill partition column: 
{}={}",
-                                         slot_desc->col_name(), value);
-        }
-        if (num_deserialized != rows) {
-            return Status::InternalError(
-                    "Failed to fill partition column: {}={} ."
-                    "Number of rows expected to be written : {}, number of 
rows actually written : "
-                    "{}",
-                    slot_desc->col_name(), value, num_deserialized, rows);
-        }
+        auto null_it = _partition_value_is_null.find(kv.first);
+        RETURN_IF_ERROR(fill_partition_column_from_path_value(
+                *col_ptr, *slot_desc, value, rows, null_it != 
_partition_value_is_null.end(),
+                null_it != _partition_value_is_null.end() && null_it->second, 
_text_formatOptions));
     }
     return Status::OK();
 }
@@ -1440,7 +1397,8 @@ Status FileScanner::_set_fill_or_truncate_columns(bool 
need_to_get_parsed_schema
 
     RETURN_IF_ERROR(_generate_missing_columns());
     if (_fill_partition_from_path) {
-        RETURN_IF_ERROR(_cur_reader->set_fill_columns(_partition_col_descs, 
_missing_col_descs));
+        RETURN_IF_ERROR(_cur_reader->set_fill_columns(_partition_col_descs, 
_missing_col_descs,
+                                                      
_partition_value_is_null));
     } else {
         // If the partition columns are not from path, we only fill the 
missing columns.
         RETURN_IF_ERROR(_cur_reader->set_fill_columns({}, _missing_col_descs));
@@ -1578,6 +1536,9 @@ Status FileScanner::_generate_partition_columns() {
     _partition_value_is_null.clear();
     const TFileRangeDesc& range = _current_range;
     if (range.__isset.columns_from_path && !_partition_slot_descs.empty()) {
+        if (range.__isset.columns_from_path_is_null) {
+            DORIS_CHECK(range.columns_from_path_is_null.size() == 
range.columns_from_path.size());
+        }
         for (const auto& slot_desc : _partition_slot_descs) {
             if (slot_desc) {
                 auto it = _partition_slot_index_map.find(slot_desc->id());
@@ -1585,6 +1546,12 @@ Status FileScanner::_generate_partition_columns() {
                     return Status::InternalError("Unknown source slot 
descriptor, slot_id={}",
                                                  slot_desc->id());
                 }
+                if (it->second < 0 ||
+                    static_cast<size_t>(it->second) >= 
range.columns_from_path.size()) {
+                    return Status::InternalError(
+                            "Invalid partition value index {}, value count {} 
for column {}",
+                            it->second, range.columns_from_path.size(), 
slot_desc->col_name());
+                }
                 const std::string& column_from_path = 
range.columns_from_path[it->second];
                 _partition_col_descs.emplace(slot_desc->col_name(),
                                              std::make_tuple(column_from_path, 
slot_desc));
diff --git a/be/test/vec/exec/format/table/partition_column_filler_test.cpp 
b/be/test/vec/exec/format/table/partition_column_filler_test.cpp
new file mode 100644
index 00000000000..130a78c54bf
--- /dev/null
+++ b/be/test/vec/exec/format/table/partition_column_filler_test.cpp
@@ -0,0 +1,88 @@
+// 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 "vec/exec/format/table/partition_column_filler.h"
+
+#include <gtest/gtest.h>
+
+#include <memory>
+
+#include "vec/columns/column_nullable.h"
+#include "vec/common/assert_cast.h"
+#include "vec/data_types/data_type_nullable.h"
+#include "vec/data_types/data_type_number.h"
+#include "vec/data_types/data_type_string.h"
+
+namespace doris::vectorized {
+
+TEST(PartitionColumnFillerTest, DistinguishLegacyAndExplicitNullMarkers) {
+    SlotDescriptor string_slot;
+    string_slot._type = make_nullable(std::make_shared<DataTypeString>());
+    string_slot._col_name = "part_col";
+
+    auto legacy_null_column = string_slot.get_empty_mutable_column();
+    ASSERT_TRUE(fill_partition_column_from_path_value(*legacy_null_column, 
string_slot, "\\N", 2,
+                                                      false, false)
+                        .ok());
+    const auto& legacy_nullable = assert_cast<const 
ColumnNullable&>(*legacy_null_column);
+    ASSERT_EQ(legacy_nullable.size(), 2);
+    EXPECT_TRUE(legacy_nullable.is_null_at(0));
+    EXPECT_TRUE(legacy_nullable.is_null_at(1));
+
+    auto literal_null_marker_column = string_slot.get_empty_mutable_column();
+    
ASSERT_TRUE(fill_partition_column_from_path_value(*literal_null_marker_column, 
string_slot,
+                                                      "\\N", 1, true, false)
+                        .ok());
+    const auto& literal_nullable = assert_cast<const 
ColumnNullable&>(*literal_null_marker_column);
+    ASSERT_EQ(literal_nullable.size(), 1);
+    EXPECT_FALSE(literal_nullable.is_null_at(0));
+    EXPECT_EQ(literal_nullable.get_nested_column().get_data_at(0).to_string(), 
"\\N");
+
+    auto empty_string_column = string_slot.get_empty_mutable_column();
+    ASSERT_TRUE(fill_partition_column_from_path_value(*empty_string_column, 
string_slot, "", 1,
+                                                      true, false)
+                        .ok());
+    const auto& empty_string_nullable = assert_cast<const 
ColumnNullable&>(*empty_string_column);
+    ASSERT_EQ(empty_string_nullable.size(), 1);
+    EXPECT_FALSE(empty_string_nullable.is_null_at(0));
+    
EXPECT_EQ(empty_string_nullable.get_nested_column().get_data_at(0).to_string(), 
"");
+
+    SlotDescriptor int_slot;
+    int_slot._type = make_nullable(std::make_shared<DataTypeInt32>());
+    int_slot._col_name = "int_part_col";
+    auto explicit_null_column = int_slot.get_empty_mutable_column();
+    ASSERT_TRUE(fill_partition_column_from_path_value(*explicit_null_column, 
int_slot, "", 1, true,
+                                                      true)
+                        .ok());
+    const auto& explicit_nullable = assert_cast<const 
ColumnNullable&>(*explicit_null_column);
+    ASSERT_EQ(explicit_nullable.size(), 1);
+    EXPECT_TRUE(explicit_nullable.is_null_at(0));
+}
+
+TEST(PartitionColumnFillerTest, RestoreColumnAfterDeserializeFailure) {
+    SlotDescriptor int_slot;
+    int_slot._type = std::make_shared<DataTypeInt32>();
+    int_slot._col_name = "int_part_col";
+    auto column = int_slot.get_empty_mutable_column();
+
+    auto status =
+            fill_partition_column_from_path_value(*column, int_slot, 
"not_an_int", 1, false, false);
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(column->size(), 0);
+}
+
+} // namespace doris::vectorized
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
index 3239035f20d..3a87bb05274 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
@@ -487,6 +487,16 @@ public class PaimonUtil {
                     return null;
                 }
                 return value.toString();
+            case FLOAT:
+                if (value == null) {
+                    return null;
+                }
+                return Float.toString((Float) value);
+            case DOUBLE:
+                if (value == null) {
+                    return null;
+                }
+                return Double.toString((Double) value);
             // case binary:
             // case varbinary: should not supported, because if return string 
with utf8,
             // the data maybe be corrupted
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
index 5b24ddfccde..d93e6f58323 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
@@ -51,6 +51,7 @@ import org.apache.doris.thrift.TPushAggOp;
 import org.apache.doris.thrift.TTableFormatFileDesc;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.paimon.data.BinaryRow;
@@ -228,6 +229,40 @@ public class PaimonScanNode extends FileQueryScanNode {
         }
     }
 
+    private List<String> getOrderedPathPartitionKeys() {
+        if (source == null) {
+            return Collections.emptyList();
+        }
+        return source.getPaimonTable().partitionKeys();
+    }
+
+    @VisibleForTesting
+    void setPartitionValues(TFileRangeDesc rangeDesc, Map<String, String> 
partitionValues) {
+        rangeDesc.unsetColumnsFromPathKeys();
+        rangeDesc.unsetColumnsFromPath();
+        rangeDesc.unsetColumnsFromPathIsNull();
+
+        List<String> orderedPartitionKeys = getOrderedPathPartitionKeys();
+        if (orderedPartitionKeys.isEmpty()) {
+            return;
+        }
+        Preconditions.checkState(partitionValues != null,
+                "Missing partition values for Paimon partitioned table");
+
+        List<String> fromPathValues = new 
ArrayList<>(orderedPartitionKeys.size());
+        List<Boolean> fromPathIsNull = new 
ArrayList<>(orderedPartitionKeys.size());
+        for (String partitionKey : orderedPartitionKeys) {
+            Preconditions.checkState(partitionValues.containsKey(partitionKey),
+                    "Missing partition value for Paimon partition key: %s", 
partitionKey);
+            String partitionValue = partitionValues.get(partitionKey);
+            fromPathValues.add(partitionValue == null ? "" : partitionValue);
+            fromPathIsNull.add(partitionValue == null);
+        }
+        rangeDesc.setColumnsFromPathKeys(orderedPartitionKeys);
+        rangeDesc.setColumnsFromPath(fromPathValues);
+        rangeDesc.setColumnsFromPathIsNull(fromPathIsNull);
+    }
+
     private void setPaimonParams(TFileRangeDesc rangeDesc, PaimonSplit 
paimonSplit) {
         TTableFormatFileDesc tableFormatFileDesc = new TTableFormatFileDesc();
         
tableFormatFileDesc.setTableFormatType(paimonSplit.getTableFormatType().value());
@@ -278,20 +313,7 @@ public class PaimonScanNode extends FileQueryScanNode {
             fileDesc.setPaimonOptions(backendPaimonOptions);
         }
         tableFormatFileDesc.setPaimonParams(fileDesc);
-        Map<String, String> partitionValues = 
paimonSplit.getPaimonPartitionValues();
-        if (partitionValues != null) {
-            List<String> fromPathKeys = new ArrayList<>();
-            List<String> fromPathValues = new ArrayList<>();
-            List<Boolean> fromPathIsNull = new ArrayList<>();
-            for (Map.Entry<String, String> entry : partitionValues.entrySet()) 
{
-                fromPathKeys.add(entry.getKey());
-                fromPathValues.add(entry.getValue() != null ? entry.getValue() 
: "");
-                fromPathIsNull.add(entry.getValue() == null);
-            }
-            rangeDesc.setColumnsFromPathKeys(fromPathKeys);
-            rangeDesc.setColumnsFromPath(fromPathValues);
-            rangeDesc.setColumnsFromPathIsNull(fromPathIsNull);
-        }
+        setPartitionValues(rangeDesc, paimonSplit.getPaimonPartitionValues());
         rangeDesc.setTableFormatParams(tableFormatFileDesc);
     }
 
@@ -340,6 +362,7 @@ public class PaimonScanNode extends FileQueryScanNode {
         // partition data.
         // And for counting the number of selected partitions for this paimon 
table.
         Map<BinaryRow, Map<String, String>> partitionInfoMaps = new 
HashMap<>();
+        boolean needPartitionMetadata = 
!getOrderedPathPartitionKeys().isEmpty();
         // if applyCountPushdown is true, we can't split the DataSplit
         boolean hasDeterminedTargetFileSplitSize = false;
         long targetFileSplitSize = 0;
@@ -349,9 +372,7 @@ public class PaimonScanNode extends FileQueryScanNode {
 
             BinaryRow partitionValue = dataSplit.partition();
             Map<String, String> partitionInfoMap = null;
-            if (sessionVariable.isEnableRuntimeFilterPartitionPrune()) {
-                // If the partition value is not in the map, we need to 
calculate the partition
-                // info map and store it in the map.
+            if (needPartitionMetadata) {
                 partitionInfoMap = 
partitionInfoMaps.computeIfAbsent(partitionValue, k -> {
                     return PaimonUtil.getPartitionInfoMap(
                             source.getPaimonTable(), partitionValue, 
sessionVariable.getTimeZone());
@@ -593,10 +614,7 @@ public class PaimonScanNode extends FileQueryScanNode {
 
     @Override
     public List<String> getPathPartitionKeys() throws DdlException, 
MetaNotFoundException {
-        // return new ArrayList<>(source.getPaimonTable().partitionKeys());
-        // Paimon is not aware of partitions and bypasses some existing logic 
by
-        // returning an empty list
-        return new ArrayList<>();
+        return getOrderedPathPartitionKeys();
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonSplit.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonSplit.java
index 00700c3827f..52c2fa8e7e9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonSplit.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonSplit.java
@@ -26,6 +26,7 @@ import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.table.source.DeletionFile;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -41,7 +42,7 @@ public class PaimonSplit extends FileSplit {
     private Map<String, String> paimonPartitionValues = null;
 
     public PaimonSplit(DataSplit split) {
-        super(DUMMY_PATH, 0, 0, 0, 0, null, null);
+        super(DUMMY_PATH, 0, 0, 0, 0, null, Collections.emptyList());
         this.split = split;
         this.tableFormatType = TableFormatType.PAIMON;
 
@@ -53,7 +54,8 @@ public class PaimonSplit extends FileSplit {
 
     private PaimonSplit(LocationPath file, long start, long length, long 
fileLength, long modificationTime,
             String[] hosts, List<String> partitionList) {
-        super(file, start, length, fileLength, modificationTime, hosts, 
partitionList);
+        super(file, start, length, fileLength, modificationTime, hosts,
+                partitionList == null ? Collections.emptyList() : 
partitionList);
         this.tableFormatType = TableFormatType.PAIMON;
         this.selfSplitWeight = length;
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
index 046b38e311b..cbddcba24ac 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
@@ -21,6 +21,7 @@ import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.Type;
 
 import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.types.CharType;
@@ -32,6 +33,7 @@ import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -76,4 +78,34 @@ public class PaimonUtilTest {
         Assert.assertFalse(partitionInfoMap.containsKey("dt"));
         Assert.assertEquals("2026-05-26", partitionInfoMap.get("Dt"));
     }
+
+    @Test
+    public void testGetPartitionInfoMapSupportsFloatingPointPartitions() {
+        DataField floatPartition = DataTypes.FIELD(0, "float_partition", 
DataTypes.FLOAT());
+        DataField doublePartition = DataTypes.FIELD(1, "double_partition", 
DataTypes.DOUBLE());
+        Table table = Mockito.mock(Table.class);
+        Mockito.when(table.name()).thenReturn("mock_table");
+        
Mockito.when(table.partitionKeys()).thenReturn(Arrays.asList("float_partition", 
"double_partition"));
+        Mockito.when(table.rowType()).thenReturn(DataTypes.ROW(floatPartition, 
doublePartition));
+
+        float floatValue = Math.nextUp(0.1F);
+        double doubleValue = Math.nextUp(0.1D);
+        BinaryRow partitionValues = new BinaryRow(2);
+        BinaryRowWriter writer = new BinaryRowWriter(partitionValues);
+        writer.writeFloat(0, floatValue);
+        writer.writeDouble(1, doubleValue);
+        writer.complete();
+
+        Map<String, String> partitionInfoMap = PaimonUtil.getPartitionInfoMap(
+                table, partitionValues, "UTC");
+
+        String serializedFloat = partitionInfoMap.get("float_partition");
+        String serializedDouble = partitionInfoMap.get("double_partition");
+        Assert.assertEquals(Float.toString(floatValue), serializedFloat);
+        Assert.assertEquals(Double.toString(doubleValue), serializedDouble);
+        Assert.assertEquals(Float.floatToIntBits(floatValue),
+                Float.floatToIntBits(Float.parseFloat(serializedFloat)));
+        Assert.assertEquals(Double.doubleToLongBits(doubleValue),
+                Double.doubleToLongBits(Double.parseDouble(serializedDouble)));
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
index 45d8c8909a4..d2d5a8467dc 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/source/PaimonScanNodeTest.java
@@ -29,10 +29,12 @@ import 
org.apache.doris.datasource.paimon.PaimonFileExternalCatalog;
 import org.apache.doris.planner.PlanNodeId;
 import org.apache.doris.planner.ScanContext;
 import org.apache.doris.qe.SessionVariable;
+import org.apache.doris.thrift.TFileRangeDesc;
 
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.stats.SimpleStats;
+import org.apache.paimon.table.Table;
 import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.table.source.RawFile;
 import org.junit.Assert;
@@ -66,7 +68,11 @@ public class PaimonScanNodeTest {
         TupleDescriptor desc = new TupleDescriptor(new TupleId(3));
         PaimonScanNode paimonScanNode = new PaimonScanNode(new PlanNodeId(1), 
desc, false, sv, ScanContext.EMPTY);
 
-        paimonScanNode.setSource(new PaimonSource());
+        PaimonSource source = Mockito.spy(new PaimonSource());
+        Table paimonTable = Mockito.mock(Table.class);
+        Mockito.doReturn(paimonTable).when(source).getPaimonTable();
+        
Mockito.when(paimonTable.partitionKeys()).thenReturn(Collections.emptyList());
+        paimonScanNode.setSource(source);
 
         DataFileMeta dfm1 = DataFileMeta.forAppend("f1.parquet", 64 * 1024 * 
1024, 1, SimpleStats.EMPTY_STATS, 1, 1, 1,
                 Collections.emptyList(), null, null, null, null);
@@ -473,6 +479,27 @@ public class PaimonScanNodeTest {
         Assert.assertEquals(-1, PaimonScanNode.getFieldIndex(fieldNames, 
"missing_col"));
     }
 
+    @Test
+    public void testSetPartitionValuesBuildsAlignedMetadata() {
+        PaimonScanNode node = new PaimonScanNode(new PlanNodeId(0), new 
TupleDescriptor(new TupleId(0)),
+                false, sv, ScanContext.EMPTY);
+        PaimonSource source = Mockito.mock(PaimonSource.class);
+        Table paimonTable = Mockito.mock(Table.class);
+        Mockito.when(source.getPaimonTable()).thenReturn(paimonTable);
+        
Mockito.when(paimonTable.partitionKeys()).thenReturn(Arrays.asList("Region", 
"Dt"));
+        node.setSource(source);
+
+        Map<String, String> partitionValues = new HashMap<>();
+        partitionValues.put("Dt", null);
+        partitionValues.put("Region", "cn");
+        TFileRangeDesc rangeDesc = new TFileRangeDesc();
+        node.setPartitionValues(rangeDesc, partitionValues);
+
+        Assert.assertEquals(Arrays.asList("Region", "Dt"), 
rangeDesc.getColumnsFromPathKeys());
+        Assert.assertEquals(Arrays.asList("cn", ""), 
rangeDesc.getColumnsFromPath());
+        Assert.assertEquals(Arrays.asList(false, true), 
rangeDesc.getColumnsFromPathIsNull());
+    }
+
     private void mockJniReader(PaimonScanNode spyNode) {
         
Mockito.doReturn(false).when(spyNode).supportNativeReader(ArgumentMatchers.any(Optional.class));
     }
diff --git 
a/regression-test/data/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.out
 
b/regression-test/data/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.out
index 738d40fabe0..7c4e120d506 100644
--- 
a/regression-test/data/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.out
+++ 
b/regression-test/data/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.out
@@ -32,6 +32,9 @@
 -- !runtime_filter_partition_pruning_string_in_null --
 2
 
+-- !static_partition_pruning_string_in --
+6
+
 -- !runtime_filter_partition_pruning_date1 --
 1
 
@@ -128,6 +131,9 @@
 -- !runtime_filter_partition_pruning_string_in_null --
 2
 
+-- !static_partition_pruning_string_in --
+6
+
 -- !runtime_filter_partition_pruning_date1 --
 1
 
@@ -190,4 +196,3 @@
 
 -- !null_partition_4 --
 1      \N      100.0
-
diff --git 
a/regression-test/suites/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.groovy
 
b/regression-test/suites/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.groovy
index 325e9f242d0..e8a3634a2e5 100644
--- 
a/regression-test/suites/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.groovy
+++ 
b/regression-test/suites/external_table_p0/paimon/test_paimon_runtime_filter_partition_pruning.groovy
@@ -131,6 +131,10 @@ suite("test_paimon_runtime_filter_partition_pruning", 
"p0,external,doris,externa
                     (select partition_key from string_partitioned
                     order by id desc limit 2);
             """
+            qt_static_partition_pruning_string_in """
+                select count(*) from string_partitioned
+                where partition_key in ('North America', 'Europe', 'Asia');
+            """
             qt_runtime_filter_partition_pruning_date1 """
                 select count(*) from date_partitioned where partition_key =
                     (select partition_key from date_partitioned
@@ -267,5 +271,3 @@ suite("test_paimon_runtime_filter_partition_pruning", 
"p0,external,doris,externa
         }
     }
 }
-
-


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


Reply via email to