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 54e9da0b631 [branch-4.0](cherry-pick) Backport Paimon and Iceberg 
fixes (#65397)
54e9da0b631 is described below

commit 54e9da0b631ba8e77510d7b1704319d4bc58fa56
Author: Gabriel <[email protected]>
AuthorDate: Tue Jul 14 15:23:26 2026 +0800

    [branch-4.0](cherry-pick) Backport Paimon and Iceberg fixes (#65397)
    
    ## Proposed changes
    
    Backport the following PRs to `branch-4.0` in order:
    
    - #65054
    - #65147
    - #65332
    - #65354
    - #65094
    
    This includes task executor scan handle validation, S3 TVF regression
    isolation, Paimon JNI IOManager/profile improvements, and mixed-case
    external table column handling for Iceberg/Paimon.
    
    ## Notes
    
    - PR #65094 was adapted for `branch-4.0`; the final JDBC catalog
    regression expectation commit only touched a test file that is already
    deleted on this branch, so it was skipped as empty after conflict
    resolution.
    - The Iceberg sort-order fragment from #65094 was not applied because
    `SortFieldInfo` and the corresponding create-table sort-order path are
    not present on `branch-4.0`.
    
    ## Validation
    
    - `git diff --check origin/branch-4.0..HEAD`
    
    Cherry-picked from apache/doris#65054, apache/doris#65147,
    apache/doris#65332, apache/doris#65354, and apache/doris#65094.
---
 .../time_sharing/time_sharing_task_executor.cpp    |  28 +-
 be/src/vec/exec/format/table/paimon_jni_reader.cpp |  24 ++
 be/src/vec/exec/format/table/paimon_jni_reader.h   |   2 +
 be/src/vec/exec/jni_connector.cpp                  |  36 ++-
 be/src/vec/exec/scan/scanner_scheduler.h           |  22 +-
 .../time_sharing_task_executor_test.cpp            |  83 ++++-
 .../org/apache/doris/paimon/PaimonJniScanner.java  | 359 ++++++++++++++++++++-
 .../doris/paimon/PaimonSysTableJniScanner.java     |   8 +-
 .../apache/doris/paimon/PaimonJniScannerTest.java  | 243 ++++++++++++++
 .../datasource/iceberg/DorisTypeToIcebergType.java |  13 +-
 .../datasource/iceberg/IcebergMetadataOps.java     |   4 +-
 .../doris/datasource/iceberg/IcebergUtils.java     |  26 +-
 .../datasource/paimon/PaimonExternalTable.java     |   2 +-
 .../apache/doris/datasource/paimon/PaimonUtil.java |   2 +-
 .../paimon/source/PaimonPredicateConverter.java    |  15 +-
 .../datasource/paimon/source/PaimonScanNode.java   |  81 ++++-
 .../tablefunction/PaimonTableValuedFunction.java   |  35 +-
 .../datasource/iceberg/CreateIcebergTableTest.java |  22 ++
 .../doris/datasource/iceberg/IcebergUtilsTest.java |  13 +
 .../doris/datasource/paimon/PaimonUtilTest.java    |  40 +++
 .../paimon/source/PaimonScanNodeTest.java          |  65 ++++
 .../iceberg/test_iceberg_invaild_avro_name.out     |   5 +-
 .../paimon/test_paimon_catalog.groovy              |   3 +-
 23 files changed, 1071 insertions(+), 60 deletions(-)

diff --git 
a/be/src/vec/exec/executor/time_sharing/time_sharing_task_executor.cpp 
b/be/src/vec/exec/executor/time_sharing/time_sharing_task_executor.cpp
index 4459f20037b..876645927fa 100644
--- a/be/src/vec/exec/executor/time_sharing/time_sharing_task_executor.cpp
+++ b/be/src/vec/exec/executor/time_sharing/time_sharing_task_executor.cpp
@@ -58,6 +58,24 @@ 
DEFINE_COUNTER_METRIC_PROTOTYPE_2ARG(thread_pool_task_wait_worker_time_ns_total,
                                      MetricUnit::NANOSECONDS);
 DEFINE_COUNTER_METRIC_PROTOTYPE_2ARG(thread_pool_task_wait_worker_count_total, 
MetricUnit::NOUNIT);
 
+namespace {
+
+Result<std::shared_ptr<TimeSharingTaskHandle>> get_time_sharing_task_handle(
+        const std::shared_ptr<TaskHandle>& task_handle, const char* operation) 
{
+    if (task_handle == nullptr) {
+        return ResultError(Status::InternalError("{} got null task handle", 
operation));
+    }
+
+    auto handle = 
std::dynamic_pointer_cast<TimeSharingTaskHandle>(task_handle);
+    if (handle == nullptr) {
+        return ResultError(Status::InternalError("{} got invalid task handle 
type, task id: {}",
+                                                 operation, 
task_handle->task_id().to_string()));
+    }
+    return handle;
+}
+
+} // namespace
+
 SplitThreadPoolToken::SplitThreadPoolToken(TimeSharingTaskExecutor* pool,
                                            
TimeSharingTaskExecutor::ExecutionMode mode,
                                            std::shared_ptr<SplitQueue> 
split_queue,
@@ -755,7 +773,7 @@ Status TimeSharingTaskExecutor::add_task(const TaskId& 
task_id,
 }
 
 Status TimeSharingTaskExecutor::remove_task(std::shared_ptr<TaskHandle> 
task_handle) {
-    auto handle = 
std::dynamic_pointer_cast<TimeSharingTaskHandle>(task_handle);
+    auto handle = DORIS_TRY(get_time_sharing_task_handle(task_handle, 
"remove_task"));
     std::vector<std::shared_ptr<PrioritizedSplitRunner>> splits_to_destroy;
 
     {
@@ -818,7 +836,11 @@ Result<std::vector<SharedListenableFuture<Void>>> 
TimeSharingTaskExecutor::enque
         }
     }};
     std::vector<SharedListenableFuture<Void>> finished_futures;
-    auto handle = 
std::dynamic_pointer_cast<TimeSharingTaskHandle>(task_handle);
+    auto handle_result = get_time_sharing_task_handle(task_handle, 
"enqueue_splits");
+    if (!handle_result.has_value()) {
+        return ResultError(handle_result.error());
+    }
+    auto handle = handle_result.value();
     {
         std::unique_lock<std::mutex> lock(_mutex);
         for (const auto& task_split : splits) {
@@ -851,7 +873,7 @@ Result<std::vector<SharedListenableFuture<Void>>> 
TimeSharingTaskExecutor::enque
 Status TimeSharingTaskExecutor::re_enqueue_split(std::shared_ptr<TaskHandle> 
task_handle,
                                                  bool intermediate,
                                                  const 
std::shared_ptr<SplitRunner>& split) {
-    auto handle = 
std::dynamic_pointer_cast<TimeSharingTaskHandle>(task_handle);
+    auto handle = DORIS_TRY(get_time_sharing_task_handle(task_handle, 
"re_enqueue_split"));
     std::shared_ptr<PrioritizedSplitRunner> prioritized_split =
             handle->get_split(split, intermediate);
     prioritized_split->reset_level_priority();
diff --git a/be/src/vec/exec/format/table/paimon_jni_reader.cpp 
b/be/src/vec/exec/format/table/paimon_jni_reader.cpp
index 3c9afe93eb3..c8cd4b30910 100644
--- a/be/src/vec/exec/format/table/paimon_jni_reader.cpp
+++ b/be/src/vec/exec/format/table/paimon_jni_reader.cpp
@@ -18,10 +18,14 @@
 #include "paimon_jni_reader.h"
 
 #include <map>
+#include <string_view>
+#include <vector>
 
 #include "runtime/descriptors.h"
+#include "runtime/exec_env.h"
 #include "runtime/runtime_state.h"
 #include "runtime/types.h"
+#include "util/string_util.h"
 #include "vec/core/block.h"
 #include "vec/core/types.h"
 namespace doris {
@@ -35,8 +39,14 @@ class Block;
 namespace doris::vectorized {
 #include "common/compile_check_begin.h"
 
+namespace {
+constexpr std::string_view PAIMON_JNI_SCANNER_IO_TMP_DIR = 
"paimon_jni_scanner_io_tmp";
+} // namespace
+
 const std::string PaimonJniReader::PAIMON_OPTION_PREFIX = "paimon.";
 const std::string PaimonJniReader::HADOOP_OPTION_PREFIX = "hadoop.";
+const std::string PaimonJniReader::DORIS_ENABLE_JNI_IO_MANAGER = 
"doris.enable_jni_io_manager";
+const std::string PaimonJniReader::DORIS_JNI_IO_MANAGER_TMP_DIR = 
"doris.jni_io_manager.tmp_dir";
 
 PaimonJniReader::PaimonJniReader(const std::vector<SlotDescriptor*>& 
file_slot_descs,
                                  RuntimeState* state, RuntimeProfile* profile,
@@ -73,6 +83,20 @@ PaimonJniReader::PaimonJniReader(const 
std::vector<SlotDescriptor*>& file_slot_d
     for (const auto& kv : 
range.table_format_params.paimon_params.paimon_options) {
         params[PAIMON_OPTION_PREFIX + kv.first] = kv.second;
     }
+    const std::string enable_io_manager_key = PAIMON_OPTION_PREFIX + 
DORIS_ENABLE_JNI_IO_MANAGER;
+    const std::string io_manager_tmp_dir_key = PAIMON_OPTION_PREFIX + 
DORIS_JNI_IO_MANAGER_TMP_DIR;
+    auto enable_io_manager_it = params.find(enable_io_manager_key);
+    if (enable_io_manager_it != params.end() && 
iequal(enable_io_manager_it->second, "true") &&
+        params.find(io_manager_tmp_dir_key) == params.end()) {
+        std::vector<std::string> tmp_dirs;
+        for (const auto& store_path : state->exec_env()->store_paths()) {
+            tmp_dirs.push_back(store_path.path + "/" + 
std::string(PAIMON_JNI_SCANNER_IO_TMP_DIR));
+        }
+        DORIS_CHECK(!tmp_dirs.empty());
+        // Paimon's IOManager creates and later removes its own paimon-* child
+        // directory under these Doris storage-root scoped parent directories.
+        params[io_manager_tmp_dir_key] = join(tmp_dirs, ":");
+    }
     // Prefer hadoop conf from scan node level (range_params->properties) over 
split level
     // to avoid redundant configuration in each split
     if (range_params->__isset.properties && !range_params->properties.empty()) 
{
diff --git a/be/src/vec/exec/format/table/paimon_jni_reader.h 
b/be/src/vec/exec/format/table/paimon_jni_reader.h
index 81b5bd68d29..78c17f9f2db 100644
--- a/be/src/vec/exec/format/table/paimon_jni_reader.h
+++ b/be/src/vec/exec/format/table/paimon_jni_reader.h
@@ -50,6 +50,8 @@ class PaimonJniReader : public JniReader {
 public:
     static const std::string PAIMON_OPTION_PREFIX;
     static const std::string HADOOP_OPTION_PREFIX;
+    static const std::string DORIS_ENABLE_JNI_IO_MANAGER;
+    static const std::string DORIS_JNI_IO_MANAGER_TMP_DIR;
     PaimonJniReader(const std::vector<SlotDescriptor*>& file_slot_descs, 
RuntimeState* state,
                     RuntimeProfile* profile, const TFileRangeDesc& range,
                     const TFileScanRangeParams* range_params);
diff --git a/be/src/vec/exec/jni_connector.cpp 
b/be/src/vec/exec/jni_connector.cpp
index 2cad5967e86..df7f4f9e0ec 100644
--- a/be/src/vec/exec/jni_connector.cpp
+++ b/be/src/vec/exec/jni_connector.cpp
@@ -869,6 +869,9 @@ void JniConnector::_collect_profile_before_close() {
             return;
         }
 
+        const auto update_peak = [](int64_t previous, int64_t current) {
+            return current > previous;
+        };
         for (const auto& metric : statistics_result) {
             std::vector<std::string> type_and_name = split(metric.first, ":");
             if (type_and_name.size() != 2) {
@@ -876,22 +879,49 @@ void JniConnector::_collect_profile_before_close() {
                              << "'metricType:metricName'";
                 continue;
             }
-            long metric_value = std::stol(metric.second);
+            int64_t metric_value = std::stoll(metric.second);
             RuntimeProfile::Counter* scanner_counter;
             if (type_and_name[0] == "timer") {
                 scanner_counter =
                         ADD_CHILD_TIMER(_profile, type_and_name[1], 
_connector_name.c_str());
+                COUNTER_UPDATE(scanner_counter, metric_value);
             } else if (type_and_name[0] == "counter") {
                 scanner_counter = ADD_CHILD_COUNTER(_profile, 
type_and_name[1], TUnit::UNIT,
                                                     _connector_name.c_str());
+                COUNTER_UPDATE(scanner_counter, metric_value);
             } else if (type_and_name[0] == "bytes") {
                 scanner_counter = ADD_CHILD_COUNTER(_profile, 
type_and_name[1], TUnit::BYTES,
                                                     _connector_name.c_str());
+                COUNTER_UPDATE(scanner_counter, metric_value);
+            } else if (type_and_name[0] == "timer_gauge") {
+                scanner_counter =
+                        ADD_CHILD_TIMER(_profile, type_and_name[1], 
_connector_name.c_str());
+                COUNTER_SET(scanner_counter, metric_value);
+            } else if (type_and_name[0] == "gauge") {
+                scanner_counter = ADD_CHILD_COUNTER(_profile, 
type_and_name[1], TUnit::UNIT,
+                                                    _connector_name.c_str());
+                COUNTER_SET(scanner_counter, metric_value);
+            } else if (type_and_name[0] == "bytes_gauge") {
+                scanner_counter = ADD_CHILD_COUNTER(_profile, 
type_and_name[1], TUnit::BYTES,
+                                                    _connector_name.c_str());
+                COUNTER_SET(scanner_counter, metric_value);
+            } else if (type_and_name[0] == "timer_peak") {
+                auto* scanner_peak_counter = _profile->add_conditition_counter(
+                        type_and_name[1], TUnit::TIME_NS, update_peak, 
_connector_name.c_str());
+                scanner_peak_counter->conditional_update(metric_value, 
metric_value);
+            } else if (type_and_name[0] == "peak") {
+                auto* scanner_peak_counter = _profile->add_conditition_counter(
+                        type_and_name[1], TUnit::UNIT, update_peak, 
_connector_name.c_str());
+                scanner_peak_counter->conditional_update(metric_value, 
metric_value);
+            } else if (type_and_name[0] == "bytes_peak") {
+                auto* scanner_peak_counter = _profile->add_conditition_counter(
+                        type_and_name[1], TUnit::BYTES, update_peak, 
_connector_name.c_str());
+                scanner_peak_counter->conditional_update(metric_value, 
metric_value);
             } else {
-                LOG(WARNING) << "Type of JNI Scanner metric should be timer, 
counter or bytes";
+                LOG(WARNING) << "Type of JNI Scanner metric should be timer, 
counter, bytes, "
+                             << "timer_gauge, gauge, bytes_gauge, timer_peak, 
peak or bytes_peak";
                 continue;
             }
-            COUNTER_UPDATE(scanner_counter, metric_value);
         }
     }
 }
diff --git a/be/src/vec/exec/scan/scanner_scheduler.h 
b/be/src/vec/exec/scan/scanner_scheduler.h
index 089f3e1e5b7..1790c30faa8 100644
--- a/be/src/vec/exec/scan/scanner_scheduler.h
+++ b/be/src/vec/exec/scan/scanner_scheduler.h
@@ -294,13 +294,28 @@ public:
 
     Status submit_scan_task(SimplifiedScanTask scan_task) override {
         if (!_is_stop) {
+            if (scan_task.scanner_context == nullptr) {
+                return Status::InternalError<false>("scanner pool {} got null 
scanner context.",
+                                                    _sched_name);
+            }
+            if (scan_task.scan_task == nullptr) {
+                return Status::InternalError<false>("scanner pool {} got null 
scan task.",
+                                                    _sched_name);
+            }
+            auto task_handle = scan_task.scanner_context->task_handle();
+            if (task_handle == nullptr) {
+                return Status::InternalError<false>(
+                        "scanner pool {} got null task handle, scan task first 
schedule: {}, "
+                        "scanner context: {}",
+                        _sched_name, scan_task.scan_task->is_first_schedule,
+                        scan_task.scanner_context->debug_string());
+            }
             std::shared_ptr<SplitRunner> split_runner;
             if (scan_task.scan_task->is_first_schedule) {
                 split_runner = 
std::make_shared<ScannerSplitRunner>("scanner_split_runner",
                                                                     
scan_task.scan_func);
                 RETURN_IF_ERROR(split_runner->init());
-                auto result = _task_executor->enqueue_splits(
-                        scan_task.scanner_context->task_handle(), false, 
{split_runner});
+                auto result = _task_executor->enqueue_splits(task_handle, 
false, {split_runner});
                 if (!result.has_value()) {
                     LOG(WARNING) << "enqueue_splits failed: " << 
result.error();
                     return result.error();
@@ -311,8 +326,7 @@ public:
                 if (split_runner == nullptr) {
                     return Status::OK();
                 }
-                RETURN_IF_ERROR(_task_executor->re_enqueue_split(
-                        scan_task.scanner_context->task_handle(), false, 
split_runner));
+                RETURN_IF_ERROR(_task_executor->re_enqueue_split(task_handle, 
false, split_runner));
             }
             scan_task.scan_task->split_runner = split_runner;
             return Status::OK();
diff --git 
a/be/test/vec/exec/executor/time_sharing/time_sharing_task_executor_test.cpp 
b/be/test/vec/exec/executor/time_sharing/time_sharing_task_executor_test.cpp
index a1e604cb6f9..70187f9ef66 100644
--- a/be/test/vec/exec/executor/time_sharing/time_sharing_task_executor_test.cpp
+++ b/be/test/vec/exec/executor/time_sharing/time_sharing_task_executor_test.cpp
@@ -26,6 +26,7 @@
 #include <future>
 #include <mutex>
 #include <random>
+#include <string>
 #include <thread>
 
 #include "vec/exec/executor/ticker.h"
@@ -290,13 +291,51 @@ private:
     ListenableFuture<Void> _completion_future {};
 };
 
+class QueueOnlySplitRunner : public SplitRunner {
+public:
+    Status init() override { return Status::OK(); }
+
+    Result<SharedListenableFuture<Void>> process_for(std::chrono::nanoseconds) 
override {
+        _started = true;
+        _finished = true;
+        return SharedListenableFuture<Void>::create_ready();
+    }
+
+    void close(const Status& status) override {}
+
+    bool is_finished() override { return _finished.load(); }
+
+    Status finished_status() override { return Status::OK(); }
+
+    std::string get_info() const override { return "queue_only_split"; }
+
+    bool is_started() const { return _started.load(); }
+
+private:
+    std::atomic<bool> _started {false};
+    std::atomic<bool> _finished {false};
+};
+
+class TestingTaskHandle final : public TaskHandle {
+public:
+    explicit TestingTaskHandle(std::string task_id) : 
_task_id(std::move(task_id)) {}
+
+    Status init() override { return Status::OK(); }
+
+    bool is_closed() const override { return false; }
+
+    TaskId task_id() const override { return _task_id; }
+
+private:
+    TaskId _task_id;
+};
+
 class TimeSharingTaskExecutorTest : public testing::Test {
 protected:
     void SetUp() override {}
 
     void TearDown() override {}
 
-private:
     template <typename Container>
     void assert_split_states(int end_index, const Container& splits) {
         for (int i = 0; i <= end_index; ++i) {
@@ -324,6 +363,48 @@ private:
     }
 };
 
+TEST_F(TimeSharingTaskExecutorTest, test_invalid_task_handle_returns_error) {
+    auto ticker = std::make_shared<TestingTicker>();
+
+    TimeSharingTaskExecutor::ThreadConfig thread_config;
+    thread_config.thread_name = "invalid_task_handle";
+    thread_config.workload_group = "normal";
+    TimeSharingTaskExecutor executor(thread_config, 0, 1, 1, ticker);
+    ASSERT_TRUE(executor.init().ok());
+
+    auto split = std::make_shared<QueueOnlySplitRunner>();
+
+    auto null_enqueue_result = executor.enqueue_splits(nullptr, false, 
{split});
+    ASSERT_FALSE(null_enqueue_result.has_value());
+    EXPECT_NE(std::string(null_enqueue_result.error().msg()).find("null task 
handle"),
+              std::string::npos);
+
+    Status null_re_enqueue_status = executor.re_enqueue_split(nullptr, false, 
split);
+    ASSERT_FALSE(null_re_enqueue_status.ok());
+    EXPECT_NE(std::string(null_re_enqueue_status.msg()).find("null task 
handle"),
+              std::string::npos);
+
+    Status null_remove_status = executor.remove_task(nullptr);
+    ASSERT_FALSE(null_remove_status.ok());
+    EXPECT_NE(std::string(null_remove_status.msg()).find("null task handle"), 
std::string::npos);
+
+    auto invalid_task_handle = 
std::make_shared<TestingTaskHandle>("invalid_task");
+    auto invalid_enqueue_result = executor.enqueue_splits(invalid_task_handle, 
false, {split});
+    ASSERT_FALSE(invalid_enqueue_result.has_value());
+    EXPECT_NE(std::string(invalid_enqueue_result.error().msg()).find("invalid 
task handle type"),
+              std::string::npos);
+
+    Status invalid_re_enqueue_status = 
executor.re_enqueue_split(invalid_task_handle, false, split);
+    ASSERT_FALSE(invalid_re_enqueue_status.ok());
+    EXPECT_NE(std::string(invalid_re_enqueue_status.msg()).find("invalid task 
handle type"),
+              std::string::npos);
+
+    Status invalid_remove_status = executor.remove_task(invalid_task_handle);
+    ASSERT_FALSE(invalid_remove_status.ok());
+    EXPECT_NE(std::string(invalid_remove_status.msg()).find("invalid task 
handle type"),
+              std::string::npos);
+}
+
 TEST_F(TimeSharingTaskExecutorTest, test_tasks_complete) {
     auto ticker = std::make_shared<TestingTicker>();
 
diff --git 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
index 5690b7f6505..cfbed4969da 100644
--- 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
+++ 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
@@ -25,26 +25,49 @@ import 
org.apache.doris.common.security.authentication.PreExecutionAuthenticator
 
 import com.google.common.base.Preconditions;
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.disk.IOManagerImpl;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.reader.RecordReader;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.table.source.ReadBuilder;
 import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableRead;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.TimestampType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryUsage;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
 public class PaimonJniScanner extends JniScanner {
     private static final Logger LOG = 
LoggerFactory.getLogger(PaimonJniScanner.class);
     private static final String HADOOP_OPTION_PREFIX = "hadoop.";
+    private static final String PAIMON_OPTION_PREFIX = "paimon.";
+    private static final String ASYNC_READER_THREAD_NAME_PREFIX = 
"paimon-reader-async-thread";
+    private static final String FILE_READER_ASYNC_THRESHOLD = 
"file-reader-async-threshold";
+    static final String ENABLE_JNI_IO_MANAGER = 
"paimon.doris.enable_jni_io_manager";
+    static final String JNI_IO_MANAGER_TMP_DIR = 
"paimon.doris.jni_io_manager.tmp_dir";
+    static final String JNI_IO_MANAGER_IMPL_CLASS = 
"paimon.doris.jni_io_manager.impl_class";
+    private static final AtomicInteger ACTIVE_SCANNERS = new AtomicInteger();
 
     private final Map<String, String> params;
     private final Map<String, String> hadoopOptionParams;
@@ -52,12 +75,20 @@ public class PaimonJniScanner extends JniScanner {
     private final String paimonPredicate;
     private Table table;
     private RecordReader<InternalRow> reader;
+    private IOManager ioManager;
+    private String ioManagerTempDirs;
     private final PaimonColumnValue columnValue = new PaimonColumnValue();
     private List<String> paimonAllFieldNames;
     private List<DataType> paimonDataTypeList;
     private RecordReader.RecordIterator<InternalRow> recordIterator = null;
     private final ClassLoader classLoader;
     private PreExecutionAuthenticator preExecutionAuthenticator;
+    private boolean scannerCounted;
+    private long openTimeNanos;
+    private long readBatchTimeNanos;
+    private long readBatchCalls;
+    private long emptyReadBatchCalls;
+    private long rowsRead;
 
     public PaimonJniScanner(int batchSize, Map<String, String> params) {
         this.classLoader = this.getClass().getClassLoader();
@@ -65,8 +96,11 @@ public class PaimonJniScanner extends JniScanner {
             LOG.debug("params:{}", params);
         }
         this.params = params;
-        String[] requiredFields = params.get("required_fields").split(",");
-        String[] requiredTypes = params.get("columns_types").split("#");
+        String[] requiredFields = 
splitRequiredParam(params.get("required_fields"), ",");
+        String[] requiredTypes = 
splitRequiredParam(params.get("columns_types"), "#");
+        Preconditions.checkArgument(requiredFields.length == 
requiredTypes.length,
+                "Required fields size %s is not matched with required types 
size %s",
+                requiredFields.length, requiredTypes.length);
         ColumnType[] columnTypes = new ColumnType[requiredTypes.length];
         for (int i = 0; i < requiredTypes.length; i++) {
             columnTypes[i] = ColumnType.parseType(requiredFields[i], 
requiredTypes[i]);
@@ -85,6 +119,8 @@ public class PaimonJniScanner extends JniScanner {
 
     @Override
     public void open() throws IOException {
+        markScannerOpenedForMetrics();
+        long startTime = System.nanoTime();
         try {
             // When the user does not specify hive-site.xml, Paimon will look 
for the file from the classpath:
             //    org.apache.paimon.hive.HiveCatalog.createHiveConf:
@@ -99,8 +135,15 @@ public class PaimonJniScanner extends JniScanner {
             resetDatetimeV2Precision();
 
         } catch (Throwable e) {
+            try {
+                close();
+            } catch (IOException closeException) {
+                e.addSuppressed(closeException);
+            }
             LOG.warn("Failed to open paimon_scanner: " + e.getMessage(), e);
             throw new RuntimeException(e);
+        } finally {
+            openTimeNanos += System.nanoTime() - startTime;
         }
     }
 
@@ -116,13 +159,118 @@ public class PaimonJniScanner extends JniScanner {
         int[] projected = getProjected();
         readBuilder.withProjection(projected);
         readBuilder.withFilter(getPredicates());
-        reader = 
readBuilder.newRead().executeFilter().createReader(getSplit());
+        reader = 
newReadWithOptionalIOManager(readBuilder).executeFilter().createReader(getSplit());
         paimonDataTypeList =
                 Arrays.stream(projected).mapToObj(i -> 
table.rowType().getTypeAt(i)).collect(Collectors.toList());
     }
 
+    private TableRead newReadWithOptionalIOManager(ReadBuilder readBuilder) 
throws IOException {
+        TableRead tableRead = readBuilder.newRead();
+        if (!isIOManagerEnabled(params)) {
+            return tableRead;
+        }
+        ioManagerTempDirs = getIOManagerTempDirs(params);
+        ioManager = createIOManager(ioManagerTempDirs, 
getIOManagerImplClass(params));
+        LOG.info("Enable Paimon JNI IOManager with temp dirs: {}, 
implementation: {}",
+                ioManagerTempDirs, ioManager.getClass().getName());
+        return tableRead.withIOManager(ioManager);
+    }
+
+    static boolean isIOManagerEnabled(Map<String, String> params) {
+        return Boolean.parseBoolean(params.getOrDefault(ENABLE_JNI_IO_MANAGER, 
"false"));
+    }
+
+    static String getIOManagerTempDirs(Map<String, String> params) throws 
IOException {
+        String tempDirs = params.get(JNI_IO_MANAGER_TMP_DIR);
+        if (tempDirs == null || tempDirs.trim().isEmpty()) {
+            throw new IOException("Paimon JNI IOManager is enabled but " + 
JNI_IO_MANAGER_TMP_DIR + " is not set");
+        }
+        return tempDirs.trim();
+    }
+
+    static String getIOManagerImplClass(Map<String, String> params) {
+        String implClass = params.get(JNI_IO_MANAGER_IMPL_CLASS);
+        return implClass == null || implClass.trim().isEmpty() ? null : 
implClass.trim();
+    }
+
+    static IOManager createIOManager(String tempDirs) throws IOException {
+        return createIOManager(tempDirs, null);
+    }
+
+    static IOManager createIOManager(String tempDirs, String implClassName) 
throws IOException {
+        String[] splitDirs = IOManagerImpl.splitPaths(tempDirs);
+        if (splitDirs.length == 0) {
+            throw new IOException("Paimon JNI IOManager temp dirs are empty");
+        }
+        for (String splitDir : splitDirs) {
+            Files.createDirectories(Paths.get(splitDir));
+        }
+        if (implClassName == null) {
+            return IOManager.create(splitDirs);
+        }
+        return createCustomIOManager(implClassName, splitDirs, tempDirs);
+    }
+
+    private static IOManager createCustomIOManager(String implClassName, 
String[] splitDirs, String tempDirs)
+            throws IOException {
+        ClassLoader loader = Thread.currentThread().getContextClassLoader();
+        if (loader == null) {
+            loader = PaimonJniScanner.class.getClassLoader();
+        }
+        try {
+            Class<?> implClass = Class.forName(implClassName, true, loader);
+            if (!IOManager.class.isAssignableFrom(implClass)) {
+                throw new IOException("Paimon JNI IOManager implementation " + 
implClassName
+                        + " does not implement " + IOManager.class.getName());
+            }
+            return (IOManager) instantiateCustomIOManager(implClass, 
splitDirs, tempDirs);
+        } catch (ClassNotFoundException e) {
+            throw new IOException("Failed to find Paimon JNI IOManager 
implementation: " + implClassName, e);
+        } catch (ReflectiveOperationException e) {
+            throw new IOException("Failed to create Paimon JNI IOManager 
implementation: " + implClassName, e);
+        }
+    }
+
+    private static Object instantiateCustomIOManager(Class<?> implClass, 
String[] splitDirs, String tempDirs)
+            throws ReflectiveOperationException {
+        try {
+            Constructor<?> constructor = 
implClass.getConstructor(String[].class);
+            return constructor.newInstance((Object) splitDirs);
+        } catch (NoSuchMethodException e) {
+            try {
+                Constructor<?> constructor = 
implClass.getConstructor(String.class);
+                return constructor.newInstance(tempDirs);
+            } catch (NoSuchMethodException stringConstructorMissing) {
+                Constructor<?> constructor = implClass.getConstructor();
+                return constructor.newInstance();
+            }
+        } catch (InvocationTargetException e) {
+            throw e;
+        }
+    }
+
     private int[] getProjected() {
-        return 
Arrays.stream(fields).mapToInt(paimonAllFieldNames::indexOf).toArray();
+        return Arrays.stream(fields).mapToInt(fieldName -> {
+            int index = getFieldIndex(paimonAllFieldNames, fieldName);
+            Preconditions.checkArgument(index >= 0, "RequiredField %s not 
found in schema", fieldName);
+            return index;
+        }).toArray();
+    }
+
+    static int getFieldIndex(List<String> fieldNames, String fieldName) {
+        for (int i = 0; i < fieldNames.size(); i++) {
+            if (fieldNames.get(i).equalsIgnoreCase(fieldName)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    static String[] splitRequiredParam(String value, String delimiterRegex) {
+        if (value == null || value.isEmpty()) {
+            return new String[0];
+        }
+        return value.split(delimiterRegex);
     }
 
     private List<Predicate> getPredicates() {
@@ -146,7 +294,7 @@ public class PaimonJniScanner extends JniScanner {
             if (types[i].isDateTimeV2()) {
                 // paimon support precision > 6, but it has been reset as 6 in 
FE
                 // try to get the right precision for datetimev2
-                int index = paimonAllFieldNames.indexOf(fields[i]);
+                int index = getFieldIndex(paimonAllFieldNames, fields[i]);
                 if (index != -1) {
                     DataType dataType = table.rowType().getTypeAt(index);
                     if (dataType instanceof TimestampType) {
@@ -159,8 +307,36 @@ public class PaimonJniScanner extends JniScanner {
 
     @Override
     public void close() throws IOException {
-        if (reader != null) {
-            reader.close();
+        IOException exception = null;
+        try {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (IOException e) {
+                    exception = e;
+                } finally {
+                    reader = null;
+                }
+            }
+            if (ioManager != null) {
+                try {
+                    ioManager.close();
+                } catch (Exception e) {
+                    LOG.warn("Failed to close Paimon JNI IOManager, temp dirs: 
{}", ioManagerTempDirs, e);
+                    if (exception == null) {
+                        exception = new IOException(e);
+                    } else {
+                        exception.addSuppressed(e);
+                    }
+                } finally {
+                    ioManager = null;
+                }
+            }
+        } finally {
+            markScannerClosedForMetrics();
+        }
+        if (exception != null) {
+            throw exception;
         }
     }
 
@@ -168,7 +344,7 @@ public class PaimonJniScanner extends JniScanner {
         int rows = 0;
         try {
             if (recordIterator == null) {
-                recordIterator = reader.readBatch();
+                recordIterator = readBatchWithMetrics();
             }
 
             while (recordIterator != null) {
@@ -183,15 +359,23 @@ public class PaimonJniScanner extends JniScanner {
                         appendData(i, columnValue);
                     }
                     if (rows >= batchSize) {
+                        if (fields.length == 0) {
+                            vectorTable.appendVirtualData(rows);
+                        }
                         appendDataTime += System.nanoTime() - startTime;
+                        rowsRead += rows;
                         return rows;
                     }
                 }
                 appendDataTime += System.nanoTime() - startTime;
 
                 recordIterator.releaseBatch();
-                recordIterator = reader.readBatch();
+                recordIterator = readBatchWithMetrics();
             }
+            if (fields.length == 0 && rows > 0) {
+                vectorTable.appendVirtualData(rows);
+            }
+            rowsRead += rows;
         } catch (Exception e) {
             close();
             LOG.warn("Failed to get the next batch of paimon. "
@@ -202,6 +386,20 @@ public class PaimonJniScanner extends JniScanner {
         return rows;
     }
 
+    private RecordReader.RecordIterator<InternalRow> readBatchWithMetrics() 
throws IOException {
+        long startTime = System.nanoTime();
+        try {
+            RecordReader.RecordIterator<InternalRow> iterator = 
reader.readBatch();
+            if (iterator == null) {
+                emptyReadBatchCalls++;
+            }
+            return iterator;
+        } finally {
+            readBatchCalls++;
+            readBatchTimeNanos += System.nanoTime() - startTime;
+        }
+    }
+
     @Override
     protected int getNext() {
         try {
@@ -217,6 +415,148 @@ public class PaimonJniScanner extends JniScanner {
         return null;
     }
 
+    @Override
+    public Map<String, String> getStatistics() {
+        Map<String, String> statistics = new HashMap<>();
+        statistics.put("gauge:PaimonJniIOManagerEnabled", ioManager != null ? 
"1" : "0");
+        statistics.put("gauge:PaimonJniActiveScannerCount", 
String.valueOf(ACTIVE_SCANNERS.get()));
+        statistics.put("gauge:PaimonJniAsyncReaderThreadCount",
+                String.valueOf(currentAsyncReaderThreadCount()));
+        statistics.put("gauge:PaimonJniRequiredFieldCount", 
String.valueOf(fields.length));
+        statistics.put("counter:PaimonJniSplitEncodedLength", 
String.valueOf(lengthOfParam("paimon_split")));
+        statistics.put("counter:PaimonJniPredicateEncodedLength", 
String.valueOf(lengthOfParam("paimon_predicate")));
+        statistics.put("gauge:PaimonJniAsyncThresholdConfigured",
+                hasPaimonOption(FILE_READER_ASYNC_THRESHOLD) ? "1" : "0");
+        
parseDataSizeBytes(paimonOption(FILE_READER_ASYNC_THRESHOLD)).ifPresent(
+                bytes -> 
statistics.put("bytes_gauge:PaimonJniAsyncThresholdBytes", 
String.valueOf(bytes)));
+        statistics.put("counter:PaimonJniReadBatchCalls", 
String.valueOf(readBatchCalls));
+        statistics.put("counter:PaimonJniEmptyReadBatchCalls", 
String.valueOf(emptyReadBatchCalls));
+        statistics.put("counter:PaimonJniRowsRead", String.valueOf(rowsRead));
+        statistics.put("timer:PaimonJniScannerOpenTime", 
String.valueOf(openTimeNanos));
+        statistics.put("timer:PaimonJniReadBatchTime", 
String.valueOf(readBatchTimeNanos));
+        putMemoryStatistics(statistics);
+        return statistics;
+    }
+
+    private int lengthOfParam(String key) {
+        String value = params.get(key);
+        return value == null ? 0 : value.length();
+    }
+
+    private boolean hasPaimonOption(String key) {
+        return paimonOption(key) != null;
+    }
+
+    private String paimonOption(String key) {
+        if (table != null) {
+            String tableOption = table.options().get(key);
+            if (tableOption != null) {
+                return tableOption;
+            }
+        }
+        return params.get(PAIMON_OPTION_PREFIX + key);
+    }
+
+    private static void putMemoryStatistics(Map<String, String> statistics) {
+        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
+        MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
+        MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage();
+        statistics.put("bytes_gauge:PaimonJniJvmHeapUsed", 
String.valueOf(nonNegative(heapUsage.getUsed())));
+        statistics.put("bytes_gauge:PaimonJniJvmHeapCommitted", 
String.valueOf(nonNegative(heapUsage.getCommitted())));
+        statistics.put("bytes_gauge:PaimonJniJvmHeapMax", 
String.valueOf(nonNegative(heapUsage.getMax())));
+        statistics.put("bytes_gauge:PaimonJniJvmNonHeapUsed", 
String.valueOf(nonNegative(nonHeapUsage.getUsed())));
+        statistics.put("bytes_gauge:PaimonJniJvmNonHeapCommitted",
+                String.valueOf(nonNegative(nonHeapUsage.getCommitted())));
+        statistics.put("bytes_gauge:PaimonJniJvmNonHeapMax", 
String.valueOf(nonNegative(nonHeapUsage.getMax())));
+    }
+
+    private static long nonNegative(long value) {
+        return Math.max(value, 0L);
+    }
+
+    private static int currentAsyncReaderThreadCount() {
+        return countThreadsByNamePrefix(ASYNC_READER_THREAD_NAME_PREFIX);
+    }
+
+    static int countThreadsByNamePrefix(String threadNamePrefix) {
+        int count = 0;
+        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
+        ThreadInfo[] threadInfos = 
threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds(), 0);
+        for (ThreadInfo threadInfo : threadInfos) {
+            if (threadInfo != null && 
threadInfo.getThreadName().startsWith(threadNamePrefix)) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    private void markScannerOpenedForMetrics() {
+        if (!scannerCounted) {
+            scannerCounted = true;
+            ACTIVE_SCANNERS.incrementAndGet();
+        }
+    }
+
+    private void markScannerClosedForMetrics() {
+        if (scannerCounted) {
+            scannerCounted = false;
+            ACTIVE_SCANNERS.decrementAndGet();
+        }
+    }
+
+    static Optional<Long> parseDataSizeBytes(String value) {
+        if (value == null || value.trim().isEmpty()) {
+            return Optional.empty();
+        }
+        String normalized = value.trim().toLowerCase(Locale.ROOT).replace("_", 
"").replace(" ", "");
+        int unitStart = 0;
+        while (unitStart < normalized.length()
+                && (Character.isDigit(normalized.charAt(unitStart)) || 
normalized.charAt(unitStart) == '.')) {
+            unitStart++;
+        }
+        if (unitStart == 0) {
+            return Optional.empty();
+        }
+        try {
+            double number = Double.parseDouble(normalized.substring(0, 
unitStart));
+            String unit = normalized.substring(unitStart);
+            long multiplier;
+            switch (unit) {
+                case "":
+                case "b":
+                case "byte":
+                case "bytes":
+                    multiplier = 1L;
+                    break;
+                case "k":
+                case "kb":
+                case "kib":
+                    multiplier = 1024L;
+                    break;
+                case "m":
+                case "mb":
+                case "mib":
+                    multiplier = 1024L * 1024L;
+                    break;
+                case "g":
+                case "gb":
+                case "gib":
+                    multiplier = 1024L * 1024L * 1024L;
+                    break;
+                case "t":
+                case "tb":
+                case "tib":
+                    multiplier = 1024L * 1024L * 1024L * 1024L;
+                    break;
+                default:
+                    return Optional.empty();
+            }
+            return Optional.of((long) (number * multiplier));
+        } catch (NumberFormatException e) {
+            return Optional.empty();
+        }
+    }
+
     private void initTable() {
         Preconditions.checkState(params.containsKey("serialized_table"));
         table = PaimonUtils.deserialize(params.get("serialized_table"));
@@ -227,4 +567,3 @@ public class PaimonJniScanner extends JniScanner {
     }
 
 }
-
diff --git 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonSysTableJniScanner.java
 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonSysTableJniScanner.java
index 6c2eab84129..90d309defcc 100644
--- 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonSysTableJniScanner.java
+++ 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonSysTableJniScanner.java
@@ -85,7 +85,11 @@ public class PaimonSysTableJniScanner extends JniScanner {
             LOG.debug("paimonAllFieldNames:{}", paimonAllFieldNames);
         }
         resetDatetimeV2Precision();
-        this.projected = 
Arrays.stream(fields).mapToInt(paimonAllFieldNames::indexOf).toArray();
+        this.projected = Arrays.stream(fields).mapToInt(fieldName -> {
+            int index = PaimonJniScanner.getFieldIndex(paimonAllFieldNames, 
fieldName);
+            Preconditions.checkArgument(index >= 0, "RequiredField %s not 
found in schema", fieldName);
+            return index;
+        }).toArray();
         this.paimonDataTypeList = Arrays.stream(projected).mapToObj(i -> 
table.rowType().getTypeAt(i))
                 .collect(Collectors.toList());
         this.paimonSplits = 
Arrays.stream(params.get("serialized_splits").split(","))
@@ -148,7 +152,7 @@ public class PaimonSysTableJniScanner extends JniScanner {
             if (types[i].isDateTimeV2()) {
                 // paimon support precision > 6, but it has been reset as 6 in 
FE
                 // try to get the right precision for datetimev2
-                int index = paimonAllFieldNames.indexOf(fields[i]);
+                int index = 
PaimonJniScanner.getFieldIndex(paimonAllFieldNames, fields[i]);
                 if (index != -1) {
                     DataType dataType = table.rowType().getTypeAt(index);
                     if (dataType instanceof TimestampType) {
diff --git 
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
new file mode 100644
index 00000000000..7ab07c8efcf
--- /dev/null
+++ 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
@@ -0,0 +1,243 @@
+// 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.
+
+package org.apache.doris.paimon;
+
+import org.apache.paimon.disk.BufferFileReader;
+import org.apache.paimon.disk.BufferFileWriter;
+import org.apache.paimon.disk.FileIOChannel;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.table.Table;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class PaimonJniScannerTest {
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Test
+    public void testConstructorAcceptsEmptyProjection() {
+        new PaimonJniScanner(128, createBaseParams());
+        Assert.assertArrayEquals(new String[0], 
PaimonJniScanner.splitRequiredParam("", ","));
+        Assert.assertArrayEquals(new String[0], 
PaimonJniScanner.splitRequiredParam("", "#"));
+        Assert.assertArrayEquals(new String[] {"id", "name"},
+                PaimonJniScanner.splitRequiredParam("id,name", ","));
+    }
+
+    @Test
+    public void testIOManagerOptionHelpers() throws Exception {
+        Map<String, String> params = createBaseParams();
+        Assert.assertFalse(PaimonJniScanner.isIOManagerEnabled(params));
+
+        params.put(PaimonJniScanner.ENABLE_JNI_IO_MANAGER, "true");
+        File tempDir = new File(temporaryFolder.getRoot(), 
"paimon-io-manager");
+        params.put(PaimonJniScanner.JNI_IO_MANAGER_TMP_DIR, 
tempDir.getAbsolutePath());
+
+        Assert.assertTrue(PaimonJniScanner.isIOManagerEnabled(params));
+        Assert.assertEquals(tempDir.getAbsolutePath(), 
PaimonJniScanner.getIOManagerTempDirs(params));
+        Assert.assertNull(PaimonJniScanner.getIOManagerImplClass(params));
+        PaimonJniScanner.createIOManager(tempDir.getAbsolutePath()).close();
+        Assert.assertTrue(tempDir.exists());
+    }
+
+    @Test
+    public void testCreateDefaultAndCustomIOManager() throws Exception {
+        File tempDir = new File(temporaryFolder.getRoot(), 
"paimon-io-manager-impl");
+        IOManager defaultIOManager = 
PaimonJniScanner.createIOManager(tempDir.getAbsolutePath());
+        Assert.assertTrue(defaultIOManager instanceof IOManagerImpl);
+        defaultIOManager.close();
+
+        Map<String, String> params = createBaseParams();
+        params.put(PaimonJniScanner.JNI_IO_MANAGER_IMPL_CLASS, 
TestIOManager.class.getName());
+        Assert.assertEquals(TestIOManager.class.getName(), 
PaimonJniScanner.getIOManagerImplClass(params));
+        IOManager customIOManager = PaimonJniScanner.createIOManager(
+                tempDir.getAbsolutePath(), 
PaimonJniScanner.getIOManagerImplClass(params));
+        Assert.assertTrue(customIOManager instanceof TestIOManager);
+        Assert.assertArrayEquals(new String[] {tempDir.getAbsolutePath()}, 
customIOManager.tempDirs());
+    }
+
+    @Test
+    public void testCloseCleansIOManagerTempDirectory() throws Exception {
+        File tempDir = temporaryFolder.newFolder("paimon-io-manager-clean");
+        IOManager ioManager = 
PaimonJniScanner.createIOManager(tempDir.getAbsolutePath());
+        FileIOChannel.ID channel = ioManager.createChannel();
+        File spillFile = channel.getPathFile();
+        Assert.assertTrue(spillFile.createNewFile());
+        File spillDir = spillFile.getParentFile();
+        Assert.assertTrue(spillDir.exists());
+
+        PaimonJniScanner scanner = new PaimonJniScanner(128, 
createBaseParams());
+        Field ioManagerField = 
PaimonJniScanner.class.getDeclaredField("ioManager");
+        ioManagerField.setAccessible(true);
+        ioManagerField.set(scanner, ioManager);
+        Assert.assertEquals("1", 
scanner.getStatistics().get("gauge:PaimonJniIOManagerEnabled"));
+
+        scanner.close();
+        Assert.assertFalse(spillDir.exists());
+    }
+
+    @Test
+    public void testStatisticsIncludePaimonDiagnostics() throws Exception {
+        Map<String, String> params = createBaseParams();
+        params.put("paimon_split", "encoded-split");
+        params.put("paimon_predicate", "encoded-predicate");
+        PaimonJniScanner scanner = new PaimonJniScanner(128, params);
+        setTableOptions(scanner, 
Collections.singletonMap("file-reader-async-threshold", "10 MiB"));
+
+        Map<String, String> statistics = scanner.getStatistics();
+
+        Assert.assertEquals("0", 
statistics.get("gauge:PaimonJniIOManagerEnabled"));
+        Assert.assertEquals("0", 
statistics.get("gauge:PaimonJniRequiredFieldCount"));
+        Assert.assertEquals("13", 
statistics.get("counter:PaimonJniSplitEncodedLength"));
+        Assert.assertEquals("17", 
statistics.get("counter:PaimonJniPredicateEncodedLength"));
+        Assert.assertEquals("1", 
statistics.get("gauge:PaimonJniAsyncThresholdConfigured"));
+        Assert.assertEquals(String.valueOf(10L * 1024L * 1024L),
+                statistics.get("bytes_gauge:PaimonJniAsyncThresholdBytes"));
+        
Assert.assertTrue(statistics.containsKey("gauge:PaimonJniAsyncReaderThreadCount"));
+        
Assert.assertTrue(statistics.containsKey("gauge:PaimonJniActiveScannerCount"));
+        
Assert.assertFalse(statistics.containsKey("peak:PaimonJniActiveScannerPeakCount"));
+        
Assert.assertFalse(statistics.containsKey("peak:PaimonJniAsyncReaderThreadPeakCount"));
+        
Assert.assertTrue(statistics.containsKey("counter:PaimonJniReadBatchCalls"));
+        
Assert.assertTrue(statistics.containsKey("timer:PaimonJniScannerOpenTime"));
+        
Assert.assertTrue(statistics.containsKey("timer:PaimonJniReadBatchTime"));
+        
Assert.assertTrue(Long.parseLong(statistics.get("bytes_gauge:PaimonJniJvmHeapUsed"))
 > 0);
+        
Assert.assertTrue(Long.parseLong(statistics.get("bytes_gauge:PaimonJniJvmHeapCommitted"))
 > 0);
+    }
+
+    @Test
+    public void testCountThreadsByNamePrefix() throws Exception {
+        CountDownLatch started = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        Thread thread = new Thread(() -> {
+            started.countDown();
+            try {
+                release.await(30, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }, "paimon-reader-async-thread-test");
+
+        thread.start();
+        try {
+            Assert.assertTrue(started.await(5, TimeUnit.SECONDS));
+            
Assert.assertTrue(PaimonJniScanner.countThreadsByNamePrefix("paimon-reader-async-thread")
 >= 1);
+        } finally {
+            release.countDown();
+            thread.join(5000);
+        }
+    }
+
+    @Test
+    public void testParseDataSizeBytes() {
+        Assert.assertEquals(Long.valueOf(1024L), 
PaimonJniScanner.parseDataSizeBytes("1 KiB").get());
+        Assert.assertEquals(Long.valueOf(10L * 1024L * 1024L),
+                PaimonJniScanner.parseDataSizeBytes("10 MiB").get());
+        Assert.assertEquals(Long.valueOf(2L * 1024L * 1024L * 1024L),
+                PaimonJniScanner.parseDataSizeBytes("2GB").get());
+        
Assert.assertFalse(PaimonJniScanner.parseDataSizeBytes("unknown").isPresent());
+    }
+
+    private Map<String, String> createBaseParams() {
+        Map<String, String> params = new HashMap<>();
+        params.put("required_fields", "");
+        params.put("columns_types", "");
+        params.put("paimon_split", "");
+        params.put("paimon_predicate", "");
+        return params;
+    }
+
+    private void setTableOptions(PaimonJniScanner scanner, Map<String, String> 
options) throws Exception {
+        Table table = (Table) Proxy.newProxyInstance(
+                Table.class.getClassLoader(), new Class[] {Table.class}, 
(proxy, method, args) -> {
+                    if ("options".equals(method.getName())) {
+                        return options;
+                    }
+                    if ("toString".equals(method.getName())) {
+                        return "TestPaimonTable";
+                    }
+                    throw new UnsupportedOperationException(method.getName());
+                });
+        Field tableField = PaimonJniScanner.class.getDeclaredField("table");
+        tableField.setAccessible(true);
+        tableField.set(scanner, table);
+    }
+
+    public static class TestIOManager implements IOManager {
+        private final String[] tempDirs;
+
+        public TestIOManager(String[] tempDirs) {
+            this.tempDirs = tempDirs;
+        }
+
+        @Override
+        public FileIOChannel.ID createChannel() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public FileIOChannel.ID createChannel(String channelName) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String[] tempDirs() {
+            return tempDirs;
+        }
+
+        @Override
+        public FileIOChannel.Enumerator createChannelEnumerator() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public BufferFileWriter createBufferFileWriter(FileIOChannel.ID 
channel) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public BufferFileReader createBufferFileReader(FileIOChannel.ID 
channel) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void close() {
+        }
+    }
+
+    @Test
+    public void testGetFieldIndexMatchesMixedCaseColumns() {
+        Assert.assertEquals(1, 
PaimonJniScanner.getFieldIndex(Arrays.asList("data", "mIxEd_COL", "PART"),
+                "mixed_col"));
+        Assert.assertEquals(2, 
PaimonJniScanner.getFieldIndex(Arrays.asList("data", "mIxEd_COL", "PART"),
+                "part"));
+        Assert.assertEquals(-1, 
PaimonJniScanner.getFieldIndex(Arrays.asList("data", "mIxEd_COL", "PART"),
+                "missing_col"));
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/DorisTypeToIcebergType.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/DorisTypeToIcebergType.java
index 56fa03120d9..de19d90728d 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/DorisTypeToIcebergType.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/DorisTypeToIcebergType.java
@@ -29,6 +29,7 @@ import com.google.common.collect.Lists;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.Types;
 
+import java.util.Collections;
 import java.util.List;
 
 
@@ -37,14 +38,21 @@ import java.util.List;
  */
 public class DorisTypeToIcebergType extends DorisTypeVisitor<Type> {
     private final StructType root;
+    private final List<String> rootFieldNames;
     private int nextId = 0;
 
     public DorisTypeToIcebergType() {
         this.root = null;
+        this.rootFieldNames = Collections.emptyList();
     }
 
     public DorisTypeToIcebergType(StructType root) {
+        this(root, Collections.emptyList());
+    }
+
+    public DorisTypeToIcebergType(StructType root, List<String> 
rootFieldNames) {
         this.root = root;
+        this.rootFieldNames = rootFieldNames;
         // the root struct's fields use the first ids
         this.nextId = root.getFields().size();
     }
@@ -65,10 +73,11 @@ public class DorisTypeToIcebergType extends 
DorisTypeVisitor<Type> {
             Type type = types.get(i);
 
             int id = isRoot ? i : getNextId();
+            String fieldName = isRoot && !rootFieldNames.isEmpty() ? 
rootFieldNames.get(i) : field.getName();
             if (field.getContainsNull()) {
-                newFields.add(Types.NestedField.optional(id, field.getName(), 
type, field.getComment()));
+                newFields.add(Types.NestedField.optional(id, fieldName, type, 
field.getComment()));
             } else {
-                newFields.add(Types.NestedField.required(id, field.getName(), 
type, field.getComment()));
+                newFields.add(Types.NestedField.required(id, fieldName, type, 
field.getComment()));
             }
         }
         return Types.StructType.of(newFields);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
index 545237cea27..3aa26f3572c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
@@ -353,8 +353,8 @@ public class IcebergMetadataOps implements 
ExternalMetadataOps {
                 .map(col -> new StructField(col.getName(), col.getType(), 
col.getComment(), col.isAllowNull()))
                 .collect(Collectors.toList());
         StructType structType = new StructType(new ArrayList<>(collect));
-        Type visit =
-                DorisTypeVisitor.visit(structType, new 
DorisTypeToIcebergType(structType));
+        List<String> rootFieldNames = 
columns.stream().map(Column::getName).collect(Collectors.toList());
+        Type visit = DorisTypeVisitor.visit(structType, new 
DorisTypeToIcebergType(structType, rootFieldNames));
         Schema schema = new 
Schema(visit.asNestedType().asStructType().fields());
         Map<String, String> properties = createTableInfo.getProperties();
         properties.put(ExternalCatalog.DORIS_VERSION, 
ExternalCatalog.DORIS_VERSION_VALUE);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
index 9895ceb7023..222809ca6a4 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
@@ -133,7 +133,6 @@ import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -520,34 +519,38 @@ public class IcebergUtils {
         PartitionSpec.Builder builder = PartitionSpec.builderFor(schema);
         for (Expr expr : partitionExprs) {
             if (expr instanceof SlotRef) {
-                builder.identity(((SlotRef) expr).getColumnName());
+                builder.identity(getIcebergColumnName(schema, ((SlotRef) 
expr).getColumnName()));
             } else if (expr instanceof FunctionCallExpr) {
                 String exprName = expr.getExprName();
                 List<Expr> params = ((FunctionCallExpr) 
expr).getParams().exprs();
                 switch (exprName.toLowerCase()) {
                     case "bucket":
-                        builder.bucket(params.get(1).getExprName(), 
Integer.parseInt(params.get(0).getStringValue()));
+                        builder.bucket(
+                                getIcebergColumnName(schema, 
params.get(1).getExprName()),
+                                
Integer.parseInt(params.get(0).getStringValue()));
                         break;
                     case "year":
                     case "years":
-                        builder.year(params.get(0).getExprName());
+                        builder.year(getIcebergColumnName(schema, 
params.get(0).getExprName()));
                         break;
                     case "month":
                     case "months":
-                        builder.month(params.get(0).getExprName());
+                        builder.month(getIcebergColumnName(schema, 
params.get(0).getExprName()));
                         break;
                     case "date":
                     case "day":
                     case "days":
-                        builder.day(params.get(0).getExprName());
+                        builder.day(getIcebergColumnName(schema, 
params.get(0).getExprName()));
                         break;
                     case "date_hour":
                     case "hour":
                     case "hours":
-                        builder.hour(params.get(0).getExprName());
+                        builder.hour(getIcebergColumnName(schema, 
params.get(0).getExprName()));
                         break;
                     case "truncate":
-                        builder.truncate(params.get(1).getExprName(), 
Integer.parseInt(params.get(0).getStringValue()));
+                        builder.truncate(
+                                getIcebergColumnName(schema, 
params.get(1).getExprName()),
+                                
Integer.parseInt(params.get(0).getStringValue()));
                         break;
                     default:
                         throw new UserException("unsupported partition for " + 
exprName);
@@ -557,6 +560,11 @@ public class IcebergUtils {
         return builder.build();
     }
 
+    private static String getIcebergColumnName(Schema schema, String 
columnName) {
+        Types.NestedField field = schema.caseInsensitiveFindField(columnName);
+        return field == null ? columnName : field.name();
+    }
+
     private static Type 
icebergPrimitiveTypeToDorisType(org.apache.iceberg.types.Type.PrimitiveType 
primitive,
             boolean enableMappingVarbinary, boolean enableMappingTimestampTz) {
         switch (primitive.typeId()) {
@@ -974,7 +982,7 @@ public class IcebergUtils {
         List<Types.NestedField> columns = schema.columns();
         List<Column> resSchema = 
Lists.newArrayListWithCapacity(columns.size());
         for (Types.NestedField field : columns) {
-            Column column = new Column(field.name().toLowerCase(Locale.ROOT),
+            Column column = new Column(field.name(),
                     IcebergUtils.icebergTypeToDorisType(field.type(), 
enableMappingVarbinary, enableMappingTimestampTz),
                     true, null,
                     true, field.doc(), true, -1);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalTable.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalTable.java
index 1782bae59b0..1a99aad60f6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalTable.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalTable.java
@@ -330,7 +330,7 @@ public class PaimonExternalTable extends ExternalTable 
implements MTMVRelatedTab
             Set<String> partitionColumnNames = 
Sets.newHashSet(tableSchema.partitionKeys());
             List<Column> partitionColumns = Lists.newArrayList();
             for (DataField field : columns) {
-                Column column = new Column(field.name().toLowerCase(),
+                Column column = new Column(field.name(),
                         PaimonUtil.paimonTypeToDorisType(field.type(), 
getCatalog().getEnableMappingVarbinary(),
                                 getCatalog().getEnableMappingTimestampTz()),
                         true,
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 08358a3da99..3239035f20d 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
@@ -430,7 +430,7 @@ public class PaimonUtil {
             boolean enableTimestampTzMapping) {
         List<Column> resSchema = 
Lists.newArrayListWithCapacity(rowType.getFields().size());
         rowType.getFields().forEach(field -> {
-            resSchema.add(new Column(field.name().toLowerCase(),
+            resSchema.add(new Column(field.name(),
                     PaimonUtil.paimonTypeToDorisType(field.type(), 
enableVarbinaryMapping, enableTimestampTzMapping),
                     primaryKeys.contains(field.name()),
                     null,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
index 73a3c72ddcc..ae45c242718 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
@@ -46,7 +46,7 @@ public class PaimonPredicateConverter {
 
     public PaimonPredicateConverter(RowType rowType) {
         this.builder = new PredicateBuilder(rowType);
-        this.fieldNames = rowType.getFields().stream().map(f -> 
f.name().toLowerCase()).collect(Collectors.toList());
+        this.fieldNames = 
rowType.getFields().stream().map(DataField::name).collect(Collectors.toList());
         this.paimonFieldTypes = 
rowType.getFields().stream().map(DataField::type).collect(Collectors.toList());
     }
 
@@ -99,7 +99,7 @@ public class PaimonPredicateConverter {
             return null;
         }
         String colName = slotRef.getColumnName();
-        int idx = fieldNames.indexOf(colName);
+        int idx = getFieldIndex(colName);
         DataType dataType = paimonFieldTypes.get(idx);
         List<Object> valueList = new ArrayList<>();
         for (int i = 1; i < predicate.getChildren().size(); i++) {
@@ -132,7 +132,7 @@ public class PaimonPredicateConverter {
             return null;
         }
         String colName = slotRef.getColumnName();
-        int idx = fieldNames.indexOf(colName);
+        int idx = getFieldIndex(colName);
         DataType dataType = paimonFieldTypes.get(idx);
         Object value = dataType.accept(new PaimonValueConverter(literalExpr));
         if (value == null) {
@@ -174,6 +174,15 @@ public class PaimonPredicateConverter {
 
     }
 
+    private int getFieldIndex(String colName) {
+        for (int i = 0; i < fieldNames.size(); i++) {
+            if (fieldNames.get(i).equalsIgnoreCase(colName)) {
+                return i;
+            }
+        }
+        return fieldNames.indexOf(colName);
+    }
+
 
     public static SlotRef convertDorisExprToSlotRef(Expr expr) {
         SlotRef slotRef = null;
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 00359b5a11c..5b24ddfccde 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
@@ -19,6 +19,7 @@ package org.apache.doris.datasource.paimon.source;
 
 import org.apache.doris.analysis.TableScanParams;
 import org.apache.doris.analysis.TupleDescriptor;
+import org.apache.doris.catalog.JdbcResource;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.MetaNotFoundException;
@@ -65,6 +66,7 @@ import org.apache.paimon.table.source.TableScan;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -89,6 +91,17 @@ public class PaimonScanNode extends FileQueryScanNode {
     private static final String DORIS_START_TIMESTAMP = "startTimestamp";
     private static final String DORIS_END_TIMESTAMP = "endTimestamp";
     private static final String DORIS_INCREMENTAL_BETWEEN_SCAN_MODE = 
"incrementalBetweenScanMode";
+    private static final String PAIMON_PROPERTY_PREFIX = "paimon.";
+    private static final String DORIS_ENABLE_JNI_IO_MANAGER = 
"doris.enable_jni_io_manager";
+    private static final String DORIS_JNI_IO_MANAGER_TMP_DIR = 
"doris.jni_io_manager.tmp_dir";
+    private static final String DORIS_JNI_IO_MANAGER_IMPL_CLASS = 
"doris.jni_io_manager.impl_class";
+    private static final String JDBC_PREFIX = "jdbc.";
+    private static final String JDBC_DRIVER_URL = JDBC_PREFIX + 
JdbcResource.DRIVER_URL;
+    private static final String JDBC_DRIVER_CLASS = JDBC_PREFIX + 
JdbcResource.DRIVER_CLASS;
+    private static final List<String> BACKEND_PAIMON_OPTIONS = Arrays.asList(
+            DORIS_ENABLE_JNI_IO_MANAGER,
+            DORIS_JNI_IO_MANAGER_TMP_DIR,
+            DORIS_JNI_IO_MANAGER_IMPL_CLASS);
 
     private enum SplitReadType {
         JNI,
@@ -142,6 +155,7 @@ public class PaimonScanNode extends FileQueryScanNode {
     // get them in doInitialize() to ensure internal consistency of ScanNode
     private Map<StorageProperties.Type, StorageProperties> 
storagePropertiesMap;
     private Map<String, String> backendStorageProperties;
+    private Map<String, String> backendPaimonOptions = Collections.emptyMap();
 
     // The schema information involved in the current query process (including 
historical schema).
     protected ConcurrentHashMap<Long, Boolean> currentQuerySchema = new 
ConcurrentHashMap<>();
@@ -169,6 +183,7 @@ public class PaimonScanNode extends FileQueryScanNode {
                 source.getPaimonTable()
         );
         backendStorageProperties = 
CredentialUtils.getBackendPropertiesFromStorageMap(storagePropertiesMap);
+        backendPaimonOptions = getBackendPaimonOptions();
     }
 
     @VisibleForTesting
@@ -259,6 +274,9 @@ public class PaimonScanNode extends FileQueryScanNode {
             // MUST explicitly set to -1, to be distinct from valid row count 
>= 0
             tableFormatFileDesc.setTableLevelRowCount(-1);
         }
+        if (!backendPaimonOptions.isEmpty()) {
+            fileDesc.setPaimonOptions(backendPaimonOptions);
+        }
         tableFormatFileDesc.setPaimonParams(fileDesc);
         Map<String, String> partitionValues = 
paimonSplit.getPaimonPartitionValues();
         if (partitionValues != null) {
@@ -428,6 +446,51 @@ public class PaimonScanNode extends FileQueryScanNode {
         return splits;
     }
 
+    @VisibleForTesting
+    Map<String, String> getBackendPaimonOptions() {
+        if (source == null) {
+            return Collections.emptyMap();
+        }
+        if (!(source.getCatalog() instanceof PaimonExternalCatalog)) {
+            return Collections.emptyMap();
+        }
+        PaimonExternalCatalog catalog = (PaimonExternalCatalog) 
source.getCatalog();
+        Map<String, String> backendOptions = new HashMap<>();
+        Map<String, String> catalogProperties = 
catalog.getCatalogProperty().getProperties();
+        if (catalogProperties == null) {
+            catalogProperties = Collections.emptyMap();
+        }
+        for (String option : BACKEND_PAIMON_OPTIONS) {
+            String catalogProperty = PAIMON_PROPERTY_PREFIX + option;
+            if (catalogProperties.containsKey(catalogProperty)) {
+                backendOptions.put(option, 
catalogProperties.get(catalogProperty));
+            }
+        }
+        String driverUrl = getCatalogProperty(catalogProperties, 
JdbcResource.DRIVER_URL);
+        if (driverUrl == null) {
+            return backendOptions;
+        }
+        String driverClass = getCatalogProperty(catalogProperties, 
JdbcResource.DRIVER_CLASS);
+        if (driverClass == null) {
+            throw new IllegalArgumentException("jdbc.driver_class or 
paimon.jdbc.driver_class is required when "
+                    + "jdbc.driver_url or paimon.jdbc.driver_url is 
specified");
+        }
+        backendOptions.put(JDBC_DRIVER_URL, 
JdbcResource.getFullDriverUrl(driverUrl));
+        backendOptions.put(JDBC_DRIVER_CLASS, driverClass);
+        return backendOptions;
+    }
+
+    private String getCatalogProperty(Map<String, String> catalogProperties, 
String property) {
+        String value = catalogProperties.get(PAIMON_PROPERTY_PREFIX + 
JDBC_PREFIX + property);
+        if (value == null || value.trim().isEmpty()) {
+            value = catalogProperties.get(JDBC_PREFIX + property);
+        }
+        if (value == null || value.trim().isEmpty()) {
+            return null;
+        }
+        return value;
+    }
+
     private long determineTargetFileSplitSize(List<DataSplit> dataSplits,
             boolean isBatchMode) {
         if (sessionVariable.getFileSplitSize() > 0) {
@@ -473,13 +536,9 @@ public class PaimonScanNode extends FileQueryScanNode {
     @VisibleForTesting
     public List<org.apache.paimon.table.source.Split> getPaimonSplitFromAPI() 
throws UserException {
         Table paimonTable = getProcessedTable();
+        List<String> fieldNames = paimonTable.rowType().getFieldNames();
         int[] projected = desc.getSlots().stream().mapToInt(
-                slot -> paimonTable.rowType()
-                        .getFieldNames()
-                        .stream()
-                        .map(String::toLowerCase)
-                        .collect(Collectors.toList())
-                        .indexOf(slot.getColumn().getName()))
+                slot -> getFieldIndex(fieldNames, slot.getColumn().getName()))
                 .filter(i -> i >= 0)
                 .toArray();
         ReadBuilder readBuilder = paimonTable.newReadBuilder();
@@ -498,6 +557,16 @@ public class PaimonScanNode extends FileQueryScanNode {
         return splits;
     }
 
+    @VisibleForTesting
+    static int getFieldIndex(List<String> fieldNames, String columnName) {
+        for (int i = 0; i < fieldNames.size(); i++) {
+            if (fieldNames.get(i).equalsIgnoreCase(columnName)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     private String getFileFormat(String path) {
         return 
FileFormatUtils.getFileFormatBySuffix(path).orElse(source.getFileFormatFromTableProperties());
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
index 525593cbd75..adca1635ef6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/PaimonTableValuedFunction.java
@@ -167,17 +167,18 @@ public class PaimonTableValuedFunction extends 
MetadataTableValuedFunction {
 
     @Override
     public TMetaScanRange getMetaScanRange(List<String> requiredFileds) {
-        int[] projections = requiredFileds.stream().mapToInt(
-                        field -> paimonSysTable.rowType().getFieldNames()
-                                .stream()
-                                .map(String::toLowerCase)
-                                .collect(Collectors.toList())
-                                .indexOf(field))
+        List<String> paimonFieldNames = 
paimonSysTable.rowType().getFieldNames();
+        int[] projections = requiredFileds.stream()
+                .mapToInt(field -> getFieldIndex(paimonFieldNames, field))
                 .toArray();
         List<Split> splits;
         try {
-            splits = hadoopAuthenticator.execute(
-                    () -> 
paimonSysTable.newReadBuilder().withProjection(projections).newScan().plan().splits());
+            splits = hadoopAuthenticator.execute(() -> {
+                if (hasInvalidProjection(projections)) {
+                    return 
paimonSysTable.newReadBuilder().newScan().plan().splits();
+                }
+                return 
paimonSysTable.newReadBuilder().withProjection(projections).newScan().plan().splits();
+            });
         } catch (Exception e) {
             throw new RuntimeException(ExceptionUtils.getRootCauseMessage(e));
         }
@@ -191,6 +192,24 @@ public class PaimonTableValuedFunction extends 
MetadataTableValuedFunction {
         return tMetaScanRange;
     }
 
+    private static boolean hasInvalidProjection(int[] projections) {
+        for (int projection : projections) {
+            if (projection < 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static int getFieldIndex(List<String> fieldNames, String 
fieldName) {
+        for (int i = 0; i < fieldNames.size(); i++) {
+            if (fieldNames.get(i).equalsIgnoreCase(fieldName)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     @Override
     public String getTableName() {
         return "PaimonTableValuedFunction<" + queryType + ">";
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/CreateIcebergTableTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/CreateIcebergTableTest.java
index 3422100de0f..0a23870c80f 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/CreateIcebergTableTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/CreateIcebergTableTest.java
@@ -177,6 +177,28 @@ public class CreateIcebergTableTest {
         Assert.assertEquals("b", table.properties().get("a"));
     }
 
+    @Test
+    public void testPartitionPreservesNonLowercaseColumnNames() throws 
UserException {
+        TableIdentifier tb = TableIdentifier.of(dbName, getTableName());
+        String sql = "create table " + tb + " ("
+                + "data int, "
+                + "`PART` int, "
+                + "`mIxEd_COL` int"
+                + ") engine = iceberg "
+                + "partition by (`PART`, bucket(2, `mIxEd_COL`)) ()";
+        createTable(sql);
+        Table table = ops.getCatalog().loadTable(tb);
+        Schema schema = table.schema();
+
+        Assert.assertEquals("PART", schema.columns().get(1).name());
+        Assert.assertEquals("mIxEd_COL", schema.columns().get(2).name());
+        PartitionSpec spec = PartitionSpec.builderFor(schema)
+                .identity("PART")
+                .bucket("mIxEd_COL", 2)
+                .build();
+        Assert.assertEquals(spec, table.spec());
+    }
+
     public void createTable(String sql) throws UserException {
         LogicalPlan plan = new NereidsParser().parseSingle(sql);
         Assertions.assertTrue(plan instanceof CreateTableCommand);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
index e4ee0ec5e46..3f4828efb51 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
@@ -19,6 +19,7 @@ package org.apache.doris.datasource.iceberg;
 
 import org.apache.doris.analysis.TableScanParams;
 import org.apache.doris.analysis.TableSnapshot;
+import org.apache.doris.catalog.Column;
 import org.apache.doris.common.UserException;
 import org.apache.doris.datasource.iceberg.source.IcebergTableQueryInfo;
 
@@ -98,6 +99,18 @@ public class IcebergUtilsTest {
         return declaredField.getBoolean(hiveCatalog);
     }
 
+    @Test
+    public void testParseSchemaPreservesNonLowercaseColumnNames() {
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "mIxEd_COL", 
Types.IntegerType.get()),
+                Types.NestedField.required(2, "PART", Types.StringType.get()));
+
+        List<Column> columns = IcebergUtils.parseSchema(schema, false, false);
+
+        Assert.assertEquals("mIxEd_COL", columns.get(0).getName());
+        Assert.assertEquals("PART", columns.get(1).getName());
+    }
+
     @Test
     public void testGetMatchingManifest() {
 
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 e06b7dee7bf..046b38e311b 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
@@ -17,13 +17,24 @@
 
 package org.apache.doris.datasource.paimon;
 
+import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.Type;
 
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.table.Table;
 import org.apache.paimon.types.CharType;
 import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
 import org.apache.paimon.types.VarCharType;
 import org.junit.Assert;
 import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 
 public class PaimonUtilTest {
     @Test
@@ -36,4 +47,33 @@ public class PaimonUtilTest {
         Assert.assertEquals(32, type1.getLength());
         Assert.assertEquals(14, type2.getLength());
     }
+
+    @Test
+    public void testParseSchemaPreservesNonLowercaseColumnNames() {
+        RowType rowType = DataTypes.ROW(
+                DataTypes.FIELD(0, "mIxEd_COL", DataTypes.INT()),
+                DataTypes.FIELD(1, "PART", DataTypes.STRING()));
+
+        List<Column> columns = PaimonUtil.parseSchema(rowType, 
Collections.singletonList("PART"), false, false);
+
+        Assert.assertEquals("mIxEd_COL", columns.get(0).getName());
+        Assert.assertEquals("PART", columns.get(1).getName());
+        Assert.assertTrue(columns.get(1).isKey());
+    }
+
+    @Test
+    public void testGetPartitionInfoMapPreservesNonLowercaseKeys() {
+        DataField mixedCasePartition = DataTypes.FIELD(0, "Dt", 
DataTypes.STRING());
+        Table table = Mockito.mock(Table.class);
+        Mockito.when(table.name()).thenReturn("mock_table");
+        
Mockito.when(table.partitionKeys()).thenReturn(Collections.singletonList("Dt"));
+        
Mockito.when(table.rowType()).thenReturn(DataTypes.ROW(mixedCasePartition));
+
+        BinaryRow partitionValues = 
BinaryRow.singleColumn(BinaryString.fromString("2026-05-26"));
+
+        Map<String, String> partitionInfoMap = 
PaimonUtil.getPartitionInfoMap(table, partitionValues, "UTC");
+
+        Assert.assertFalse(partitionInfoMap.containsKey("dt"));
+        Assert.assertEquals("2026-05-26", partitionInfoMap.get("Dt"));
+    }
 }
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 454f2d42d9e..45d8c8909a4 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
@@ -21,8 +21,10 @@ import org.apache.doris.analysis.TupleDescriptor;
 import org.apache.doris.analysis.TupleId;
 import org.apache.doris.common.ExceptionChecker;
 import org.apache.doris.common.UserException;
+import org.apache.doris.datasource.CatalogProperty;
 import org.apache.doris.datasource.FileQueryScanNode;
 import org.apache.doris.datasource.FileSplitter;
+import org.apache.doris.datasource.paimon.PaimonExternalCatalog;
 import org.apache.doris.datasource.paimon.PaimonFileExternalCatalog;
 import org.apache.doris.planner.PlanNodeId;
 import org.apache.doris.planner.ScanContext;
@@ -43,6 +45,7 @@ import org.mockito.junit.MockitoJUnitRunner;
 
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -408,6 +411,68 @@ public class PaimonScanNodeTest {
         Assert.assertEquals(100L * 1024L * 1024L, target);
     }
 
+    @Test
+    public void testGetBackendPaimonOptionsForJniIOManager() {
+        Map<String, String> props = new HashMap<>();
+        props.put("paimon.doris.enable_jni_io_manager", "true");
+        props.put("paimon.doris.jni_io_manager.tmp_dir", "/tmp/doris-paimon");
+        props.put("paimon.doris.jni_io_manager.impl_class", 
"org.example.CustomIOManager");
+
+        CatalogProperty catalogProperty = Mockito.mock(CatalogProperty.class);
+        Mockito.when(catalogProperty.getProperties()).thenReturn(props);
+
+        PaimonExternalCatalog catalog = 
Mockito.mock(PaimonExternalCatalog.class);
+        Mockito.when(catalog.getCatalogProperty()).thenReturn(catalogProperty);
+
+        PaimonSource source = Mockito.mock(PaimonSource.class);
+        Mockito.when(source.getCatalog()).thenReturn(catalog);
+
+        PaimonScanNode node = new PaimonScanNode(new PlanNodeId(0),
+                new TupleDescriptor(new TupleId(0)), false, sv, 
ScanContext.EMPTY);
+        node.setSource(source);
+
+        Map<String, String> backendOptions = node.getBackendPaimonOptions();
+        Assert.assertEquals("true", 
backendOptions.get("doris.enable_jni_io_manager"));
+        Assert.assertEquals("/tmp/doris-paimon", 
backendOptions.get("doris.jni_io_manager.tmp_dir"));
+        Assert.assertEquals("org.example.CustomIOManager",
+                backendOptions.get("doris.jni_io_manager.impl_class"));
+        Assert.assertEquals(3, backendOptions.size());
+    }
+
+    @Test
+    public void testGetBackendPaimonOptionsForJdbcDriver() {
+        Map<String, String> props = new HashMap<>();
+        props.put("paimon.jdbc.driver_url", "file:///tmp/postgresql.jar");
+        props.put("paimon.jdbc.driver_class", "org.postgresql.Driver");
+
+        CatalogProperty catalogProperty = Mockito.mock(CatalogProperty.class);
+        Mockito.when(catalogProperty.getProperties()).thenReturn(props);
+
+        PaimonExternalCatalog catalog = 
Mockito.mock(PaimonExternalCatalog.class);
+        Mockito.when(catalog.getCatalogProperty()).thenReturn(catalogProperty);
+
+        PaimonSource source = Mockito.mock(PaimonSource.class);
+        Mockito.when(source.getCatalog()).thenReturn(catalog);
+
+        PaimonScanNode node = new PaimonScanNode(new PlanNodeId(0),
+                new TupleDescriptor(new TupleId(0)), false, sv, 
ScanContext.EMPTY);
+        node.setSource(source);
+
+        Map<String, String> backendOptions = node.getBackendPaimonOptions();
+        Assert.assertEquals("file:///tmp/postgresql.jar", 
backendOptions.get("jdbc.driver_url"));
+        Assert.assertEquals("org.postgresql.Driver", 
backendOptions.get("jdbc.driver_class"));
+        Assert.assertEquals(2, backendOptions.size());
+    }
+
+    @Test
+    public void testGetFieldIndexMatchesMixedCaseColumns() {
+        List<String> fieldNames = Arrays.asList("data", "mIxEd_COL", "PART");
+
+        Assert.assertEquals(1, PaimonScanNode.getFieldIndex(fieldNames, 
"mixed_col"));
+        Assert.assertEquals(2, PaimonScanNode.getFieldIndex(fieldNames, 
"part"));
+        Assert.assertEquals(-1, PaimonScanNode.getFieldIndex(fieldNames, 
"missing_col"));
+    }
+
     private void mockJniReader(PaimonScanNode spyNode) {
         
Mockito.doReturn(false).when(spyNode).supportNativeReader(ArgumentMatchers.any(Optional.class));
     }
diff --git 
a/regression-test/data/external_table_p0/iceberg/test_iceberg_invaild_avro_name.out
 
b/regression-test/data/external_table_p0/iceberg/test_iceberg_invaild_avro_name.out
index 1b8af743c6a..be0c0e22364 100644
--- 
a/regression-test/data/external_table_p0/iceberg/test_iceberg_invaild_avro_name.out
+++ 
b/regression-test/data/external_table_p0/iceberg/test_iceberg_invaild_avro_name.out
@@ -1,7 +1,7 @@
 -- This file is automatically generated. You should know what you did if you 
want to edit this
 -- !desc --
 id     int     Yes     true    \N      
-test:a1b2.raw.abc-gg-1-a       text    Yes     true    \N      
+TEST:A1B2.RAW.ABC-GG-1-A       text    Yes     true    \N      
 
 -- !q_1 --
 1      row1
@@ -29,7 +29,7 @@ test:a1b2.raw.abc-gg-1-a      text    Yes     true    \N
 
 -- !desc --
 id     int     Yes     true    \N      
-test:a1b2.raw.abc-gg-1-a       text    Yes     true    \N      
+TEST:A1B2.RAW.ABC-GG-1-A       text    Yes     true    \N      
 
 -- !q_1 --
 1      row1
@@ -54,4 +54,3 @@ test:a1b2.raw.abc-gg-1-a      text    Yes     true    \N
 3      row3
 2      row2
 1      row1
-
diff --git 
a/regression-test/suites/external_table_p0/paimon/test_paimon_catalog.groovy 
b/regression-test/suites/external_table_p0/paimon/test_paimon_catalog.groovy
index b5ca34e5a18..91d38cf7294 100644
--- a/regression-test/suites/external_table_p0/paimon/test_paimon_catalog.groovy
+++ b/regression-test/suites/external_table_p0/paimon/test_paimon_catalog.groovy
@@ -307,7 +307,7 @@ suite("test_paimon_catalog", 
"p0,external,doris,external_docker,external_docker_
 
         test {
             sql """select * from dup_columns_table;"""
-            exception "Duplicate column name found: id"
+            exception "Duplicate column name found: ID"
         }
 
         sql """ set force_jni_scanner=false; """
@@ -332,4 +332,3 @@ suite("test_paimon_catalog", 
"p0,external,doris,external_docker,external_docker_
     }
 }
 
-


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


Reply via email to