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

zclll pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new e8bc244aa56 [Enhancement](pyudf) add helper command to show more py 
info (#60751)
e8bc244aa56 is described below

commit e8bc244aa5675067e68663c9e47cf17432f8cf32
Author: linrrarity <[email protected]>
AuthorDate: Thu Feb 26 17:11:23 2026 +0800

    [Enhancement](pyudf) add helper command to show more py info (#60751)
    
    doc: https://github.com/apache/doris-website/pull/3402
    
    Add the auxiliary commands `SHOW PYTHON VERSIONS` and `SHOW PYTHON
    PACKAGES IN '<VERSION>'` to display more PythonUDF-related information.
    
    ```sql
    Doris> show python versions;
    
+---------+---------+---------+---------------------------------------------+--------------------------------------------------------+
    | Version | EnvName | EnvType | BasePath                                    
| ExecutablePath                                         |
    
+---------+---------+---------+---------------------------------------------+--------------------------------------------------------+
    | 3.9.18  | py39    | conda   | /mnt/disk7/linzhenqi/miniconda3/envs/py39   
| /mnt/disk7/linzhenqi/miniconda3/envs/py39/bin/python   |
    | 3.8.10  | py3810  | conda   | /mnt/disk7/linzhenqi/miniconda3/envs/py3810 
| /mnt/disk7/linzhenqi/miniconda3/envs/py3810/bin/python |
    | 3.12.11 | py312   | conda   | /mnt/disk7/linzhenqi/miniconda3/envs/py312  
| /mnt/disk7/linzhenqi/miniconda3/envs/py312/bin/python  |
    
+---------+---------+---------+---------------------------------------------+--------------------------------------------------------+
    3 rows in set (0.02 sec)
    
    Doris> show python packages in '3.9.18';
    +-----------------+-------------+
    | Package         | Version     |
    +-----------------+-------------+
    | pyarrow         | 21.0.0      |
    | Bottleneck      | 1.4.2       |
    | jieba           | 0.42.1      |
    | six             | 1.17.0      |
    | wheel           | 0.45.1      |
    | python-dateutil | 2.9.0.post0 |
    | tzdata          | 2025.3      |
    | setuptools      | 80.9.0      |
    | numpy           | 2.0.1       |
    | psutil          | 7.0.0       |
    | pandas          | 2.3.3       |
    | mkl_random      | 1.2.8       |
    | pip             | 25.3        |
    | snownlp         | 0.12.3      |
    | pytz            | 2025.2      |
    | mkl_fft         | 1.3.11      |
    | mkl-service     | 2.4.0       |
    | numexpr         | 2.10.1      |
    +-----------------+-------------+
    ```
---
 be/src/service/backend_service.cpp                 |  16 ++
 be/src/service/backend_service.h                   |   5 +
 be/src/udf/python/python_env.cpp                   |  96 +++++++++++
 be/src/udf/python/python_env.h                     |  18 ++
 be/test/udf/python/python_env_test.cpp             | 102 ++++++++++++
 .../antlr4/org/apache/doris/nereids/DorisLexer.g4  |   3 +
 .../antlr4/org/apache/doris/nereids/DorisParser.g4 |   5 +
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  13 ++
 .../apache/doris/nereids/trees/plans/PlanType.java |   2 +
 .../plans/commands/ShowPythonPackagesCommand.java  | 183 +++++++++++++++++++++
 .../plans/commands/ShowPythonVersionsCommand.java  | 140 ++++++++++++++++
 .../trees/plans/visitor/CommandVisitor.java        |  10 ++
 .../org/apache/doris/common/GenericPoolTest.java   |  12 ++
 .../apache/doris/utframe/MockedBackendFactory.java |  12 ++
 gensrc/thrift/BackendService.thrift                |  19 +++
 .../show/test_show_python_packages_command.groovy  |  55 +++++++
 .../show/test_show_python_versions_command.groovy  |  59 +++++++
 17 files changed, 750 insertions(+)

diff --git a/be/src/service/backend_service.cpp 
b/be/src/service/backend_service.cpp
index dd71e2c92b2..240707aca70 100644
--- a/be/src/service/backend_service.cpp
+++ b/be/src/service/backend_service.cpp
@@ -71,6 +71,7 @@
 #include "runtime/routine_load/routine_load_task_executor.h"
 #include "runtime/stream_load/stream_load_context.h"
 #include "runtime/stream_load/stream_load_recorder.h"
+#include "udf/python/python_env.h"
 #include "util/arrow/row_batch.h"
 #include "util/defer_op.h"
 #include "util/runtime_profile.h"
@@ -1311,5 +1312,20 @@ void 
BaseBackendService::test_storage_connectivity(TTestStorageConnectivityRespo
     response.__set_status(status.to_thrift());
 }
 
+void BaseBackendService::get_python_envs(std::vector<TPythonEnvInfo>& result) {
+    result = PythonVersionManager::instance().env_infos_to_thrift();
+}
+
+void BaseBackendService::get_python_packages(std::vector<TPythonPackageInfo>& 
result,
+                                             const std::string& 
python_version) {
+    PythonVersion version;
+    auto& manager = PythonVersionManager::instance();
+    THROW_IF_ERROR(manager.get_version(python_version, &version));
+
+    std::vector<std::pair<std::string, std::string>> packages;
+    THROW_IF_ERROR(list_installed_packages(version, &packages));
+    result = manager.package_infos_to_thrift(packages);
+}
+
 #include "common/compile_check_end.h"
 } // namespace doris
diff --git a/be/src/service/backend_service.h b/be/src/service/backend_service.h
index 6c43f69170d..4796412ef5f 100644
--- a/be/src/service/backend_service.h
+++ b/be/src/service/backend_service.h
@@ -122,6 +122,11 @@ public:
     void test_storage_connectivity(TTestStorageConnectivityResponse& response,
                                    const TTestStorageConnectivityRequest& 
request) override;
 
+    void get_python_envs(std::vector<TPythonEnvInfo>& result) override;
+
+    void get_python_packages(std::vector<TPythonPackageInfo>& result,
+                             const std::string& python_version) override;
+
     
////////////////////////////////////////////////////////////////////////////
     // begin cloud backend functions
     
////////////////////////////////////////////////////////////////////////////
diff --git a/be/src/udf/python/python_env.cpp b/be/src/udf/python/python_env.cpp
index 2d122a235f1..4b26b29614c 100644
--- a/be/src/udf/python/python_env.cpp
+++ b/be/src/udf/python/python_env.cpp
@@ -18,6 +18,7 @@
 #include "python_env.h"
 
 #include <fmt/core.h>
+#include <rapidjson/document.h>
 
 #include <filesystem>
 #include <memory>
@@ -25,6 +26,7 @@
 #include <vector>
 
 #include "common/status.h"
+#include "gen_cpp/BackendService_types.h"
 #include "udf/python/python_server.h"
 #include "util/string_util.h"
 
@@ -32,6 +34,16 @@ namespace doris {
 
 namespace fs = std::filesystem;
 
+static std::string _python_env_type_to_string(PythonEnvType env_type) {
+    switch (env_type) {
+    case PythonEnvType::CONDA:
+        return "conda";
+    case PythonEnvType::VENV:
+        return "venv";
+    }
+    return "unknown";
+}
+
 // extract python version by executing `python --version` and extract "3.9.16" 
from "Python 3.9.16"
 // @param python_path: path to python executable, e.g. 
"/opt/miniconda3/envs/myenv/bin/python"
 // @param version: extracted python version, e.g. "3.9.16"
@@ -288,4 +300,88 @@ Status PythonVersionManager::init(PythonEnvType env_type, 
const fs::path& python
     return Status::OK();
 }
 
+std::vector<TPythonEnvInfo> PythonVersionManager::env_infos_to_thrift() const {
+    std::vector<TPythonEnvInfo> infos;
+    const auto& envs = _env_scanner->get_envs();
+    infos.reserve(envs.size());
+
+    const auto env_type_str = 
_python_env_type_to_string(_env_scanner->env_type());
+    for (const auto& env : envs) {
+        TPythonEnvInfo info;
+        info.__set_env_name(env.env_name);
+        info.__set_full_version(env.python_version.full_version);
+        info.__set_env_type(env_type_str);
+        info.__set_base_path(env.python_version.base_path);
+        info.__set_executable_path(env.python_version.executable_path);
+        infos.emplace_back(std::move(info));
+    }
+
+    return infos;
+}
+
+std::vector<TPythonPackageInfo> PythonVersionManager::package_infos_to_thrift(
+        const std::vector<std::pair<std::string, std::string>>& packages) 
const {
+    std::vector<TPythonPackageInfo> infos;
+    infos.reserve(packages.size());
+    for (const auto& [name, ver] : packages) {
+        TPythonPackageInfo info;
+        info.__set_package_name(name);
+        info.__set_version(ver);
+        infos.emplace_back(std::move(info));
+    }
+    return infos;
+}
+
+Status list_installed_packages(const PythonVersion& version,
+                               std::vector<std::pair<std::string, 
std::string>>* packages) {
+    DCHECK(packages != nullptr);
+    if (!version.is_valid()) {
+        return Status::InvalidArgument("Invalid python version: {}", 
version.to_string());
+    }
+
+    // Run pip list --format=json to get installed packages
+    std::string cmd =
+            fmt::format("\"{}\" -m pip list --format=json 2>/dev/null", 
version.executable_path);
+    FILE* pipe = popen(cmd.c_str(), "r");
+    if (!pipe) {
+        return Status::InternalError("Failed to run pip list for python 
version: {}",
+                                     version.full_version);
+    }
+
+    std::string result;
+    char buf[4096];
+    while (fgets(buf, sizeof(buf), pipe)) {
+        result += buf;
+    }
+    int ret = pclose(pipe);
+    if (ret != 0) {
+        return Status::InternalError(
+                "pip list failed for python version: {}, exit code: {}, 
output: {}",
+                version.full_version, ret, result);
+    }
+
+    // Parse JSON output: [{"name": "pkg", "version": "1.0"}, ...]
+    // Simple JSON parsing without external library
+    // Each entry looks like: {"name": "package_name", "version": "1.2.3"}
+    rapidjson::Document doc;
+    if (doc.Parse(result.data(), result.size()).HasParseError() || 
!doc.IsArray()) [[unlikely]] {
+        return Status::InternalError("Failed to parse pip list json output for 
python version: {}",
+                                     version.full_version);
+    }
+
+    packages->reserve(packages->size() + doc.Size());
+    for (const auto& item : doc.GetArray()) {
+        auto name_it = item.FindMember("name");
+        auto version_it = item.FindMember("version");
+        if (name_it == item.MemberEnd() || version_it == item.MemberEnd() ||
+            !name_it->value.IsString() || !version_it->value.IsString()) 
[[unlikely]] {
+            return Status::InternalError("Invalid pip list json format for 
python version: {}",
+                                         version.full_version);
+        }
+        packages->emplace_back(name_it->value.GetString(), 
version_it->value.GetString());
+    }
+
+    return Status::OK();
+}
+
 } // namespace doris
diff --git a/be/src/udf/python/python_env.h b/be/src/udf/python/python_env.h
index 4d3a5acca60..6fb148ab234 100644
--- a/be/src/udf/python/python_env.h
+++ b/be/src/udf/python/python_env.h
@@ -18,8 +18,10 @@
 #pragma once
 
 #include <filesystem>
+#include <utility>
 
 #include "common/status.h"
+#include "gen_cpp/BackendService_types.h"
 
 namespace doris {
 
@@ -90,6 +92,8 @@ public:
 
     Status get_version(const std::string& runtime_version, PythonVersion* 
version) const;
 
+    const std::vector<PythonEnvironment>& get_envs() const { return _envs; }
+
     std::string root_path() const { return _env_root_path.string(); }
 
     virtual PythonEnvType env_type() const = 0;
@@ -146,12 +150,26 @@ public:
         return _env_scanner->get_version(runtime_version, version);
     }
 
+    const std::vector<PythonEnvironment>& get_envs() const { return 
_env_scanner->get_envs(); }
+
+    PythonEnvType env_type() const { return _env_scanner->env_type(); }
+
     std::string to_string() const { return _env_scanner->to_string(); }
 
+    std::vector<TPythonEnvInfo> env_infos_to_thrift() const;
+
+    std::vector<TPythonPackageInfo> package_infos_to_thrift(
+            const std::vector<std::pair<std::string, std::string>>& packages) 
const;
+
 private:
     std::unique_ptr<PythonEnvScanner> _env_scanner;
 };
 
+// List installed pip packages for a given Python version.
+// Returns pairs of (package_name, version).
+Status list_installed_packages(const PythonVersion& version,
+                               std::vector<std::pair<std::string, 
std::string>>* packages);
+
 } // namespace doris
 
 namespace std {
diff --git a/be/test/udf/python/python_env_test.cpp 
b/be/test/udf/python/python_env_test.cpp
index ab1494867cd..0ccbb63cf1d 100644
--- a/be/test/udf/python/python_env_test.cpp
+++ b/be/test/udf/python/python_env_test.cpp
@@ -19,6 +19,7 @@
 
 #include <gtest/gtest.h>
 
+#include <csignal>
 #include <filesystem>
 #include <fstream>
 #include <string>
@@ -28,17 +29,41 @@ namespace doris {
 
 namespace fs = std::filesystem;
 
+static PythonVersion create_fake_python_version_for_pip_list(const 
std::string& base_path,
+                                                             const 
std::string& script_body,
+                                                             const 
std::string& full_version) {
+    const std::string bin_path = base_path + "/bin";
+    const std::string exec_path = bin_path + "/python3";
+    fs::create_directories(bin_path);
+
+    {
+        std::ofstream ofs(exec_path);
+        ofs << "#!/bin/bash\n";
+        ofs << script_body;
+    }
+    fs::permissions(exec_path, fs::perms::owner_all);
+
+    return PythonVersion(full_version, base_path, exec_path);
+}
+
 class PythonEnvTest : public ::testing::Test {
 protected:
     std::string test_dir_;
+    // Some test frameworks set SIGCHLD to SIG_IGN,
+    // which causes pclose() to get ECHILD because the kernel auto-reaps 
children.
+    // We reset SIGCHLD to SIG_DFL for the duration of each test to mimic 
production
+    // behaviour, and restore the original handler afterwards.
+    sighandler_t old_sigchld_ = SIG_DFL;
 
     void SetUp() override {
         test_dir_ = fs::temp_directory_path().string() + "/python_env_test_" +
                     std::to_string(getpid()) + "_" + std::to_string(rand());
         fs::create_directories(test_dir_);
+        old_sigchld_ = signal(SIGCHLD, SIG_DFL);
     }
 
     void TearDown() override {
+        signal(SIGCHLD, old_sigchld_);
         if (!test_dir_.empty() && fs::exists(test_dir_)) {
             fs::remove_all(test_dir_);
         }
@@ -601,4 +626,81 @@ TEST_F(PythonEnvTest, 
PythonVersionManagerInitCondaSuccess) {
     EXPECT_TRUE(status.ok()) << status.to_string();
 }
 
+// ============================================================================
+// list_installed_packages tests
+// ============================================================================
+
+TEST_F(PythonEnvTest, ListInstalledPackagesInvalidPythonVersion) {
+    PythonVersion invalid_version;
+    std::vector<std::pair<std::string, std::string>> packages;
+
+    Status status = list_installed_packages(invalid_version, &packages);
+    EXPECT_FALSE(status.ok());
+    EXPECT_TRUE(status.to_string().find("Invalid python version") != 
std::string::npos);
+}
+
+TEST_F(PythonEnvTest, ListInstalledPackagesPipListExitNonZero) {
+    PythonVersion version = create_fake_python_version_for_pip_list(
+            test_dir_ + "/pip_nonzero",
+            "echo '[{\"name\":\"numpy\",\"version\":\"1.26.0\"}]'\n"
+            "exit 1\n",
+            "3.9.16");
+    std::vector<std::pair<std::string, std::string>> packages;
+
+    Status status = list_installed_packages(version, &packages);
+    EXPECT_FALSE(status.ok());
+    EXPECT_TRUE(status.to_string().find("pip list failed") != 
std::string::npos);
+}
+
+TEST_F(PythonEnvTest, ListInstalledPackagesParseError) {
+    PythonVersion version = create_fake_python_version_for_pip_list(
+            test_dir_ + "/pip_parse_error", "echo 'not-json'\nexit 0\n", 
"3.9.16");
+    std::vector<std::pair<std::string, std::string>> packages;
+
+    Status status = list_installed_packages(version, &packages);
+    EXPECT_FALSE(status.ok());
+    EXPECT_TRUE(status.to_string().find("Failed to parse pip list json 
output") !=
+                std::string::npos);
+}
+
+TEST_F(PythonEnvTest, ListInstalledPackagesJsonIsNotArray) {
+    PythonVersion version = create_fake_python_version_for_pip_list(
+            test_dir_ + "/pip_not_array", "echo '{\"name\":\"numpy\"}'\nexit 
0\n", "3.9.16");
+    std::vector<std::pair<std::string, std::string>> packages;
+
+    Status status = list_installed_packages(version, &packages);
+    EXPECT_FALSE(status.ok());
+    EXPECT_TRUE(status.to_string().find("Failed to parse pip list json 
output") !=
+                std::string::npos);
+}
+
+TEST_F(PythonEnvTest, ListInstalledPackagesInvalidJsonItemFormat) {
+    PythonVersion version = create_fake_python_version_for_pip_list(
+            test_dir_ + "/pip_invalid_item", "echo 
'[{\"name\":\"numpy\"}]'\nexit 0\n", "3.9.16");
+    std::vector<std::pair<std::string, std::string>> packages;
+
+    Status status = list_installed_packages(version, &packages);
+    EXPECT_FALSE(status.ok());
+    EXPECT_TRUE(status.to_string().find("Invalid pip list json format") != 
std::string::npos);
+}
+
+TEST_F(PythonEnvTest, ListInstalledPackagesSuccess) {
+    PythonVersion version = create_fake_python_version_for_pip_list(
+            test_dir_ + "/pip_success",
+            "echo "
+            
"'[{\"name\":\"numpy\",\"version\":\"1.26.0\"},{\"name\":\"pandas\",\"version\":\"2.2."
+            "0\"}]'\n"
+            "exit 0\n",
+            "3.9.16");
+    std::vector<std::pair<std::string, std::string>> packages;
+
+    Status status = list_installed_packages(version, &packages);
+    EXPECT_TRUE(status.ok()) << status.to_string();
+    ASSERT_EQ(packages.size(), 2);
+    EXPECT_EQ(packages[0].first, "numpy");
+    EXPECT_EQ(packages[0].second, "1.26.0");
+    EXPECT_EQ(packages[1].first, "pandas");
+    EXPECT_EQ(packages[1].second, "2.2.0");
+}
+
 } // namespace doris
diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 
b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
index 12a73beb2c5..0b2b5bf1ef7 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
@@ -414,6 +414,7 @@ PARAMETER: 'PARAMETER';
 PARSED: 'PARSED';
 PARTITION: 'PARTITION';
 PARTITIONS: 'PARTITIONS';
+PACKAGES: 'PACKAGES';
 PASSWORD: 'PASSWORD';
 PASSWORD_EXPIRE: 'PASSWORD_EXPIRE';
 PASSWORD_HISTORY: 'PASSWORD_HISTORY';
@@ -431,6 +432,7 @@ PLAN: 'PLAN';
 PLAY: 'PLAY';
 PRIVILEGES: 'PRIVILEGES';
 PROCESS: 'PROCESS';
+PYTHON: 'PYTHON';
 PLUGIN: 'PLUGIN';
 PLUGINS: 'PLUGINS';
 POLICY: 'POLICY';
@@ -603,6 +605,7 @@ VAULT: 'VAULT';
 VAULTS: 'VAULTS';
 VERBOSE: 'VERBOSE';
 VERSION: 'VERSION';
+VERSIONS: 'VERSIONS';
 VIEW: 'VIEW';
 VIEWS: 'VIEWS';
 WARM: 'WARM';
diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 
b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
index 4276eaf55b4..535fc479716 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
@@ -475,6 +475,8 @@ supportedShowStatement
         (FROM |IN) tableName=multipartIdentifier
         ((FROM | IN) database=multipartIdentifier)?                            
     #showIndex
     | SHOW WARM UP JOB wildWhere?                                              
     #showWarmUpJob
+    | SHOW PYTHON VERSIONS                                                     
      #showPythonVersions
+    | SHOW PYTHON PACKAGES IN STRING_LITERAL                                   
      #showPythonPackages
     ;
 
 supportedLoadStatement
@@ -2190,6 +2192,7 @@ nonReserved
     | PASSWORD_LOCK_TIME
     | PASSWORD_REUSE
     | PARTITIONS
+    | PACKAGES
     | PATH
     | PAUSE
     | PERCENT
@@ -2209,6 +2212,7 @@ nonReserved
     | PROFILE
     | PROPERTIES
     | PROPERTY
+    | PYTHON
     | QUANTILE_STATE
        | QUANTILE_UNION
        | QUARTER
@@ -2315,6 +2319,7 @@ nonReserved
     | VAULTS
     | VERBOSE
     | VERSION
+    | VERSIONS
     | VIEW
     | VIEWS
     | WARM
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 005bac3c01b..afc3131b109 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -836,6 +836,8 @@ import 
org.apache.doris.nereids.trees.plans.commands.ShowPluginsCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowPrivilegesCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowProcCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowProcessListCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPythonPackagesCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPythonVersionsCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowQueryProfileCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowQueryStatsCommand;
 import 
org.apache.doris.nereids.trees.plans.commands.ShowQueuedAnalyzeJobsCommand;
@@ -6513,6 +6515,17 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
         return new ShowTrashCommand();
     }
 
+    @Override
+    public LogicalPlan 
visitShowPythonVersions(DorisParser.ShowPythonVersionsContext ctx) {
+        return new ShowPythonVersionsCommand();
+    }
+
+    @Override
+    public LogicalPlan 
visitShowPythonPackages(DorisParser.ShowPythonPackagesContext ctx) {
+        String version = stripQuotes(ctx.STRING_LITERAL().getText());
+        return new ShowPythonPackagesCommand(version);
+    }
+
     @Override
     public LogicalPlan visitAdminCleanTrash(DorisParser.AdminCleanTrashContext 
ctx) {
         if (ctx.ON() != null) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
index 4d071c838dc..dcff41a3175 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
@@ -314,6 +314,8 @@ public enum PlanType {
     SHOW_PROC_COMMAND,
     SHOW_PLUGINS_COMMAND,
     SHOW_PRIVILEGES_COMMAND,
+    SHOW_PYTHON_PACKAGES_COMMAND,
+    SHOW_PYTHON_VERSIONS_COMMAND,
     SHOW_REPLICA_DISTRIBUTION_COMMAND,
     SHOW_REPLICA_STATUS_COMMAND,
     SHOW_REPOSITORIES_COMMAND,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowPythonPackagesCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowPythonPackagesCommand.java
new file mode 100644
index 00000000000..f4b5ad8f99a
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowPythonPackagesCommand.java
@@ -0,0 +1,183 @@
+// 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.nereids.trees.plans.commands;
+
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.common.ClientPool;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.BackendService;
+import org.apache.doris.thrift.TNetworkAddress;
+import org.apache.doris.thrift.TPythonPackageInfo;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SHOW PYTHON PACKAGES IN '&lt;version&gt;' command.
+ * Shows pip packages installed for the given Python version, collected from 
all alive BEs.
+ */
+public class ShowPythonPackagesCommand extends ShowCommand {
+    private static final Logger LOG = 
LogManager.getLogger(ShowPythonPackagesCommand.class);
+
+    private static final String[] TITLE_NAMES = {"Package", "Version"};
+    private static final String[] TITLE_NAMES_INCONSISTENT = {"Package", 
"Version", "Consistent", "Backends"};
+
+    private final String pythonVersion;
+
+    public ShowPythonPackagesCommand(String pythonVersion) {
+        super(PlanType.SHOW_PYTHON_PACKAGES_COMMAND);
+        this.pythonVersion = pythonVersion;
+    }
+
+    public String getPythonVersion() {
+        return pythonVersion;
+    }
+
+    @Override
+    public ShowResultSetMetaData getMetaData() {
+        return getMetaData(true);
+    }
+
+    private ShowResultSetMetaData getMetaData(boolean consistent) {
+        ShowResultSetMetaData.Builder builder = 
ShowResultSetMetaData.builder();
+        String[] titles = consistent ? TITLE_NAMES : TITLE_NAMES_INCONSISTENT;
+        for (String title : titles) {
+            builder.addColumn(new Column(title, 
ScalarType.createVarchar(256)));
+        }
+        return builder.build();
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitShowPythonPackagesCommand(this, context);
+    }
+
+    @Override
+    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
+        if 
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
+                PrivPredicate.ADMIN)) {
+            
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, 
"ADMIN");
+        }
+
+        ImmutableMap<Long, Backend> backendsInfo = 
Env.getCurrentSystemInfo().getAllBackendsByAllCluster();
+
+        // Collect packages from each alive BE, tracking which BE each result 
came from
+        List<Map<String, String>> allBePackages = new ArrayList<>();
+        List<String> beIdentifiers = new ArrayList<>();
+        for (Backend backend : backendsInfo.values()) {
+            if (!backend.isAlive()) {
+                continue;
+            }
+            BackendService.Client client = null;
+            TNetworkAddress address = null;
+            boolean ok = false;
+            try {
+                address = new TNetworkAddress(backend.getHost(), 
backend.getBePort());
+                client = ClientPool.backendPool.borrowObject(address);
+                List<TPythonPackageInfo> packages = 
client.getPythonPackages(pythonVersion);
+                ok = true;
+
+                Map<String, String> pkgMap = new HashMap<>();
+                for (TPythonPackageInfo pkg : packages) {
+                    pkgMap.put(pkg.getPackageName(), pkg.getVersion());
+                }
+                allBePackages.add(pkgMap);
+                beIdentifiers.add(backend.getHost() + ":" + 
backend.getBePort());
+            } catch (Exception e) {
+                LOG.warn("Failed to get python packages from backend[{}]", 
backend.getId(), e);
+            } finally {
+                if (ok) {
+                    ClientPool.backendPool.returnObject(address, client);
+                } else {
+                    ClientPool.backendPool.invalidateObject(address, client);
+                }
+            }
+        }
+
+        if (allBePackages.isEmpty()) {
+            return new ShowResultSet(getMetaData(), Lists.newArrayList());
+        }
+
+        // Check consistency across BEs
+        boolean consistent = true;
+        Map<String, String> referencePackages = allBePackages.get(0);
+        for (int i = 1; i < allBePackages.size(); i++) {
+            if (!referencePackages.equals(allBePackages.get(i))) {
+                consistent = false;
+                break;
+            }
+        }
+
+        List<List<String>> rows = Lists.newArrayList();
+        if (consistent) {
+            for (Map.Entry<String, String> entry : 
referencePackages.entrySet()) {
+                List<String> row = new ArrayList<>();
+                row.add(entry.getKey());
+                row.add(entry.getValue());
+                rows.add(row);
+            }
+        } else {
+            // For each package+version, collect which BEs have it
+            // key: pkgName -> (version -> list of BE identifiers)
+            Map<String, Map<String, List<String>>> pkgVersionBes = new 
HashMap<>();
+            for (int i = 0; i < allBePackages.size(); i++) {
+                String beId = beIdentifiers.get(i);
+                for (Map.Entry<String, String> entry : 
allBePackages.get(i).entrySet()) {
+                    pkgVersionBes.computeIfAbsent(entry.getKey(), k -> new 
HashMap<>())
+                            .computeIfAbsent(entry.getValue(), k -> new 
ArrayList<>())
+                            .add(beId);
+                }
+            }
+            int totalBes = allBePackages.size();
+            for (Map.Entry<String, Map<String, List<String>>> entry : 
pkgVersionBes.entrySet()) {
+                String pkgName = entry.getKey();
+                Map<String, List<String>> versionBes = entry.getValue();
+                boolean pkgConsistent = versionBes.size() == 1
+                        && versionBes.values().iterator().next().size() == 
totalBes;
+                for (Map.Entry<String, List<String>> vb : 
versionBes.entrySet()) {
+                    List<String> row = new ArrayList<>();
+                    row.add(pkgName);
+                    row.add(vb.getKey());
+                    row.add(pkgConsistent ? "Yes" : "No");
+                    row.add(pkgConsistent ? "" : String.join(", ", 
vb.getValue()));
+                    rows.add(row);
+                }
+            }
+        }
+
+        return new ShowResultSet(getMetaData(consistent), rows);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowPythonVersionsCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowPythonVersionsCommand.java
new file mode 100644
index 00000000000..16fab957bce
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowPythonVersionsCommand.java
@@ -0,0 +1,140 @@
+// 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.nereids.trees.plans.commands;
+
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.common.ClientPool;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.BackendService;
+import org.apache.doris.thrift.TNetworkAddress;
+import org.apache.doris.thrift.TPythonEnvInfo;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * SHOW PYTHON VERSIONS command.
+ * Shows Python versions available on all alive backends (intersection).
+ */
+public class ShowPythonVersionsCommand extends ShowCommand {
+    private static final Logger LOG = 
LogManager.getLogger(ShowPythonVersionsCommand.class);
+
+    private static final String[] TITLE_NAMES = {
+        "Version", "EnvName", "EnvType", "BasePath", "ExecutablePath"
+    };
+
+    public ShowPythonVersionsCommand() {
+        super(PlanType.SHOW_PYTHON_VERSIONS_COMMAND);
+    }
+
+    @Override
+    public ShowResultSetMetaData getMetaData() {
+        ShowResultSetMetaData.Builder builder = 
ShowResultSetMetaData.builder();
+        for (String title : TITLE_NAMES) {
+            builder.addColumn(new Column(title, 
ScalarType.createVarchar(256)));
+        }
+        return builder.build();
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitShowPythonVersionsCommand(this, context);
+    }
+
+    @Override
+    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
+        if 
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
+                PrivPredicate.ADMIN)) {
+            
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, 
"ADMIN");
+        }
+
+        ImmutableMap<Long, Backend> backendsInfo = 
Env.getCurrentSystemInfo().getAllBackendsByAllCluster();
+        List<List<TPythonEnvInfo>> allBeEnvs = new ArrayList<>();
+        Set<String> commonVersions = null;
+
+        for (Backend backend : backendsInfo.values()) {
+            if (!backend.isAlive()) {
+                continue;
+            }
+            BackendService.Client client = null;
+            TNetworkAddress address = null;
+            boolean ok = false;
+            try {
+                address = new TNetworkAddress(backend.getHost(), 
backend.getBePort());
+                client = ClientPool.backendPool.borrowObject(address);
+                List<TPythonEnvInfo> envs = client.getPythonEnvs();
+                ok = true;
+
+                allBeEnvs.add(envs);
+                Set<String> versions = new HashSet<>();
+                for (TPythonEnvInfo env : envs) {
+                    versions.add(env.getFullVersion());
+                }
+                if (commonVersions == null) {
+                    commonVersions = versions;
+                } else {
+                    commonVersions.retainAll(versions);
+                }
+            } catch (Exception e) {
+                LOG.warn("Failed to get python envs from backend[{}]", 
backend.getId(), e);
+            } finally {
+                if (ok) {
+                    ClientPool.backendPool.returnObject(address, client);
+                } else {
+                    ClientPool.backendPool.invalidateObject(address, client);
+                }
+            }
+        }
+
+        List<List<String>> rows = Lists.newArrayList();
+        if (commonVersions != null && !allBeEnvs.isEmpty()) {
+            // Use envs from the first BE as reference, filtered to common 
versions
+            for (TPythonEnvInfo env : allBeEnvs.get(0)) {
+                if (commonVersions.contains(env.getFullVersion())) {
+                    List<String> row = new ArrayList<>();
+                    row.add(env.getFullVersion());
+                    row.add(env.getEnvName());
+                    row.add(env.getEnvType());
+                    row.add(env.getBasePath());
+                    row.add(env.getExecutablePath());
+                    rows.add(row);
+                }
+            }
+        }
+
+        return new ShowResultSet(getMetaData(), rows);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
index 743dd9dbbb5..0f1625c629b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
@@ -231,6 +231,8 @@ import 
org.apache.doris.nereids.trees.plans.commands.ShowPluginsCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowPrivilegesCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowProcCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowProcessListCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPythonPackagesCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowPythonVersionsCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowQueryProfileCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowQueryStatsCommand;
 import 
org.apache.doris.nereids.trees.plans.commands.ShowQueuedAnalyzeJobsCommand;
@@ -717,6 +719,14 @@ public interface CommandVisitor<R, C> {
         return visitCommand(showPluginsCommand, context);
     }
 
+    default R visitShowPythonVersionsCommand(ShowPythonVersionsCommand 
showPythonVersionsCommand, C context) {
+        return visitCommand(showPythonVersionsCommand, context);
+    }
+
+    default R visitShowPythonPackagesCommand(ShowPythonPackagesCommand 
showPythonPackagesCommand, C context) {
+        return visitCommand(showPythonPackagesCommand, context);
+    }
+
     default R visitShowTrashCommand(ShowTrashCommand showTrashCommand, C 
context) {
         return visitCommand(showTrashCommand, context);
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/GenericPoolTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/common/GenericPoolTest.java
index e761f07a90e..dd5e4984e71 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/common/GenericPoolTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/common/GenericPoolTest.java
@@ -35,6 +35,8 @@ import org.apache.doris.thrift.TIngestBinlogResult;
 import org.apache.doris.thrift.TNetworkAddress;
 import org.apache.doris.thrift.TPublishTopicRequest;
 import org.apache.doris.thrift.TPublishTopicResult;
+import org.apache.doris.thrift.TPythonEnvInfo;
+import org.apache.doris.thrift.TPythonPackageInfo;
 import org.apache.doris.thrift.TQueryIngestBinlogRequest;
 import org.apache.doris.thrift.TQueryIngestBinlogResult;
 import org.apache.doris.thrift.TRoutineLoadTask;
@@ -246,6 +248,16 @@ public class GenericPoolTest {
                 org.apache.doris.thrift.TTestStorageConnectivityRequest 
request) throws TException {
             return null;
         }
+
+        @Override
+        public List<TPythonEnvInfo> getPythonEnvs() throws TException {
+            return null;
+        }
+
+        @Override
+        public List<TPythonPackageInfo> getPythonPackages(String 
pythonVersion) throws TException {
+            return null;
+        }
     }
 
     @Test
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/utframe/MockedBackendFactory.java 
b/fe/fe-core/src/test/java/org/apache/doris/utframe/MockedBackendFactory.java
index 0d4a109921a..5c20a2b64f6 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/utframe/MockedBackendFactory.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/utframe/MockedBackendFactory.java
@@ -55,6 +55,8 @@ import org.apache.doris.thrift.TMasterInfo;
 import org.apache.doris.thrift.TNetworkAddress;
 import org.apache.doris.thrift.TPublishTopicRequest;
 import org.apache.doris.thrift.TPublishTopicResult;
+import org.apache.doris.thrift.TPythonEnvInfo;
+import org.apache.doris.thrift.TPythonPackageInfo;
 import org.apache.doris.thrift.TQueryIngestBinlogRequest;
 import org.apache.doris.thrift.TQueryIngestBinlogResult;
 import org.apache.doris.thrift.TRoutineLoadTask;
@@ -478,6 +480,16 @@ public class MockedBackendFactory {
             response.setStatus(new TStatus(TStatusCode.OK));
             return response;
         }
+
+        @Override
+        public java.util.List<TPythonEnvInfo> getPythonEnvs() throws 
TException {
+            return java.util.Collections.emptyList();
+        }
+
+        @Override
+        public java.util.List<TPythonPackageInfo> getPythonPackages(String 
pythonVersion) throws TException {
+            return java.util.Collections.emptyList();
+        }
     }
 
     // The default Brpc service.
diff --git a/gensrc/thrift/BackendService.thrift 
b/gensrc/thrift/BackendService.thrift
index 684f2404fcb..44c1e7cd94c 100644
--- a/gensrc/thrift/BackendService.thrift
+++ b/gensrc/thrift/BackendService.thrift
@@ -378,6 +378,19 @@ struct TTestStorageConnectivityResponse {
     1: optional Status.TStatus status;
 }
 
+struct TPythonEnvInfo {
+    1: optional string env_name       // e.g. "myenv"
+    2: optional string full_version   // e.g. "3.9.16"
+    3: optional string env_type       // "conda" or "venv"
+    4: optional string base_path      // e.g. "/opt/miniconda3/envs/myenv"
+    5: optional string executable_path
+}
+
+struct TPythonPackageInfo {
+    1: optional string package_name
+    2: optional string version
+}
+
 service BackendService {
     AgentService.TAgentResult 
submit_tasks(1:list<AgentService.TAgentTaskRequest> tasks);
 
@@ -431,4 +444,10 @@ service BackendService {
 
     // Test storage connectivity (S3, HDFS, etc.)
     TTestStorageConnectivityResponse 
test_storage_connectivity(1:TTestStorageConnectivityRequest request);
+
+    // Get Python environments available on this BE
+    list<TPythonEnvInfo> get_python_envs();
+
+    // Get installed pip packages for a specific Python version
+    list<TPythonPackageInfo> get_python_packages(1:string python_version);
 }
diff --git 
a/regression-test/suites/nereids_p0/show/test_show_python_packages_command.groovy
 
b/regression-test/suites/nereids_p0/show/test_show_python_packages_command.groovy
new file mode 100644
index 00000000000..0dbbc2ca958
--- /dev/null
+++ 
b/regression-test/suites/nereids_p0/show/test_show_python_packages_command.groovy
@@ -0,0 +1,55 @@
+// 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_show_python_packages_command') {
+    // get available Python versions so we can pick one for the packages test
+    def versions = sql 'SHOW PYTHON VERSIONS'
+    if (versions.size() == 0) {
+        return
+    }
+    def testVersion = versions[0][0]
+
+    // Execute SHOW PYTHON PACKAGES IN '<version>'
+    def result = sql "SHOW PYTHON PACKAGES IN '${testVersion}'"
+
+    // There should be at least some packages installed
+    assertTrue(result.size() > 0,
+            "Expected at least some packages for Python ${testVersion}")
+
+    // Collect all package names for later assertions
+    def packageNames = [] as Set
+    for (row in result) {
+        // Package name (column 0) should be non-empty
+        def pkgName = row[0]
+        assertNotNull(pkgName)
+        assertTrue(pkgName.length() > 0, 'Package name should not be empty')
+
+        // Package version (column 1) should be non-empty
+        def pkgVersion = row[1]
+        assertNotNull(pkgVersion)
+        assertTrue(pkgVersion.length() > 0,
+                "Package version for '${pkgName}' should not be empty")
+
+        packageNames.add(pkgName.toLowerCase())
+    }
+
+    // Verify that essential packages (pyarrow and pandas) are present
+    assertTrue(packageNames.contains('pyarrow'),
+            "pyarrow should be installed, found packages: ${packageNames}")
+    assertTrue(packageNames.contains('pandas'),
+            "pandas should be installed, found packages: ${packageNames}")
+}
diff --git 
a/regression-test/suites/nereids_p0/show/test_show_python_versions_command.groovy
 
b/regression-test/suites/nereids_p0/show/test_show_python_versions_command.groovy
new file mode 100644
index 00000000000..dc322ea6b6b
--- /dev/null
+++ 
b/regression-test/suites/nereids_p0/show/test_show_python_versions_command.groovy
@@ -0,0 +1,59 @@
+// 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_show_python_versions_command') {
+    def result = sql 'SHOW PYTHON VERSIONS'
+
+    if (result.size() > 0) {
+        // Verify column structure: [Version, EnvName, EnvType, BasePath, 
ExecutablePath]
+        for (row in result) {
+            // Version (column 0) should be a non-empty version string like 
"3.9.16"
+            def version = row[0]
+            assertNotNull(version)
+            assertTrue(version.length() > 0, 'Version should not be empty')
+            assertTrue(version ==~ /\d+\.\d+(\.\d+)?/,
+                    "Version '${version}' should match pattern x.y or x.y.z")
+
+            // EnvName (column 1) should be non-empty
+            def envName = row[1]
+            assertNotNull(envName)
+            assertTrue(envName.length() > 0, 'EnvName should not be empty')
+
+            // EnvType (column 2) must be either "conda" or "venv"
+            def envType = row[2]
+            assertTrue(envType == 'conda' || envType == 'venv',
+                    "EnvType '${envType}' should be 'conda' or 'venv'")
+
+            // BasePath (column 3) should be a non-empty path
+            def basePath = row[3]
+            assertNotNull(basePath)
+            assertTrue(basePath.length() > 0, 'BasePath should not be empty')
+            assertTrue(basePath.startsWith('/'),
+                    "BasePath '${basePath}' should be an absolute path")
+
+            // ExecutablePath (column 4) should be a non-empty path
+            def execPath = row[4]
+            assertNotNull(execPath)
+            assertTrue(execPath.length() > 0, 'ExecutablePath should not be 
empty')
+            assertTrue(execPath.startsWith('/'),
+                    "ExecutablePath '${execPath}' should be an absolute path")
+        }
+
+        // Verify uniqueness of versions in the result
+        result.collect { it[0] } as Set
+    }
+}


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


Reply via email to