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

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


The following commit(s) were added to refs/heads/branch-3.1 by this push:
     new 69e909b4404 branch-3.1: [Enhancement](external) Support downloading 
dependency packages from the cloud #54304 (#56738)
69e909b4404 is described below

commit 69e909b4404fc22193af8a7a235c01636db74563
Author: zy-kkk <[email protected]>
AuthorDate: Sat Oct 11 11:30:31 2025 +0800

    branch-3.1: [Enhancement](external) Support downloading dependency packages 
from the cloud #54304 (#56738)
    
    picked from #54304
---
 be/src/runtime/plugin/cloud_plugin_downloader.cpp  | 149 +++++++++++++
 be/src/runtime/plugin/cloud_plugin_downloader.h    |  67 ++++++
 be/src/service/internal_service.cpp                |   4 +
 be/src/util/path_util.cpp                          |  36 ++-
 .../plugin/cloud_plugin_downloader_test.cpp        | 241 +++++++++++++++++++++
 be/test/runtime/user_function_cache_test.cpp       |  26 ++-
 .../apache/doris/analysis/CreateFunctionStmt.java  |  47 +++-
 .../org/apache/doris/catalog/JdbcResource.java     |  25 ++-
 .../doris/common/plugin/CloudPluginDownloader.java | 164 ++++++++++++++
 .../common/plugin/CloudPluginDownloaderTest.java   | 173 +++++++++++++++
 .../test_cloud_plugin_auto_download.groovy         | 121 +++++++++++
 11 files changed, 1032 insertions(+), 21 deletions(-)

diff --git a/be/src/runtime/plugin/cloud_plugin_downloader.cpp 
b/be/src/runtime/plugin/cloud_plugin_downloader.cpp
new file mode 100644
index 00000000000..6b24dc1efc1
--- /dev/null
+++ b/be/src/runtime/plugin/cloud_plugin_downloader.cpp
@@ -0,0 +1,149 @@
+// 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 "runtime/plugin/cloud_plugin_downloader.h"
+
+#include <fmt/format.h>
+
+#include "cloud/cloud_storage_engine.h"
+#include "io/fs/local_file_system.h"
+#include "io/fs/remote_file_system.h"
+#include "runtime/exec_env.h"
+
+namespace doris {
+
+// Use 10MB buffer for all downloads - same as cloud_warm_up_manager
+static constexpr size_t DOWNLOAD_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
+
+// Static mutex definition for synchronizing downloads
+std::mutex CloudPluginDownloader::_download_mutex;
+
+Status CloudPluginDownloader::download_from_cloud(PluginType type, const 
std::string& name,
+                                                  const std::string& 
local_path,
+                                                  std::string* result_path) {
+    // Use lock_guard to synchronize concurrent downloads
+    std::lock_guard<std::mutex> lock(_download_mutex);
+
+    if (name.empty()) {
+        return Status::InvalidArgument("Plugin name cannot be empty");
+    }
+
+    CloudPluginDownloader downloader;
+
+    // 1. Get FileSystem
+    io::RemoteFileSystemSPtr filesystem;
+    RETURN_IF_ERROR(downloader._get_cloud_filesystem(&filesystem));
+
+    // 2. Build remote plugin path
+    std::string remote_path;
+    RETURN_IF_ERROR(downloader._build_plugin_path(type, name, &remote_path));
+    LOG(INFO) << "Downloading plugin: " << name << " -> " << local_path;
+
+    // 3. Prepare local environment
+    RETURN_IF_ERROR(downloader._prepare_local_path(local_path));
+
+    // 4. Download remote file to local
+    RETURN_IF_ERROR(downloader._download_remote_file(filesystem, remote_path, 
local_path));
+
+    *result_path = local_path;
+    LOG(INFO) << "Successfully downloaded plugin: " << name << " to " << 
local_path;
+
+    return Status::OK();
+}
+
+Status CloudPluginDownloader::_build_plugin_path(PluginType type, const 
std::string& name,
+                                                 std::string* path) {
+    std::string type_name;
+    switch (type) {
+    case PluginType::JDBC_DRIVERS:
+        type_name = "jdbc_drivers";
+        break;
+    case PluginType::JAVA_UDF:
+        type_name = "java_udf";
+        break;
+    default:
+        return Status::InvalidArgument("Unsupported plugin type: {}", 
static_cast<int>(type));
+    }
+    *path = fmt::format("plugins/{}/{}", type_name, name);
+    return Status::OK();
+}
+
+Status CloudPluginDownloader::_get_cloud_filesystem(io::RemoteFileSystemSPtr* 
filesystem) {
+    BaseStorageEngine& base_engine = ExecEnv::GetInstance()->storage_engine();
+    CloudStorageEngine* cloud_engine = 
dynamic_cast<CloudStorageEngine*>(&base_engine);
+    if (!cloud_engine) {
+        return Status::NotFound("CloudStorageEngine not found, not in cloud 
mode");
+    }
+
+    *filesystem = cloud_engine->latest_fs();
+    if (!*filesystem) {
+        return Status::NotFound("No latest filesystem available in cloud 
mode");
+    }
+
+    return Status::OK();
+}
+
+Status CloudPluginDownloader::_prepare_local_path(const std::string& 
local_path) {
+    // Remove existing file if present
+    bool exists = false;
+    RETURN_IF_ERROR(io::global_local_filesystem()->exists(local_path, 
&exists));
+    if (exists) {
+        
RETURN_IF_ERROR(io::global_local_filesystem()->delete_file(local_path));
+        LOG(INFO) << "Removed existing file: " << local_path;
+    }
+
+    // Ensure local directory exists
+    std::string dir_path = local_path.substr(0, local_path.find_last_of('/'));
+    if (!dir_path.empty()) {
+        
RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(dir_path));
+    }
+
+    return Status::OK();
+}
+
+Status CloudPluginDownloader::_download_remote_file(io::RemoteFileSystemSPtr 
filesystem,
+                                                    const std::string& 
remote_path,
+                                                    const std::string& 
local_path) {
+    // Open remote file for reading
+    io::FileReaderSPtr remote_reader;
+    io::FileReaderOptions opts;
+    RETURN_IF_ERROR(filesystem->open_file(remote_path, &remote_reader, &opts));
+
+    // Get file size
+    int64_t file_size;
+    RETURN_IF_ERROR(filesystem->file_size(remote_path, &file_size));
+
+    // Create local file writer
+    io::FileWriterPtr local_writer;
+    RETURN_IF_ERROR(io::global_local_filesystem()->create_file(local_path, 
&local_writer));
+
+    auto buffer = std::make_unique<char[]>(DOWNLOAD_BUFFER_SIZE);
+    size_t total_read = 0;
+    while (total_read < file_size) {
+        size_t to_read =
+                std::min(DOWNLOAD_BUFFER_SIZE, static_cast<size_t>(file_size - 
total_read));
+        size_t bytes_read;
+        RETURN_IF_ERROR(remote_reader->read_at(total_read, {buffer.get(), 
to_read}, &bytes_read));
+        RETURN_IF_ERROR(local_writer->append({buffer.get(), bytes_read}));
+        total_read += bytes_read;
+    }
+
+    RETURN_IF_ERROR(local_writer->close());
+    return Status::OK();
+}
+
+} // namespace doris
\ No newline at end of file
diff --git a/be/src/runtime/plugin/cloud_plugin_downloader.h 
b/be/src/runtime/plugin/cloud_plugin_downloader.h
new file mode 100644
index 00000000000..cd6b5b38e72
--- /dev/null
+++ b/be/src/runtime/plugin/cloud_plugin_downloader.h
@@ -0,0 +1,67 @@
+// 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 <memory>
+#include <mutex>
+#include <string>
+
+#include "common/status.h"
+
+namespace doris::io {
+class RemoteFileSystem;
+using RemoteFileSystemSPtr = std::shared_ptr<RemoteFileSystem>;
+} // namespace doris::io
+
+namespace doris {
+
+/**
+ * Cloud plugin downloader with testable design
+ * Uses friend class pattern for easy unit testing
+ */
+class CloudPluginDownloader {
+public:
+    enum class PluginType { JDBC_DRIVERS, JAVA_UDF, CONNECTORS, HADOOP_CONF };
+
+    /**
+     * Download plugin from cloud storage to local path
+     */
+    static Status download_from_cloud(PluginType type, const std::string& name,
+                                      const std::string& local_path, 
std::string* result_path);
+
+private:
+    friend class CloudPluginDownloaderTest;
+
+    // Build remote plugin path: plugins/{type}/{name}
+    Status _build_plugin_path(PluginType type, const std::string& name, 
std::string* path);
+
+    // Get cloud filesystem
+    Status _get_cloud_filesystem(io::RemoteFileSystemSPtr* filesystem);
+
+    // Prepare local environment (remove existing file, create directory)
+    Status _prepare_local_path(const std::string& local_path);
+
+    // High-performance file download using 10MB buffer
+    Status _download_remote_file(io::RemoteFileSystemSPtr remote_fs, const 
std::string& remote_path,
+                                 const std::string& local_path);
+
+    // Static mutex for synchronizing concurrent downloads
+    static std::mutex _download_mutex;
+};
+
+} // namespace doris
\ No newline at end of file
diff --git a/be/src/service/internal_service.cpp 
b/be/src/service/internal_service.cpp
index a1c5c98ce45..08874674544 100644
--- a/be/src/service/internal_service.cpp
+++ b/be/src/service/internal_service.cpp
@@ -972,6 +972,10 @@ void 
PInternalService::test_jdbc_connection(google::protobuf::RpcController* con
     bool ret = _heavy_work_pool.try_offer([request, result, done]() {
         VLOG_RPC << "test jdbc connection";
         brpc::ClosureGuard closure_guard(done);
+        std::shared_ptr<MemTrackerLimiter> mem_tracker = 
MemTrackerLimiter::create_shared(
+                MemTrackerLimiter::Type::OTHER,
+                fmt::format("InternalService::test_jdbc_connection"));
+        SCOPED_ATTACH_TASK(mem_tracker);
         TTableDescriptor table_desc;
         vectorized::JdbcConnectorParam jdbc_param;
         Status st = Status::OK();
diff --git a/be/src/util/path_util.cpp b/be/src/util/path_util.cpp
index 4b6f6adcbed..0f0de43d8bf 100644
--- a/be/src/util/path_util.cpp
+++ b/be/src/util/path_util.cpp
@@ -23,9 +23,11 @@
 #include <cstdlib>
 #include <filesystem>
 
+#include "cloud/config.h"
 #include "common/config.h"
 #include "gutil/strings/split.h"
 #include "gutil/strings/strip.h"
+#include "runtime/plugin/cloud_plugin_downloader.h"
 
 using std::string;
 using std::vector;
@@ -99,13 +101,37 @@ std::string check_and_return_default_plugin_url(const 
std::string& url,
         // Because in 2.1.8, we change the default value of `jdbc_drivers_dir`
         // from `DORIS_HOME/jdbc_drivers` to `DORIS_HOME/plugins/jdbc_drivers`,
         // so we need to check the old default dir for compatibility.
-        std::filesystem::path file = default_url + "/" + url;
-        if (std::filesystem::exists(file)) {
-            return "file://" + default_url + "/" + url;
-        } else {
-            return "file://" + default_old_url + "/" + url;
+        std::string target_path = default_url + "/" + url;
+        if (std::filesystem::exists(target_path)) {
+            // File exists in new default directory
+            return "file://" + target_path;
+        } else if (config::is_cloud_mode()) {
+            // Cloud mode: try to download from cloud to new default directory
+            CloudPluginDownloader::PluginType plugin_type;
+            if (plugin_dir_name == "jdbc_drivers") {
+                plugin_type = CloudPluginDownloader::PluginType::JDBC_DRIVERS;
+            } else if (plugin_dir_name == "java_udf") {
+                plugin_type = CloudPluginDownloader::PluginType::JAVA_UDF;
+            } else {
+                // Unknown plugin type, fallback to old directory
+                return "file://" + default_old_url + "/" + url;
+            }
+
+            std::string downloaded_path;
+            Status status = CloudPluginDownloader::download_from_cloud(
+                    plugin_type, url, target_path, &downloaded_path);
+            if (status.ok() && !downloaded_path.empty()) {
+                return "file://" + downloaded_path;
+            }
+            // Download failed, log warning but continue to fallback
+            LOG(WARNING) << "Failed to download plugin from cloud: " << 
status.to_string()
+                         << ", fallback to old directory";
         }
+
+        // Fallback to old default directory for compatibility
+        return "file://" + default_old_url + "/" + url;
     } else {
+        // User specified custom directory - use directly
         return "file://" + plugin_dir_config_value + "/" + url;
     }
 }
diff --git a/be/test/runtime/plugin/cloud_plugin_downloader_test.cpp 
b/be/test/runtime/plugin/cloud_plugin_downloader_test.cpp
new file mode 100644
index 00000000000..1d083939bab
--- /dev/null
+++ b/be/test/runtime/plugin/cloud_plugin_downloader_test.cpp
@@ -0,0 +1,241 @@
+// 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 "runtime/plugin/cloud_plugin_downloader.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <filesystem>
+#include <fstream>
+
+#include "cloud/cloud_storage_engine.h"
+#include "olap/storage_engine.h"
+#include "runtime/exec_env.h"
+
+namespace doris {
+
+class CloudPluginDownloaderTest : public ::testing::Test {
+protected:
+    void SetUp() override { downloader = 
std::make_unique<CloudPluginDownloader>(); }
+
+    void TearDown() override {
+        downloader.reset();
+        ExecEnv::GetInstance()->set_storage_engine(nullptr);
+    }
+
+    void SetupRegularStorageEngine() {
+        ExecEnv::GetInstance()->set_storage_engine(
+                std::make_unique<StorageEngine>(EngineOptions()));
+    }
+
+    void SetupCloudStorageEngine() {
+        ExecEnv::GetInstance()->set_storage_engine(
+                std::make_unique<CloudStorageEngine>(EngineOptions()));
+    }
+
+    std::unique_ptr<CloudPluginDownloader> downloader;
+};
+
+// ============== Core Business Logic Tests ==============
+
+TEST_F(CloudPluginDownloaderTest, TestBuildPluginPath) {
+    std::string path;
+    Status status = 
downloader->_build_plugin_path(CloudPluginDownloader::PluginType::JDBC_DRIVERS,
+                                                   "mysql-connector.jar", 
&path);
+    EXPECT_TRUE(status.ok());
+    EXPECT_EQ("plugins/jdbc_drivers/mysql-connector.jar", path);
+
+    status = 
downloader->_build_plugin_path(CloudPluginDownloader::PluginType::JAVA_UDF,
+                                            "my-udf.jar", &path);
+    EXPECT_TRUE(status.ok());
+    EXPECT_EQ("plugins/java_udf/my-udf.jar", path);
+}
+
+TEST_F(CloudPluginDownloaderTest, TestBuildPluginPathEdgeCases) {
+    std::string path;
+    Status status = 
downloader->_build_plugin_path(CloudPluginDownloader::PluginType::JDBC_DRIVERS,
+                                                   "test-file_v1.2.jar", 
&path);
+    EXPECT_TRUE(status.ok());
+    EXPECT_EQ("plugins/jdbc_drivers/test-file_v1.2.jar", path);
+
+    status = 
downloader->_build_plugin_path(CloudPluginDownloader::PluginType::JAVA_UDF,
+                                            "sub/dir/file.jar", &path);
+    EXPECT_TRUE(status.ok());
+    EXPECT_EQ("plugins/java_udf/sub/dir/file.jar", path);
+
+    std::string long_name(100, 'a');
+    long_name += ".jar";
+    status = 
downloader->_build_plugin_path(CloudPluginDownloader::PluginType::JDBC_DRIVERS,
+                                            long_name, &path);
+    EXPECT_TRUE(status.ok());
+    std::string expected = "plugins/jdbc_drivers/" + long_name;
+    EXPECT_EQ(expected, path);
+}
+
+TEST_F(CloudPluginDownloaderTest, TestBuildPluginPathUnsupportedTypes) {
+    std::string path;
+    Status status = 
downloader->_build_plugin_path(CloudPluginDownloader::PluginType::CONNECTORS,
+                                                   "kafka-connector.jar", 
&path);
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT);
+    EXPECT_THAT(status.msg(), testing::HasSubstr("Unsupported plugin type"));
+
+    status = 
downloader->_build_plugin_path(CloudPluginDownloader::PluginType::HADOOP_CONF,
+                                            "core-site.xml", &path);
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT);
+    EXPECT_THAT(status.msg(), testing::HasSubstr("Unsupported plugin type"));
+}
+
+TEST_F(CloudPluginDownloaderTest, TestBuildPluginPathInvalidType) {
+    CloudPluginDownloader::PluginType invalid_type =
+            static_cast<CloudPluginDownloader::PluginType>(999);
+    std::string path;
+    Status status = downloader->_build_plugin_path(invalid_type, "test.jar", 
&path);
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT);
+    EXPECT_THAT(status.msg(), testing::HasSubstr("Unsupported plugin type"));
+}
+
+// ============== Storage Engine Integration Tests ==============
+
+TEST_F(CloudPluginDownloaderTest, TestDownloadFromCloudEmptyName) {
+    std::string result_path;
+    Status status = CloudPluginDownloader::download_from_cloud(
+            CloudPluginDownloader::PluginType::JDBC_DRIVERS, "", 
"/tmp/test.jar", &result_path);
+
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT);
+    EXPECT_EQ("Plugin name cannot be empty", status.msg());
+}
+
+TEST_F(CloudPluginDownloaderTest, TestDownloadFromCloudRegularStorageEngine) {
+    SetupRegularStorageEngine();
+
+    std::string result_path;
+    Status status = CloudPluginDownloader::download_from_cloud(
+            CloudPluginDownloader::PluginType::JDBC_DRIVERS, "mysql.jar", 
"/tmp/test.jar",
+            &result_path);
+
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(status.code(), ErrorCode::NOT_FOUND);
+    EXPECT_TRUE(status.to_string().find("CloudStorageEngine not found") != 
std::string::npos);
+}
+
+TEST_F(CloudPluginDownloaderTest, TestGetCloudFilesystemNonCloudEnvironment) {
+    SetupRegularStorageEngine();
+
+    io::RemoteFileSystemSPtr filesystem;
+    Status status = downloader->_get_cloud_filesystem(&filesystem);
+
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(status.code(), ErrorCode::NOT_FOUND);
+    EXPECT_TRUE(status.to_string().find("CloudStorageEngine not found") != 
std::string::npos);
+}
+
+TEST_F(CloudPluginDownloaderTest, TestGetCloudFilesystemNoStorageEngine) {
+    SetupRegularStorageEngine();
+
+    io::RemoteFileSystemSPtr filesystem;
+    Status status = downloader->_get_cloud_filesystem(&filesystem);
+
+    EXPECT_FALSE(status.ok());
+    EXPECT_EQ(status.code(), ErrorCode::NOT_FOUND);
+}
+
+TEST_F(CloudPluginDownloaderTest, TestGetCloudFilesystemCloudEnvironment) {
+    SetupCloudStorageEngine();
+    io::RemoteFileSystemSPtr filesystem;
+    Status status = downloader->_get_cloud_filesystem(&filesystem);
+    if (!status.ok()) {
+        EXPECT_EQ(status.code(), ErrorCode::NOT_FOUND);
+        EXPECT_TRUE(status.to_string().find("No latest filesystem available") 
!= std::string::npos);
+    }
+}
+
+TEST_F(CloudPluginDownloaderTest, TestDownloadFromCloudCloudStorageEngine) {
+    SetupCloudStorageEngine();
+    std::string result_path;
+    Status status = CloudPluginDownloader::download_from_cloud(
+            CloudPluginDownloader::PluginType::JDBC_DRIVERS, "mysql.jar", 
"/tmp/test.jar",
+            &result_path);
+    EXPECT_FALSE(status.ok());
+}
+
+// ============== File System Tests ==============
+
+TEST_F(CloudPluginDownloaderTest, TestPrepareLocalPathSuccess) {
+    std::string test_path = "/tmp/test_new_file.jar";
+    Status status = downloader->_prepare_local_path(test_path);
+    EXPECT_TRUE(status.ok());
+}
+
+TEST_F(CloudPluginDownloaderTest, TestPrepareLocalPathWithExistingFile) {
+    std::string existing_file = "/tmp/existing_file.jar";
+    std::ofstream file(existing_file);
+    file << "existing content";
+    file.close();
+    EXPECT_TRUE(std::filesystem::exists(existing_file));
+
+    Status status = downloader->_prepare_local_path(existing_file);
+    EXPECT_TRUE(status.ok());
+    EXPECT_FALSE(std::filesystem::exists(existing_file));
+}
+
+TEST_F(CloudPluginDownloaderTest, TestPrepareLocalPathWithNestedDirectory) {
+    std::string nested_path = "/tmp/test_dir/nested/test.jar";
+    Status status = downloader->_prepare_local_path(nested_path);
+    EXPECT_TRUE(status.ok());
+    EXPECT_TRUE(std::filesystem::exists("/tmp/test_dir/nested"));
+    std::filesystem::remove_all("/tmp/test_dir");
+}
+
+TEST_F(CloudPluginDownloaderTest, TestPrepareLocalPathRootDirectory) {
+    std::string root_path = "/test_root.jar";
+    Status status = downloader->_prepare_local_path(root_path);
+    EXPECT_TRUE(status.ok());
+    std::filesystem::remove(root_path);
+}
+
+// ============== Consistency Tests ==============
+
+TEST_F(CloudPluginDownloaderTest, TestAllTypePathCombinations) {
+    struct TestCase {
+        CloudPluginDownloader::PluginType type;
+        std::string name;
+        std::string expected;
+    };
+
+    std::vector<TestCase> test_cases = {
+            {CloudPluginDownloader::PluginType::JDBC_DRIVERS, "driver.jar",
+             "plugins/jdbc_drivers/driver.jar"},
+            {CloudPluginDownloader::PluginType::JAVA_UDF, "udf.jar", 
"plugins/java_udf/udf.jar"},
+    };
+
+    for (const auto& test_case : test_cases) {
+        std::string result;
+        Status status = downloader->_build_plugin_path(test_case.type, 
test_case.name, &result);
+        EXPECT_TRUE(status.ok()) << "Failed for type: " << 
static_cast<int>(test_case.type)
+                                 << ", name: " << test_case.name << ", error: 
" << status.msg();
+        EXPECT_EQ(test_case.expected, result)
+                << "Failed for type: " << static_cast<int>(test_case.type)
+                << ", name: " << test_case.name;
+    }
+}
+
+} // namespace doris
\ No newline at end of file
diff --git a/be/test/runtime/user_function_cache_test.cpp 
b/be/test/runtime/user_function_cache_test.cpp
index b9a4694982d..a17ef2bd4ed 100644
--- a/be/test/runtime/user_function_cache_test.cpp
+++ b/be/test/runtime/user_function_cache_test.cpp
@@ -17,18 +17,36 @@
 
 #include "runtime/user_function_cache.h"
 
-#include <gtest/gtest-message.h>
-#include <gtest/gtest-test-part.h>
+#include <gtest/gtest.h>
 
+#include <cstdlib>
 #include <string>
+#include <vector>
 
-#include "gtest/gtest.h"
+#include "common/status.h"
 
 namespace doris {
 
 class UserFunctionCacheTest : public ::testing::Test {
 protected:
     UserFunctionCache ufc;
+
+    void SetUp() override {
+        // Save original DORIS_HOME
+        original_doris_home_ = getenv("DORIS_HOME");
+    }
+
+    void TearDown() override {
+        // Restore original DORIS_HOME
+        if (original_doris_home_) {
+            setenv("DORIS_HOME", original_doris_home_, 1);
+        } else {
+            unsetenv("DORIS_HOME");
+        }
+    }
+
+private:
+    const char* original_doris_home_ = nullptr;
 };
 
 TEST_F(UserFunctionCacheTest, SplitStringByChecksumTest) {
@@ -43,4 +61,4 @@ TEST_F(UserFunctionCacheTest, SplitStringByChecksumTest) {
     EXPECT_EQ(result[3], "jar");
 }
 
-} // namespace doris
+} // namespace doris
\ No newline at end of file
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateFunctionStmt.java 
b/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateFunctionStmt.java
index dab9c0b6b7a..4596aa63503 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateFunctionStmt.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateFunctionStmt.java
@@ -31,10 +31,12 @@ import org.apache.doris.catalog.StructType;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.Config;
+import org.apache.doris.common.EnvUtils;
 import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.UserException;
+import org.apache.doris.common.plugin.CloudPluginDownloader;
 import org.apache.doris.common.util.URI;
 import org.apache.doris.common.util.Util;
 import org.apache.doris.mysql.privilege.PrivPredicate;
@@ -119,6 +121,7 @@ public class CreateFunctionStmt extends DdlStmt implements 
NotFallbackInParser {
 
     // needed item set after analyzed
     private String userFile;
+    private String originalUserFile; // Keep original jar name for BE
     private Function function;
     private String checksum = "";
     private boolean isStaticLoad = false;
@@ -268,6 +271,11 @@ public class CreateFunctionStmt extends DdlStmt implements 
NotFallbackInParser {
         }
 
         userFile = properties.getOrDefault(FILE_KEY, 
properties.get(OBJECT_FILE_KEY));
+        originalUserFile = userFile; // Keep original jar name for BE
+        // Convert userFile to realUrl only for FE checksum calculation
+        if (!Strings.isNullOrEmpty(userFile) && binaryType != 
TFunctionBinaryType.RPC) {
+            userFile = getRealUrl(userFile);
+        }
         if (!Strings.isNullOrEmpty(userFile) && binaryType != 
TFunctionBinaryType.RPC) {
             try {
                 computeObjectChecksum();
@@ -336,6 +344,33 @@ public class CreateFunctionStmt extends DdlStmt implements 
NotFallbackInParser {
         }
     }
 
+    private String getRealUrl(String url) {
+        if (!url.contains(":/")) {
+            return checkAndReturnDefaultJavaUdfUrl(url);
+        }
+        return url;
+    }
+
+    private String checkAndReturnDefaultJavaUdfUrl(String url) {
+        String defaultUrl = EnvUtils.getDorisHome() + "/plugins/java_udf";
+        // In cloud mode, try cloud download first
+        if (Config.isCloudMode()) {
+            String targetPath = defaultUrl + "/" + url;
+            try {
+                String downloadedPath = 
CloudPluginDownloader.downloadFromCloud(
+                        CloudPluginDownloader.PluginType.JAVA_UDF, url, 
targetPath);
+                if (!downloadedPath.isEmpty()) {
+                    return "file://" + downloadedPath;
+                }
+            } catch (Exception e) {
+                throw new RuntimeException("Cannot download UDF from cloud: " 
+ url
+                        + ". Please retry later or check your UDF has been 
uploaded to cloud.");
+            }
+        }
+        // Return the file path (original UDF behavior)
+        return "file://" + defaultUrl + "/" + url;
+    }
+
     private void analyzeTableFunction() throws AnalysisException {
         String symbol = properties.get(SYMBOL_KEY);
         if (Strings.isNullOrEmpty(symbol)) {
@@ -346,8 +381,8 @@ public class CreateFunctionStmt extends DdlStmt implements 
NotFallbackInParser {
         }
         analyzeJavaUdf(symbol);
         URI location;
-        if (!Strings.isNullOrEmpty(userFile)) {
-            location = URI.create(userFile);
+        if (!Strings.isNullOrEmpty(originalUserFile)) {
+            location = URI.create(originalUserFile);
         } else {
             location = null;
         }
@@ -366,8 +401,8 @@ public class CreateFunctionStmt extends DdlStmt implements 
NotFallbackInParser {
         AggregateFunction.AggregateFunctionBuilder builder
                 = 
AggregateFunction.AggregateFunctionBuilder.createUdfBuilder();
         URI location;
-        if (!Strings.isNullOrEmpty(userFile)) {
-            location = URI.create(userFile);
+        if (!Strings.isNullOrEmpty(originalUserFile)) {
+            location = URI.create(originalUserFile);
         } else {
             location = null;
         }
@@ -443,8 +478,8 @@ public class CreateFunctionStmt extends DdlStmt implements 
NotFallbackInParser {
             analyzeJavaUdf(symbol);
         }
         URI location;
-        if (!Strings.isNullOrEmpty(userFile)) {
-            location = URI.create(userFile);
+        if (!Strings.isNullOrEmpty(originalUserFile)) {
+            location = URI.create(originalUserFile);
         } else {
             location = null;
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
index 2989e8859bf..5df50b09bbf 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
@@ -23,6 +23,8 @@ import org.apache.doris.common.Config;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.EnvUtils;
 import org.apache.doris.common.FeConstants;
+import org.apache.doris.common.plugin.CloudPluginDownloader;
+import org.apache.doris.common.plugin.CloudPluginDownloader.PluginType;
 import org.apache.doris.common.proc.BaseProcResult;
 import org.apache.doris.common.util.Util;
 import org.apache.doris.datasource.ExternalCatalog;
@@ -330,13 +332,24 @@ public class JdbcResource extends Resource {
             // Because in new version, we change the default value of 
`jdbc_drivers_dir`
             // from `DORIS_HOME/jdbc_drivers` to 
`DORIS_HOME/plugins/jdbc_drivers`,
             // so we need to check the old default dir for compatibility.
-            File file = new File(defaultDriverUrl + "/" + driverUrl);
-            if (file.exists()) {
-                return "file://" + defaultDriverUrl + "/" + driverUrl;
-            } else {
-                // use old one
-                return "file://" + defaultOldDriverUrl + "/" + driverUrl;
+            String targetPath = defaultDriverUrl + "/" + driverUrl;
+            File targetFile = new File(targetPath);
+            if (targetFile.exists()) {
+                // File exists in new default directory
+                return "file://" + targetPath;
+            } else if (Config.isCloudMode()) {
+                // Cloud mode: download from cloud to default directory
+                try {
+                    String downloadedPath = 
CloudPluginDownloader.downloadFromCloud(
+                            PluginType.JDBC_DRIVERS, driverUrl, targetPath);
+                    return "file://" + downloadedPath;
+                } catch (Exception e) {
+                    throw new RuntimeException("Cannot download JDBC driver 
from cloud: " + driverUrl
+                            + ". Please retry later or check your driver has 
been uploaded to cloud.");
+                }
             }
+            // Fallback to old default directory for compatibility
+            return "file://" + defaultOldDriverUrl + "/" + driverUrl;
         } else {
             // Return user specified driver url directly.
             return "file://" + Config.jdbc_drivers_dir + "/" + driverUrl;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/plugin/CloudPluginDownloader.java
 
b/fe/fe-core/src/main/java/org/apache/doris/common/plugin/CloudPluginDownloader.java
new file mode 100644
index 00000000000..967a282026b
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/common/plugin/CloudPluginDownloader.java
@@ -0,0 +1,164 @@
+// 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.common.plugin;
+
+import org.apache.doris.backup.Status;
+import org.apache.doris.cloud.proto.Cloud;
+import org.apache.doris.cloud.rpc.MetaServiceProxy;
+import org.apache.doris.common.UserException;
+import 
org.apache.doris.datasource.property.storage.AbstractS3CompatibleProperties;
+import org.apache.doris.datasource.property.storage.StorageProperties;
+import org.apache.doris.fs.obj.S3ObjStorage;
+
+import com.google.common.base.Strings;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Simple cloud plugin downloader for UDF and JDBC drivers.
+ */
+public class CloudPluginDownloader {
+
+    public enum PluginType {
+        JDBC_DRIVERS,
+        JAVA_UDF,
+        CONNECTORS,     // Reserved, not supported yet
+        HADOOP_CONF     // Reserved, not supported yet
+    }
+
+    /**
+     * Download plugin from cloud storage to local path
+     */
+    public static synchronized String downloadFromCloud(PluginType type, 
String name, String localPath) {
+        validateInput(type, name);
+        try {
+            Cloud.ObjectStoreInfoPB objInfo = getCloudStorageInfo();
+            String remotePath = buildS3Path(objInfo, type, name);
+            return doDownload(objInfo, remotePath, localPath);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to download plugin: " + 
e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Validate input parameters
+     */
+    static void validateInput(PluginType type, String name) {
+        if (Strings.isNullOrEmpty(name)) {
+            throw new IllegalArgumentException("Plugin name cannot be empty");
+        }
+
+        if (type != PluginType.JDBC_DRIVERS && type != PluginType.JAVA_UDF) {
+            throw new UnsupportedOperationException("Plugin type " + type + " 
is not supported yet");
+        }
+    }
+
+    /**
+     * Get cloud storage info from MetaService
+     * Package-private for testing
+     */
+    static Cloud.ObjectStoreInfoPB getCloudStorageInfo() throws Exception {
+        Cloud.GetObjStoreInfoResponse response = MetaServiceProxy.getInstance()
+                
.getObjStoreInfo(Cloud.GetObjStoreInfoRequest.newBuilder().build());
+
+        if (response.getStatus().getCode() != Cloud.MetaServiceCode.OK) {
+            throw new RuntimeException("Failed to get storage info: " + 
response.getStatus().getMsg());
+        }
+
+        if (response.getObjInfoList().isEmpty()) {
+            throw new RuntimeException("Only SaaS cloud storage is supported 
currently");
+        }
+
+        return response.getObjInfo(0);
+    }
+
+    /**
+     * Build complete S3 path from objInfo
+     * Package-private for testing
+     */
+    static String buildS3Path(Cloud.ObjectStoreInfoPB objInfo, PluginType 
type, String name) {
+        String bucket = objInfo.getBucket();
+        String prefix = objInfo.hasPrefix() ? objInfo.getPrefix() : "";
+        String relativePath = String.format("plugins/%s/%s", 
type.name().toLowerCase(), name);
+
+        String fullPath;
+        if (Strings.isNullOrEmpty(prefix)) {
+            fullPath = bucket + "/" + relativePath;
+        } else {
+            fullPath = bucket + "/" + prefix + "/" + relativePath;
+        }
+
+        return "s3://" + fullPath;
+    }
+
+    /**
+     * Execute download with S3ObjStorage
+     */
+    private static String doDownload(Cloud.ObjectStoreInfoPB objInfo, String 
remotePath, String localPath)
+            throws Exception {
+        // Create parent directory
+        Path parentDir = Paths.get(localPath).getParent();
+        if (parentDir != null && !Files.exists(parentDir)) {
+            Files.createDirectories(parentDir);
+        }
+
+        // Delete existing file if present
+        File localFile = new File(localPath);
+        if (localFile.exists() && !localFile.delete()) {
+            throw new RuntimeException("Failed to delete existing file: " + 
localPath);
+        }
+
+        // Create S3ObjStorage and download
+        try (S3ObjStorage s3Storage = createS3Storage(objInfo)) {
+            Status status = s3Storage.getObject(remotePath, localFile);
+            if (!status.ok()) {
+                throw new RuntimeException("Download failed: " + 
status.getErrMsg());
+            }
+            return localPath;
+        }
+    }
+
+    /**
+     * Create S3ObjStorage from objInfo
+     */
+    private static S3ObjStorage createS3Storage(Cloud.ObjectStoreInfoPB 
objInfo) {
+        Map<String, String> props = new HashMap<>();
+        props.put("s3.endpoint", objInfo.getEndpoint());
+        props.put("s3.region", objInfo.getRegion());
+        props.put("s3.access_key", objInfo.getAk());
+        props.put("s3.secret_key", objInfo.getSk());
+        props.put("s3.bucket", objInfo.getBucket());
+
+        // Auto-detect storage type (S3, COS, OSS, etc.)
+        StorageProperties storageProps;
+        try {
+            storageProps = StorageProperties.createAll(props).stream()
+                    .findFirst()
+                    .orElseThrow(() -> new RuntimeException("Failed to create 
storage properties"));
+        } catch (UserException e) {
+            throw new RuntimeException(e);
+        }
+
+        return new S3ObjStorage((AbstractS3CompatibleProperties) storageProps);
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/plugin/CloudPluginDownloaderTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/common/plugin/CloudPluginDownloaderTest.java
new file mode 100644
index 00000000000..50b91fc786b
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/common/plugin/CloudPluginDownloaderTest.java
@@ -0,0 +1,173 @@
+// 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.common.plugin;
+
+import org.apache.doris.cloud.proto.Cloud;
+import org.apache.doris.cloud.rpc.MetaServiceProxy;
+import org.apache.doris.common.plugin.CloudPluginDownloader.PluginType;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+
+/**
+ * Unit tests for CloudPluginDownloader using package-private methods for 
direct white-box testing.
+ */
+public class CloudPluginDownloaderTest {
+
+    private Cloud.GetObjStoreInfoResponse mockResponse;
+    private Cloud.ObjectStoreInfoPB mockObjInfo;
+    private MetaServiceProxy mockMetaServiceProxy;
+
+    @BeforeEach
+    void setUp() {
+        mockResponse = Mockito.mock(Cloud.GetObjStoreInfoResponse.class);
+        mockObjInfo = Mockito.mock(Cloud.ObjectStoreInfoPB.class);
+        mockMetaServiceProxy = Mockito.mock(MetaServiceProxy.class);
+    }
+
+    // ============== validateInput Tests ==============
+
+    @Test
+    void testValidateInput() {
+        // Positive cases
+        Assertions.assertDoesNotThrow(() -> {
+            CloudPluginDownloader.validateInput(PluginType.JDBC_DRIVERS, 
"mysql.jar");
+            CloudPluginDownloader.validateInput(PluginType.JAVA_UDF, 
"my_udf.jar");
+        });
+
+        // Empty/null name
+        IllegalArgumentException ex1 = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
CloudPluginDownloader.validateInput(PluginType.JDBC_DRIVERS, ""));
+        Assertions.assertEquals("Plugin name cannot be empty", 
ex1.getMessage());
+
+        IllegalArgumentException ex2 = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
CloudPluginDownloader.validateInput(PluginType.JDBC_DRIVERS, null));
+        Assertions.assertEquals("Plugin name cannot be empty", 
ex2.getMessage());
+
+        // Unsupported types
+        UnsupportedOperationException ex3 = 
Assertions.assertThrows(UnsupportedOperationException.class,
+                () -> 
CloudPluginDownloader.validateInput(PluginType.CONNECTORS, "test.jar"));
+        Assertions.assertTrue(ex3.getMessage().contains("is not supported 
yet"));
+    }
+
+    // ============== getCloudStorageInfo Tests ==============
+
+    @Test
+    void testGetCloudStorageInfo() throws Exception {
+        try (MockedStatic<MetaServiceProxy> mockedStatic = 
Mockito.mockStatic(MetaServiceProxy.class)) {
+            
mockedStatic.when(MetaServiceProxy::getInstance).thenReturn(mockMetaServiceProxy);
+
+            // Success case
+            Cloud.MetaServiceResponseStatus okStatus = 
Cloud.MetaServiceResponseStatus.newBuilder()
+                    .setCode(Cloud.MetaServiceCode.OK).build();
+            Mockito.when(mockResponse.getStatus()).thenReturn(okStatus);
+            
Mockito.when(mockResponse.getObjInfoList()).thenReturn(Collections.singletonList(mockObjInfo));
+            Mockito.when(mockResponse.getObjInfo(0)).thenReturn(mockObjInfo);
+            
Mockito.when(mockMetaServiceProxy.getObjStoreInfo(Mockito.any())).thenReturn(mockResponse);
+
+            Cloud.ObjectStoreInfoPB result = 
CloudPluginDownloader.getCloudStorageInfo();
+            Assertions.assertEquals(mockObjInfo, result);
+
+            // Error response
+            Cloud.MetaServiceResponseStatus failedStatus = 
Cloud.MetaServiceResponseStatus.newBuilder()
+                    
.setCode(Cloud.MetaServiceCode.INVALID_ARGUMENT).setMsg("Test error").build();
+            Mockito.when(mockResponse.getStatus()).thenReturn(failedStatus);
+
+            RuntimeException ex1 = 
Assertions.assertThrows(RuntimeException.class,
+                    CloudPluginDownloader::getCloudStorageInfo);
+            Assertions.assertTrue(ex1.getMessage().contains("Failed to get 
storage info"));
+
+            // Empty storage list
+            Mockito.when(mockResponse.getStatus()).thenReturn(okStatus);
+            
Mockito.when(mockResponse.getObjInfoList()).thenReturn(Collections.emptyList());
+
+            RuntimeException ex2 = 
Assertions.assertThrows(RuntimeException.class,
+                    CloudPluginDownloader::getCloudStorageInfo);
+            Assertions.assertTrue(ex2.getMessage().contains("Only SaaS cloud 
storage is supported"));
+        }
+    }
+
+    // ============== buildS3Path Tests ==============
+
+    @Test
+    void testBuildS3Path() {
+        Mockito.when(mockObjInfo.getBucket()).thenReturn("test-bucket");
+
+        // With prefix
+        Mockito.when(mockObjInfo.hasPrefix()).thenReturn(true);
+        Mockito.when(mockObjInfo.getPrefix()).thenReturn("test-prefix");
+        
Assertions.assertEquals("s3://test-bucket/test-prefix/plugins/jdbc_drivers/mysql.jar",
+                CloudPluginDownloader.buildS3Path(mockObjInfo, 
PluginType.JDBC_DRIVERS, "mysql.jar"));
+
+        // Without prefix
+        Mockito.when(mockObjInfo.hasPrefix()).thenReturn(false);
+        Assertions.assertEquals("s3://test-bucket/plugins/java_udf/my_udf.jar",
+                CloudPluginDownloader.buildS3Path(mockObjInfo, 
PluginType.JAVA_UDF, "my_udf.jar"));
+
+        // All plugin types
+        Assertions.assertEquals("s3://test-bucket/plugins/connectors/test.jar",
+                CloudPluginDownloader.buildS3Path(mockObjInfo, 
PluginType.CONNECTORS, "test.jar"));
+        
Assertions.assertEquals("s3://test-bucket/plugins/hadoop_conf/test.xml",
+                CloudPluginDownloader.buildS3Path(mockObjInfo, 
PluginType.HADOOP_CONF, "test.xml"));
+    }
+
+    // ============== Integration Test ==============
+
+    @Test
+    void testDownloadFromCloudIntegration() {
+        // Basic integration test - should fail early due to validation
+        IllegalArgumentException ex = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
CloudPluginDownloader.downloadFromCloud(PluginType.JDBC_DRIVERS, "", 
"/tmp/test.jar"));
+        Assertions.assertEquals("Plugin name cannot be empty", 
ex.getMessage());
+
+        // Should fail at MetaService level (no real cloud environment)
+        RuntimeException ex2 = Assertions.assertThrows(RuntimeException.class,
+                () -> 
CloudPluginDownloader.downloadFromCloud(PluginType.JDBC_DRIVERS, "mysql.jar", 
"/tmp/test.jar"));
+        Assertions.assertTrue(ex2.getMessage().contains("Failed to download 
plugin"));
+    }
+
+    @Test
+    void testBuildS3PathEdgeCases() {
+        // Test empty bucket (edge case)
+        Mockito.when(mockObjInfo.getBucket()).thenReturn("");
+        Mockito.when(mockObjInfo.hasPrefix()).thenReturn(false);
+        String result = CloudPluginDownloader.buildS3Path(mockObjInfo, 
PluginType.JDBC_DRIVERS, "test.jar");
+        Assertions.assertEquals("s3:///plugins/jdbc_drivers/test.jar", result);
+
+        // Test special characters in name
+        Mockito.when(mockObjInfo.getBucket()).thenReturn("test-bucket");
+        String specialResult = CloudPluginDownloader.buildS3Path(mockObjInfo, 
PluginType.JAVA_UDF, "[email protected]");
+        
Assertions.assertEquals("s3://test-bucket/plugins/java_udf/[email protected]", 
specialResult);
+    }
+
+    // ============== Enum Tests ==============
+
+    @Test
+    void testPluginTypeEnum() {
+        Assertions.assertEquals("JDBC_DRIVERS", 
PluginType.JDBC_DRIVERS.name());
+        Assertions.assertEquals("JAVA_UDF", PluginType.JAVA_UDF.name());
+        Assertions.assertEquals("CONNECTORS", PluginType.CONNECTORS.name());
+        Assertions.assertEquals("HADOOP_CONF", PluginType.HADOOP_CONF.name());
+        Assertions.assertEquals(4, PluginType.values().length);
+    }
+}
diff --git 
a/regression-test/suites/plugin_p1/test_cloud_plugin_auto_download.groovy 
b/regression-test/suites/plugin_p1/test_cloud_plugin_auto_download.groovy
new file mode 100644
index 00000000000..2b697db07ba
--- /dev/null
+++ b/regression-test/suites/plugin_p1/test_cloud_plugin_auto_download.groovy
@@ -0,0 +1,121 @@
+// 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.
+
+suite("test_cloud_plugin_auto_download", "p1,external") {
+
+    //sass cloud-mode only
+    if (!isCloudMode() || enableStoragevault()) {
+        logger.info("Skip test_plugin_auto_download because not in sass cloud 
mode")
+        return
+    }
+
+    String jdbcUrl = context.config.jdbcUrl
+    String jdbcUser = context.config.jdbcUser
+    String jdbcPassword = context.config.jdbcPassword
+
+    sql """drop database if exists internal.test_auto_download_db; """
+    sql """create database if not exists internal.test_auto_download_db;"""
+    sql """create table if not exists internal.test_auto_download_db.test_tbl
+         (id int, name varchar(20))
+         distributed by hash(id) buckets 1
+         properties('replication_num' = '1'); 
+         """
+    sql """insert into internal.test_auto_download_db.test_tbl values(1, 
'auto_download_test')"""
+
+    sql """drop catalog if exists test_auto_download_catalog """
+    sql """ CREATE CATALOG `test_auto_download_catalog` PROPERTIES (
+            "user" = "${jdbcUser}",
+            "type" = "jdbc", 
+            "password" = "${jdbcPassword}",
+            "jdbc_url" = "${jdbcUrl}",
+            "driver_url" = "mysql-connector-j-8.3.0.jar",
+            "driver_class" = "com.mysql.cj.jdbc.Driver"
+        )"""
+    
+    def result = sql """
+        select * from test_auto_download_catalog.test_auto_download_db.test_tbl
+    """
+    logger.info("result: ${result}")
+    assertTrue(result.size() > 0)
+    assertEquals(result[0][0], 1)
+    assertEquals(result[0][1], "auto_download_test")
+    
+    sql """drop catalog if exists test_auto_download_catalog """
+
+    sql """ use internal.test_auto_download_db; """
+
+    sql """DROP FUNCTION IF EXISTS java_udf_add_one(int)"""
+
+    sql """ CREATE FUNCTION java_udf_add_one(int) RETURNS int PROPERTIES (
+        "file"="java-udf-demo-jar-with-dependencies.jar",
+        "symbol"="org.apache.doris.udf.AddOne",
+        "type"="JAVA_UDF"
+    ); """
+
+    def result2 = sql """
+        select java_udf_add_one(100) as result
+    """
+    assertTrue(result2.size() > 0)
+    assertEquals(result2[0][0], 101)
+
+    sql """DROP FUNCTION IF EXISTS java_udf_add_one(int)"""
+
+    // negative test case 1: non-existent JDBC driver jar
+    sql """drop catalog if exists test_non_existent_driver_catalog """
+    try {
+        sql """ CREATE CATALOG `test_non_existent_driver_catalog` PROPERTIES (
+            "user" = "${jdbcUser}",
+            "type" = "jdbc", 
+            "password" = "${jdbcPassword}",
+            "jdbc_url" = "${jdbcUrl}",
+            "driver_url" = "non-existent-mysql-driver.jar",
+            "driver_class" = "com.mysql.cj.jdbc.Driver"
+        )"""
+        
+        sql """
+            select * from 
test_non_existent_driver_catalog.test_auto_download_db.test_tbl
+        """
+        assertTrue(false, "Should have thrown exception for non-existent 
driver jar")
+    } catch (Exception e) {
+        logger.info("Expected exception for non-existent driver jar: " + 
e.getMessage())
+        assertTrue(e.getMessage().contains("has been uploaded to cloud"))
+    } finally {
+        sql """drop catalog if exists test_non_existent_driver_catalog """
+    }
+
+    // negative test case 2: non-existent UDF jar
+    sql """DROP FUNCTION IF EXISTS java_udf_non_existent(int)"""
+    try {
+        sql """ CREATE FUNCTION java_udf_non_existent(int) RETURNS int 
PROPERTIES (
+            "file"="non-existent-udf.jar",
+            "symbol"="org.apache.doris.udf.NonExistent",
+            "type"="JAVA_UDF"
+        ); """
+        
+        sql """
+            select java_udf_non_existent(100) as result
+        """
+        assertTrue(false, "Should have thrown exception for non-existent UDF 
jar")
+    } catch (Exception e) {
+        logger.info("Expected exception for non-existent UDF jar: " + 
e.getMessage())
+        assertTrue(e.getMessage().contains("has been uploaded to cloud"))
+    } finally {
+        sql """DROP FUNCTION IF EXISTS java_udf_non_existent(int)"""
+    }
+
+    sql """ drop database if exists internal.test_auto_download_db; """
+}
\ No newline at end of file


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

Reply via email to