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

HappenLee pushed a commit to branch paimon-blob-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git

commit e49d3a547e5c075a384a3f9e343bdb176b19e7da
Author: linzhenqi <[email protected]>
AuthorDate: Thu Oct 22 02:30:27 2026 +0800

    [improvement](function) Rename LIST_FILE to LIST_FILES and return 
directories
    
    LIST_FILES returns path, size and nullable FILE metadata. Non-recursive
    listing emits immediate directories with zero size and NULL FILE values.
    Recursive listing, now the default, emits files and skips directory markers.
    Carry native S3 and Azure hierarchy prefixes through the paginated listing.
    
    Use LIST_FILES in SQL. The result includes a BIGINT size column and nullable
    file column. The recursive property defaults to true; false includes direct
    subdirectories. Ordinary zero-byte files are returned in both modes.
    
    - Test: Full BE/FE build; ListFilesRowsTest with coverage; real MinIO P0
      regression including 1005-entry directory and file listings; three docs
      SQL examples; clang-format 16 and build hygiene checks.
    - Behavior changed: Yes, function name, result schema and directory listing.
    - Does this need documentation: Yes, current and 4.x English/Chinese pages
      updated in the local doris-website repository.
    
    (cherry picked from commit e2196f37952c8dd168d5b728b2aaddb31000199a)
---
 be/src/core/value/file_value.cpp                   |   2 +-
 .../common/data_gen_functions/vlist_file_tvf.cpp   |  32 +-
 be/src/io/fs/azure_obj_storage_client.cpp          |   4 +-
 be/src/io/fs/obj_storage_client.h                  |   1 +
 be/src/io/fs/s3_obj_storage_client.cpp             |   4 +
 .../data_gen_functions/vlist_file_tvf_test.cpp     |   7 +-
 .../data_gen_functions/vlist_files_rows_test.cpp   | 102 +++++
 be/test/io/fs/azure_obj_storage_client_test.cpp    |  17 +-
 .../doris/catalog/BuiltinTableValuedFunctions.java |   4 +-
 .../table/{ListFile.java => ListFiles.java}        |  20 +-
 .../visitor/TableValuedFunctionVisitor.java        |   6 +-
 ...tion.java => ListFilesTableValuedFunction.java} |  19 +-
 .../doris/tablefunction/TableValuedFunctionIf.java |   4 +-
 ....java => ListFilesTableValuedFunctionTest.java} |  35 +-
 .../test_list_file_resource_local.out              | 165 --------
 .../datatype_p0/complex_types/test_list_files.out  |  88 ++++
 .../test_list_file_resource_local.groovy           | 447 ---------------------
 .../complex_types/test_list_files.groovy           | 101 +++++
 18 files changed, 384 insertions(+), 674 deletions(-)

diff --git a/be/src/core/value/file_value.cpp b/be/src/core/value/file_value.cpp
index 91951205691..e4b59824308 100644
--- a/be/src/core/value/file_value.cpp
+++ b/be/src/core/value/file_value.cpp
@@ -208,7 +208,7 @@ bool is_valid_file_content_type(std::string_view 
content_type) {
 }
 
 std::string_view infer_file_content_type_from_name(std::string_view name) {
-    // Shared by TO_FILE and list_file. Keep inference independent of URI 
rewriting.
+    // Shared by TO_FILE and list_files. Keep inference independent of URI 
rewriting.
     static constexpr std::pair<std::string_view, std::string_view> TYPES[] = {
             {".csv", "text/csv"},
             {".tsv", "text/tab-separated-values"},
diff --git a/be/src/exec/common/data_gen_functions/vlist_file_tvf.cpp 
b/be/src/exec/common/data_gen_functions/vlist_file_tvf.cpp
index ebb2e9d7b9e..21a0d992344 100644
--- a/be/src/exec/common/data_gen_functions/vlist_file_tvf.cpp
+++ b/be/src/exec/common/data_gen_functions/vlist_file_tvf.cpp
@@ -49,7 +49,7 @@ Status VListFileTVF::set_scan_ranges(const 
std::vector<TScanRangeParams>& scan_r
         _path.prefix += '/';
     }
     _path.delimiter = params.recursive ? "" : "/";
-    static constexpr std::array<std::string_view, 2> NAMES {"path", "file"};
+    static constexpr std::array<std::string_view, 3> NAMES {"path", "size", 
"file"};
     _column_indices.clear();
     for (const auto* slot : _tuple_desc->slots()) {
         const auto* const field = std::ranges::find(NAMES, slot->col_name());
@@ -81,6 +81,9 @@ Status VListFileTVF::_fetch_next_page(RuntimeState* state) {
         return {page.resp.status.code, std::move(page.resp.status.msg)};
     }
     _objects = std::move(page.objects);
+    for (auto& prefix : page.common_prefixes) {
+        _objects.emplace_back(ObjectMeta {.key = std::move(prefix)});
+    }
     _next_index = 0;
     _continuation_token = std::move(page.continuation_token);
     _has_more = page.has_more;
@@ -117,19 +120,26 @@ Status VListFileTVF::get_next(RuntimeState* state, Block* 
block, bool* eos) {
         }
         RETURN_IF_CANCELLED(state);
         const auto& object = _objects[_next_index++];
-        if (object.key.ends_with('/')) {
+        const bool is_directory = object.key.ends_with('/');
+        // The prefix marker names the listed directory itself; recursive 
scans omit all markers.
+        if (is_directory && (_path.delimiter.empty() || object.key == 
_path.prefix)) {
             continue;
         }
-        File value(DataTypeFile::FIELD_COUNT);
-        value[0] = Field::create_field<TYPE_STRING>("s3://" + _path.bucket + 
"/" + object.key);
-        value[2] = Field::create_field<TYPE_BIGINT>(object.size);
-        value[3] = Field::create_field<TYPE_STRING>(
-                std::string(infer_file_content_type_from_name(object.key)));
-        if (auto checksum = file_checksum_from_etag(object.etag)) {
-            value[4] = Field::create_field<TYPE_STRING>(std::move(*checksum));
+        std::array<Field, 3> fields {
+                Field::create_field<TYPE_STRING>("s3://" + _path.bucket + "/" 
+ object.key),
+                Field::create_field<TYPE_BIGINT>(is_directory ? 0 : 
object.size), Field()};
+        if (!is_directory) {
+            File value(DataTypeFile::FIELD_COUNT);
+            value[0] = fields[0];
+            value[2] = fields[1];
+            value[3] = Field::create_field<TYPE_STRING>(
+                    
std::string(infer_file_content_type_from_name(object.key)));
+            if (auto checksum = file_checksum_from_etag(object.etag)) {
+                value[4] = 
Field::create_field<TYPE_STRING>(std::move(*checksum));
+            }
+            RETURN_IF_ERROR(validate_file(value));
+            fields[2] = Field::create_field<TYPE_FILE>(std::move(value));
         }
-        RETURN_IF_ERROR(validate_file(value));
-        std::array<Field, 2> fields {value[0], 
Field::create_field<TYPE_FILE>(std::move(value))};
         for (size_t i = 0; i < columns.size(); ++i) {
             columns[i]->insert(fields[_column_indices[i]]);
         }
diff --git a/be/src/io/fs/azure_obj_storage_client.cpp 
b/be/src/io/fs/azure_obj_storage_client.cpp
index b955d0f36b8..2df2b2a6289 100644
--- a/be/src/io/fs/azure_obj_storage_client.cpp
+++ b/be/src/io/fs/azure_obj_storage_client.cpp
@@ -347,7 +347,9 @@ ObjectStorageListPageResponse 
AzureObjStorageClient::list_objects_page(
                 if (opts.delimiter.empty()) {
                     make_page(_client->ListBlobs(request));
                 } else {
-                    make_page(_client->ListBlobsByHierarchy(opts.delimiter, 
request));
+                    auto response = 
_client->ListBlobsByHierarchy(opts.delimiter, request);
+                    page.common_prefixes = std::move(response.BlobPrefixes);
+                    make_page(std::move(response));
                 }
             },
             opts, _tls_debug_context);
diff --git a/be/src/io/fs/obj_storage_client.h 
b/be/src/io/fs/obj_storage_client.h
index 686ac9c84f3..d7496bbd4b6 100644
--- a/be/src/io/fs/obj_storage_client.h
+++ b/be/src/io/fs/obj_storage_client.h
@@ -96,6 +96,7 @@ struct ObjectMeta {
 struct ObjectStorageListPageResponse {
     ObjectStorageResponse resp {};
     std::vector<ObjectMeta> objects {};
+    std::vector<std::string> common_prefixes {};
     std::string continuation_token {};
     bool has_more = false;
 };
diff --git a/be/src/io/fs/s3_obj_storage_client.cpp 
b/be/src/io/fs/s3_obj_storage_client.cpp
index ea38f1998d5..4eddb523d7f 100644
--- a/be/src/io/fs/s3_obj_storage_client.cpp
+++ b/be/src/io/fs/s3_obj_storage_client.cpp
@@ -423,6 +423,10 @@ ObjectStorageListPageResponse 
S3ObjStorageClient::list_objects_page(
             .continuation_token = result.GetNextContinuationToken(),
             .has_more = result.GetIsTruncated(),
     };
+    page.common_prefixes.reserve(result.GetCommonPrefixes().size());
+    for (const auto& entry : result.GetCommonPrefixes()) {
+        page.common_prefixes.emplace_back(entry.GetPrefix());
+    }
     page.objects.reserve(result.GetContents().size());
     for (const auto& obj : result.GetContents()) {
         page.objects.push_back(
diff --git a/be/test/exec/common/data_gen_functions/vlist_file_tvf_test.cpp 
b/be/test/exec/common/data_gen_functions/vlist_file_tvf_test.cpp
index 3beb6907520..4d46b2318a6 100644
--- a/be/test/exec/common/data_gen_functions/vlist_file_tvf_test.cpp
+++ b/be/test/exec/common/data_gen_functions/vlist_file_tvf_test.cpp
@@ -29,6 +29,7 @@
 
 #include "core/column/column_file.h"
 #include "core/data_type/data_type_file.h"
+#include "core/data_type/data_type_nullable.h"
 #include "core/data_type/data_type_string.h"
 #include "core/field.h"
 #include "exec/operator/datagen_operator.h"
@@ -82,7 +83,7 @@ protected:
     void SetUp() override {
         descriptor = std::make_unique<MockRowDescriptor>(
                 std::vector<DataTypePtr> {std::make_shared<DataTypeString>(),
-                                          std::make_shared<DataTypeFile>()},
+                                          
make_nullable(std::make_shared<DataTypeFile>())},
                 &pool);
         const std::array<std::string, 2> names {"path", "file"};
         for (size_t i = 0; i < names.size(); ++i) {
@@ -206,7 +207,7 @@ TEST_F(ListFileTVFTest, 
EmitsCompleteFilesAndSkipsDirectoryMarkers) {
     ASSERT_TRUE(function->get_next(&state, &block, &eos).ok());
     ASSERT_EQ(block.rows(), 3);
     EXPECT_TRUE(eos);
-    EXPECT_FALSE(block.get_by_position(1).type->is_nullable());
+    EXPECT_TRUE(block.get_by_position(1).type->is_nullable());
     EXPECT_EQ((*block.get_by_position(0).column)[0].get<TYPE_STRING>(),
               "s3://bucket/dir/empty.txt");
     const auto empty = row(block, 0);
@@ -299,7 +300,7 @@ TEST_F(ListFileTVFTest, 
SplitsPagesIntoBatchesWithoutPrefetching) {
 }
 
 TEST_F(ListFileTVFTest, ContinuesPastEmptyAndMarkerOnlyIntermediatePages) {
-    ASSERT_TRUE(function->set_scan_ranges(ranges()).ok());
+    ASSERT_TRUE(function->set_scan_ranges(ranges("s3://bucket/dir/", 
true)).ok());
     testing::InSequence sequence;
     EXPECT_CALL(*s3, ListObjectsV2(testing::_))
             .WillOnce(testing::Return(ListObjectsV2Outcome(page({}, 
"empty"))));
diff --git a/be/test/exec/common/data_gen_functions/vlist_files_rows_test.cpp 
b/be/test/exec/common/data_gen_functions/vlist_files_rows_test.cpp
new file mode 100644
index 00000000000..73eeec8d2e5
--- /dev/null
+++ b/be/test/exec/common/data_gen_functions/vlist_files_rows_test.cpp
@@ -0,0 +1,102 @@
+// 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 <gen_cpp/Descriptors_types.h>
+#include <gen_cpp/PlanNodes_types.h>
+#include <gtest/gtest.h>
+
+#include <array>
+
+#include "core/data_type/data_type_file.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "exec/common/data_gen_functions/vlist_file_tvf.h"
+#include "runtime/descriptors.h"
+#include "runtime/runtime_state.h"
+
+namespace doris {
+
+TEST(ListFilesRowsTest, DirectoryNullsSizesAndProjectionsAcrossBatches) {
+    const std::array<std::string, 3> names {"path", "size", "file"};
+    const std::array<DataTypePtr, 3> types {std::make_shared<DataTypeString>(),
+                                            std::make_shared<DataTypeInt64>(),
+                                            std::make_shared<DataTypeFile>()};
+    const std::vector<std::vector<size_t>> projections {{0, 1, 2}, {0}, {1}, 
{2}, {2, 0}};
+    for (bool recursive : {false, true}) {
+        for (const auto& projection : projections) {
+            TTupleDescriptor tuple_spec;
+            tuple_spec.__set_id(0);
+            TupleDescriptor tuple(tuple_spec, true);
+            for (const auto index : projection) {
+                TSlotDescriptor slot;
+                slot.__set_id(static_cast<int>(index));
+                slot.__set_parent(0);
+                slot.__set_colName(names[index]);
+                slot.__set_slotType(types[index]->to_thrift());
+                slot.__set_nullIndicatorBit(index == 2 ? 0 : -1);
+                tuple.add_slot(new SlotDescriptor(slot));
+            }
+            VListFileTVF function(0, &tuple);
+            TTVFListFileScanRange params;
+            params.__set_uri("s3://bucket/a/");
+            params.__set_recursive(recursive);
+            TScanRangeParams range;
+            
range.scan_range.data_gen_scan_range.__set_list_file_params(params);
+            ASSERT_TRUE(function.set_scan_ranges({range}).ok());
+            function._objects = {{.key = "a/"},
+                                 {.key = "a/empty.txt"},
+                                 {.key = "a/b/"},
+                                 {.key = "a/c.png", .size = 3},
+                                 {.key = "a/marker/", .size = 7}};
+            function._has_more = false;
+            RuntimeState state;
+            state._query_options.__set_batch_size(1);
+            Block block;
+            bool eos = false;
+            size_t rows = 0;
+            while (!eos) {
+                ASSERT_TRUE(function.get_next(&state, &block, &eos).ok());
+                for (size_t row = 0; row < block.rows(); ++row) {
+                    const bool directory = !recursive && (rows == 1 || rows == 
3);
+                    const int64_t size = rows == (recursive ? 1U : 2U) ? 3 : 0;
+                    for (size_t column = 0; column < projection.size(); 
++column) {
+                        const auto field = 
(*block.get_by_position(column).column)[row];
+                        switch (projection[column]) {
+                        case 0:
+                            EXPECT_EQ(field.get<TYPE_STRING>().ends_with('/'), 
directory);
+                            break;
+                        case 1:
+                            EXPECT_EQ(field.get<TYPE_BIGINT>(), size);
+                            break;
+                        case 2:
+                            EXPECT_EQ(field.is_null(), directory);
+                            if (!directory) {
+                                
EXPECT_EQ(field.get<TYPE_FILE>()[2].get<TYPE_BIGINT>(), size);
+                            }
+                            break;
+                        }
+                    }
+                    ++rows;
+                }
+                block.clear_column_data();
+            }
+            EXPECT_EQ(rows, recursive ? 2 : 4);
+        }
+    }
+}
+
+} // namespace doris
diff --git a/be/test/io/fs/azure_obj_storage_client_test.cpp 
b/be/test/io/fs/azure_obj_storage_client_test.cpp
index 2e5f94d9882..9f2fd1c8a5e 100644
--- a/be/test/io/fs/azure_obj_storage_client_test.cpp
+++ b/be/test/io/fs/azure_obj_storage_client_test.cpp
@@ -35,6 +35,7 @@
 
 #include "core/block/block.h"
 #include "core/data_type/data_type_file.h"
+#include "core/data_type/data_type_nullable.h"
 #include "core/data_type/data_type_string.h"
 #include "core/field.h"
 #include "core/value/file_value.h"
@@ -223,8 +224,9 @@ TEST_F(AzureFileMetadataTest, 
ListingEtagsReachFileChecksumsWithoutHeadRequests)
     for (const bool recursive : {false, true}) {
         auto transport = std::make_shared<AzureListTransport>();
         ObjectPool pool;
-        MockRowDescriptor descriptor(
-                {std::make_shared<DataTypeString>(), 
std::make_shared<DataTypeFile>()}, &pool);
+        MockRowDescriptor descriptor({std::make_shared<DataTypeString>(),
+                                      
make_nullable(std::make_shared<DataTypeFile>())},
+                                     &pool);
         descriptor.tuple_desc_map[0]->slots()[0]->_col_name = "path";
         descriptor.tuple_desc_map[0]->slots()[1]->_col_name = "file";
         VListFileTVF listing(0, descriptor.tuple_desc_map[0]);
@@ -242,14 +244,21 @@ TEST_F(AzureFileMetadataTest, 
ListingEtagsReachFileChecksumsWithoutHeadRequests)
         while (!eos) {
             ASSERT_TRUE(listing.get_next(&state, &block, &eos).ok());
             for (size_t i = 0; i < block.rows(); ++i) {
-                const auto value = 
(*block.get_by_position(1).column)[i].get<TYPE_FILE>();
+                const auto field = (*block.get_by_position(1).column)[i];
+                if (field.is_null()) {
+                    EXPECT_FALSE(recursive);
+                    
EXPECT_EQ((*block.get_by_position(0).column)[i].get<TYPE_STRING>(),
+                              "s3://container/tree/sub/");
+                    continue;
+                }
+                const auto value = field.get<TYPE_FILE>();
                 ASSERT_FALSE(value[4].is_null());
                 EXPECT_EQ(value[4].get<TYPE_STRING>(), "ETAG:opaque-2");
             }
             rows += block.rows();
             block.clear_column_data();
         }
-        EXPECT_EQ(rows, recursive ? 3 : 2);
+        EXPECT_EQ(rows, 3);
         EXPECT_EQ(transport->requests, 2);
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableValuedFunctions.java
 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableValuedFunctions.java
index ed124fb4fa5..4914ba4869f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableValuedFunctions.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableValuedFunctions.java
@@ -31,7 +31,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.table.HttpStream;
 import org.apache.doris.nereids.trees.expressions.functions.table.HudiMeta;
 import org.apache.doris.nereids.trees.expressions.functions.table.Jobs;
 import 
org.apache.doris.nereids.trees.expressions.functions.table.LanceIndexEntries;
-import org.apache.doris.nereids.trees.expressions.functions.table.ListFile;
+import org.apache.doris.nereids.trees.expressions.functions.table.ListFiles;
 import org.apache.doris.nereids.trees.expressions.functions.table.Local;
 import org.apache.doris.nereids.trees.expressions.functions.table.MvInfos;
 import org.apache.doris.nereids.trees.expressions.functions.table.Numbers;
@@ -65,7 +65,7 @@ public class BuiltinTableValuedFunctions implements 
FunctionHelper {
             tableValued(HudiMeta.class, "hudi_meta"),
             tableValued(Hdfs.class, "hdfs"),
             tableValued(HttpStream.class, "http_stream"),
-            tableValued(ListFile.class, "list_file"),
+            tableValued(ListFiles.class, "list_files"),
             tableValued(Numbers.class, "numbers"),
             tableValued(S3.class, "s3"),
             tableValued(MvInfos.class, "mv_infos"),
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/table/ListFile.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/table/ListFiles.java
similarity index 78%
rename from 
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/table/ListFile.java
rename to 
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/table/ListFiles.java
index 66ce307a8cf..6dcd00ff755 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/table/ListFile.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/table/ListFiles.java
@@ -23,17 +23,17 @@ import 
org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.Properties;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.FileType;
-import org.apache.doris.tablefunction.ListFileTableValuedFunction;
+import org.apache.doris.tablefunction.ListFilesTableValuedFunction;
 import org.apache.doris.tablefunction.TableValuedFunctionIf;
 
 import com.google.common.base.Preconditions;
 
 import java.util.List;
 
-/** list_file */
-public class ListFile extends TableValuedFunction {
-    public ListFile(Properties properties) {
-        super(ListFileTableValuedFunction.NAME, properties);
+/** list_files */
+public class ListFiles extends TableValuedFunction {
+    public ListFiles(Properties properties) {
+        super(ListFilesTableValuedFunction.NAME, properties);
     }
 
     @Override
@@ -44,20 +44,20 @@ public class ListFile extends TableValuedFunction {
     @Override
     protected TableValuedFunctionIf toCatalogFunction() {
         try {
-            return new 
ListFileTableValuedFunction(getTVFProperties().getMap());
+            return new 
ListFilesTableValuedFunction(getTVFProperties().getMap());
         } catch (org.apache.doris.common.AnalysisException e) {
-            throw new AnalysisException("Can not build list_file(): " + 
e.getMessage(), e);
+            throw new AnalysisException("Can not build list_files(): " + 
e.getMessage(), e);
         }
     }
 
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
-        return visitor.visitListFile(this, context);
+        return visitor.visitListFiles(this, context);
     }
 
     @Override
-    public ListFile withChildren(List<Expression> children) {
+    public ListFiles withChildren(List<Expression> children) {
         Preconditions.checkArgument(children.size() == 1 && children.get(0) 
instanceof Properties);
-        return new ListFile((Properties) children.get(0));
+        return new ListFiles((Properties) children.get(0));
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableValuedFunctionVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableValuedFunctionVisitor.java
index 3ad4e7c901b..43953eb6f3f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableValuedFunctionVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableValuedFunctionVisitor.java
@@ -29,7 +29,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.table.Http;
 import org.apache.doris.nereids.trees.expressions.functions.table.HttpStream;
 import org.apache.doris.nereids.trees.expressions.functions.table.HudiMeta;
 import org.apache.doris.nereids.trees.expressions.functions.table.Jobs;
-import org.apache.doris.nereids.trees.expressions.functions.table.ListFile;
+import org.apache.doris.nereids.trees.expressions.functions.table.ListFiles;
 import org.apache.doris.nereids.trees.expressions.functions.table.Local;
 import org.apache.doris.nereids.trees.expressions.functions.table.MvInfos;
 import org.apache.doris.nereids.trees.expressions.functions.table.Numbers;
@@ -113,8 +113,8 @@ public interface TableValuedFunctionVisitor<R, C> {
         return visitTableValuedFunction(local, context);
     }
 
-    default R visitListFile(ListFile listFile, C context) {
-        return visitTableValuedFunction(listFile, context);
+    default R visitListFiles(ListFiles listFiles, C context) {
+        return visitTableValuedFunction(listFiles, context);
     }
 
     default R visitNumbers(Numbers numbers, C context) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ListFileTableValuedFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ListFilesTableValuedFunction.java
similarity index 87%
rename from 
fe/fe-core/src/main/java/org/apache/doris/tablefunction/ListFileTableValuedFunction.java
rename to 
fe/fe-core/src/main/java/org/apache/doris/tablefunction/ListFilesTableValuedFunction.java
index ded4516d183..952d8c2174c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ListFileTableValuedFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ListFilesTableValuedFunction.java
@@ -44,15 +44,15 @@ import java.util.Locale;
 import java.util.Map;
 
 /** Resource-backed S3 directory listing, executed by one backend. */
-public class ListFileTableValuedFunction extends DataGenTableValuedFunction {
-    public static final String NAME = "list_file";
+public class ListFilesTableValuedFunction extends DataGenTableValuedFunction {
+    public static final String NAME = "list_files";
     private static final ImmutableSet<String> PROPERTIES = 
ImmutableSet.of("resource", "uri", "recursive");
 
     private final String resourceName;
     private final String uri;
     private final boolean recursive;
 
-    public ListFileTableValuedFunction(Map<String, String> params) throws 
AnalysisException {
+    public ListFilesTableValuedFunction(Map<String, String> params) throws 
AnalysisException {
         Map<String, String> properties = new HashMap<>();
         for (Map.Entry<String, String> entry : params.entrySet()) {
             String key = entry.getKey().toLowerCase(Locale.ROOT);
@@ -64,14 +64,14 @@ public class ListFileTableValuedFunction extends 
DataGenTableValuedFunction {
         resourceName = properties.get("resource");
         uri = properties.get("uri");
         if (resourceName == null || resourceName.isEmpty()) {
-            throw new AnalysisException("list_file resource is required");
+            throw new AnalysisException("list_files resource is required");
         }
         if (uri == null) {
-            throw new AnalysisException("list_file uri is required");
+            throw new AnalysisException("list_files uri is required");
         }
-        String recursiveValue = properties.getOrDefault("recursive", "false");
+        String recursiveValue = properties.getOrDefault("recursive", "true");
         if (!"true".equalsIgnoreCase(recursiveValue) && 
!"false".equalsIgnoreCase(recursiveValue)) {
-            throw new AnalysisException("list_file recursive must be true or 
false");
+            throw new AnalysisException("list_files recursive must be true or 
false");
         }
         recursive = Boolean.parseBoolean(recursiveValue);
         checkS3Resource(ConnectContext.get());
@@ -93,14 +93,15 @@ public class ListFileTableValuedFunction extends 
DataGenTableValuedFunction {
 
     @Override
     public String getTableName() {
-        return "ListFileTableValuedFunction";
+        return "ListFilesTableValuedFunction";
     }
 
     @Override
     public List<Column> getTableColumns() {
         return ImmutableList.of(
                 new Column("path", Type.STRING, false),
-                new Column("file", FileType.create(), false));
+                new Column("size", Type.BIGINT, false),
+                new Column("file", FileType.create(), true));
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/TableValuedFunctionIf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/TableValuedFunctionIf.java
index f9d81c04a5d..5f295ece185 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/TableValuedFunctionIf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/TableValuedFunctionIf.java
@@ -47,8 +47,8 @@ public abstract class TableValuedFunctionIf {
     public static TableValuedFunctionIf getTableFunction(String funcName, 
Map<String, String> params)
             throws AnalysisException {
         switch (funcName.toLowerCase()) {
-            case ListFileTableValuedFunction.NAME:
-                return new ListFileTableValuedFunction(params);
+            case ListFilesTableValuedFunction.NAME:
+                return new ListFilesTableValuedFunction(params);
             case NumbersTableValuedFunction.NAME:
                 return new NumbersTableValuedFunction(params);
             case S3TableValuedFunction.NAME:
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ListFileTableValuedFunctionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ListFilesTableValuedFunctionTest.java
similarity index 88%
rename from 
fe/fe-core/src/test/java/org/apache/doris/tablefunction/ListFileTableValuedFunctionTest.java
rename to 
fe/fe-core/src/test/java/org/apache/doris/tablefunction/ListFilesTableValuedFunctionTest.java
index 7dcdc19a8ab..0036741ad6c 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ListFileTableValuedFunctionTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ListFilesTableValuedFunctionTest.java
@@ -27,7 +27,7 @@ import 
org.apache.doris.datasource.property.storage.S3Properties;
 import org.apache.doris.mysql.privilege.AccessControllerManager;
 import org.apache.doris.mysql.privilege.PrivPredicate;
 import org.apache.doris.nereids.trees.expressions.Properties;
-import org.apache.doris.nereids.trees.expressions.functions.table.ListFile;
+import org.apache.doris.nereids.trees.expressions.functions.table.ListFiles;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.qe.SessionVariable;
 import org.apache.doris.system.Backend;
@@ -45,15 +45,15 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-class ListFileTableValuedFunctionTest {
+class ListFilesTableValuedFunctionTest {
     @Test
-    void exposesPathSizeNullableTimestampAndFileColumnsWithoutRemoteIo() 
throws Exception {
+    void exposesPathSizeAndNullableFileColumnsWithoutRemoteIo() throws 
Exception {
         try (Fixture fixture = new Fixture()) {
-            TableValuedFunctionIf function = 
TableValuedFunctionIf.getTableFunction("LIST_FILE", fixture.params());
+            TableValuedFunctionIf function = 
TableValuedFunctionIf.getTableFunction("LIST_FILES", fixture.params());
             List<Column> columns = function.getTableColumns();
             assertSchema(columns);
             Assertions.assertInstanceOf(DataGenTableValuedFunction.class, 
function);
-            ListFile nereids = new ListFile(new Properties(fixture.params()));
+            ListFiles nereids = new ListFiles(new 
Properties(fixture.params()));
             assertSchema(nereids.getTableColumns());
         }
     }
@@ -73,10 +73,10 @@ class ListFileTableValuedFunctionTest {
     @Test
     void acceptsCaseInsensitiveKeysAndStrictBooleanValues() throws Exception {
         try (Fixture fixture = new Fixture()) {
-            ListFileTableValuedFunction function = new 
ListFileTableValuedFunction(
+            ListFilesTableValuedFunction function = new 
ListFilesTableValuedFunction(
                     ImmutableMap.of("RESOURCE", "s3_resource", "URI", 
"s3://bucket/", "RECURSIVE", "TRUE"));
             Assertions.assertTrue(range(function).isRecursive());
-            Assertions.assertFalse(range(new 
ListFileTableValuedFunction(fixture.params())).isRecursive());
+            Assertions.assertTrue(range(new 
ListFilesTableValuedFunction(fixture.params())).isRecursive());
             for (String value : new String[] {"yes", "1", "", " false "}) {
                 Map<String, String> params = fixture.params();
                 params.put("recursive", value);
@@ -91,7 +91,7 @@ class ListFileTableValuedFunctionTest {
             for (String uri : new String[] {"s3://bucket", "s3://bucket/", 
"s3://bucket/dir", "s3://bucket/dir/"}) {
                 Map<String, String> params = fixture.params();
                 params.put("uri", uri);
-                Assertions.assertEquals(uri, range(new 
ListFileTableValuedFunction(params)).getUri());
+                Assertions.assertEquals(uri, range(new 
ListFilesTableValuedFunction(params)).getUri());
             }
         }
     }
@@ -103,7 +103,7 @@ class ListFileTableValuedFunctionTest {
                     "s3://another-bucket/a b+中文%ZZ#fragment"}) {
                 Map<String, String> params = fixture.params();
                 params.put("uri", uri);
-                Assertions.assertEquals(uri, range(new 
ListFileTableValuedFunction(params)).getUri());
+                Assertions.assertEquals(uri, range(new 
ListFilesTableValuedFunction(params)).getUri());
             }
         }
     }
@@ -125,7 +125,7 @@ class ListFileTableValuedFunctionTest {
     void queryConfigurationIsSharedWithToFileAndRefreshesNextExecute() throws 
Exception {
         try (Fixture fixture = new Fixture()) {
             fixture.properties.put("s3.secret_key", "prepare");
-            ListFileTableValuedFunction function = new 
ListFileTableValuedFunction(fixture.params());
+            ListFilesTableValuedFunction function = new 
ListFilesTableValuedFunction(fixture.params());
             fixture.properties.put("s3.secret_key", "first execute");
             fixture.context.getOrRegisterS3Resource("s3_resource");
             fixture.properties.put("s3.secret_key", "second execute");
@@ -149,7 +149,7 @@ class ListFileTableValuedFunctionTest {
             Backend dead = Mockito.mock(Backend.class);
             Mockito.when(fixture.system.getBackendsByCurrentCluster())
                     .thenReturn(ImmutableMap.of(1L, dead, 2L, 
fixture.backend));
-            ListFileTableValuedFunction function = new 
ListFileTableValuedFunction(fixture.params());
+            ListFilesTableValuedFunction function = new 
ListFilesTableValuedFunction(fixture.params());
             List<TableValuedFunctionTask> tasks = function.getTasks();
             Assertions.assertEquals(1, tasks.size());
             Assertions.assertSame(fixture.backend, tasks.get(0).getBackend());
@@ -161,22 +161,25 @@ class ListFileTableValuedFunctionTest {
     }
 
     private static void assertSchema(List<Column> columns) {
-        Assertions.assertEquals(2, columns.size());
+        Assertions.assertEquals(3, columns.size());
         Assertions.assertEquals("path", columns.get(0).getName());
         Assertions.assertEquals(Type.STRING, columns.get(0).getType());
         Assertions.assertFalse(columns.get(0).isAllowNull());
-        Assertions.assertEquals("file", columns.get(1).getName());
-        Assertions.assertTrue(columns.get(1).getType().isFileType());
+        Assertions.assertEquals("size", columns.get(1).getName());
+        Assertions.assertEquals(Type.BIGINT, columns.get(1).getType());
         Assertions.assertFalse(columns.get(1).isAllowNull());
+        Assertions.assertEquals("file", columns.get(2).getName());
+        Assertions.assertTrue(columns.get(2).getType().isFileType());
+        Assertions.assertTrue(columns.get(2).isAllowNull());
     }
 
-    private static TTVFListFileScanRange range(ListFileTableValuedFunction 
function) throws Exception {
+    private static TTVFListFileScanRange range(ListFilesTableValuedFunction 
function) throws Exception {
         return 
function.getTasks().get(0).getExecParams().getDataGenScanRange().getListFileParams();
     }
 
     private static void assertInvalid(Map<String, String> params, String 
message) {
         Exception error = Assertions.assertThrows(Exception.class,
-                () -> TableValuedFunctionIf.getTableFunction("list_file", 
params));
+                () -> TableValuedFunctionIf.getTableFunction("list_files", 
params));
         Assertions.assertTrue(error.getMessage().contains(message), 
error.getMessage());
     }
 
diff --git 
a/regression-test/data/datatype_p0/complex_types/test_list_file_resource_local.out
 
b/regression-test/data/datatype_p0/complex_types/test_list_file_resource_local.out
deleted file mode 100644
index 52112823787..00000000000
--- 
a/regression-test/data/datatype_p0/complex_types/test_list_file_resource_local.out
+++ /dev/null
@@ -1,165 +0,0 @@
--- This file is automatically generated. You should know what you did if you 
want to edit this
--- !creation_no_io --
-0
-
--- !schema --
-path   text    No      false   \N      NONE
-file   FILE    No      false   \N      NONE
-
--- !schema_no_io --
-0
-
--- !default --
-s3://list-file-local/dir/a-empty.txt   
{"uri":"s3://list-file-local/dir/a-empty.txt","offset":null,"size":0,"content_type":"text/plain","checksum":null,"inline":null}
-s3://list-file-local/dir/b.json        
{"uri":"s3://list-file-local/dir/b.json","offset":null,"size":7,"content_type":"application/json","checksum":"ETAG:list-7-2","inline":null}
-s3://list-file-local/dir/c.PNG 
{"uri":"s3://list-file-local/dir/c.PNG","offset":null,"size":11,"content_type":"image/png","checksum":"ETAG:list-11-2","inline":null}
-s3://list-file-local/dir/no_extension  
{"uri":"s3://list-file-local/dir/no_extension","offset":null,"size":13,"content_type":"application/octet-stream","checksum":null,"inline":null}
-
--- !default_protocol --
-4      true    true
-
--- !listing_checksums --
-s3://list-file-local/dir/a-empty.txt   \N
-s3://list-file-local/dir/b.json        ETAG:list-7-2
-s3://list-file-local/dir/c.PNG ETAG:list-11-2
-s3://list-file-local/dir/no_extension  \N
-
--- !explicit_false --
-s3://list-file-local/dir/a-empty.txt   
{"uri":"s3://list-file-local/dir/a-empty.txt","offset":null,"size":0,"content_type":"text/plain","checksum":null,"inline":null}
-s3://list-file-local/dir/b.json        
{"uri":"s3://list-file-local/dir/b.json","offset":null,"size":7,"content_type":"application/json","checksum":"ETAG:list-7-2","inline":null}
-s3://list-file-local/dir/c.PNG 
{"uri":"s3://list-file-local/dir/c.PNG","offset":null,"size":11,"content_type":"image/png","checksum":"ETAG:list-11-2","inline":null}
-s3://list-file-local/dir/no_extension  
{"uri":"s3://list-file-local/dir/no_extension","offset":null,"size":13,"content_type":"application/octet-stream","checksum":null,"inline":null}
-
--- !path_only --
-s3://list-file-local/dir/a-empty.txt
-s3://list-file-local/dir/b.json
-s3://list-file-local/dir/c.PNG
-s3://list-file-local/dir/no_extension
-
--- !size_only --
-0
-7
-11
-13
-
--- !file_only --
-{"uri":"s3://list-file-local/dir/a-empty.txt","offset":null,"size":0,"content_type":"text/plain","checksum":null,"inline":null}
-{"uri":"s3://list-file-local/dir/b.json","offset":null,"size":7,"content_type":"application/json","checksum":"ETAG:list-7-2","inline":null}
-{"uri":"s3://list-file-local/dir/c.PNG","offset":null,"size":11,"content_type":"image/png","checksum":"ETAG:list-11-2","inline":null}
-{"uri":"s3://list-file-local/dir/no_extension","offset":null,"size":13,"content_type":"application/octet-stream","checksum":null,"inline":null}
-
--- !recursive_getters --
-s3://list-file-local/dir/a-empty.txt   0       true    \N      text/plain      
\N      false
-s3://list-file-local/dir/b.json        7       true    \N      
application/json        ETAG:list-7-2   false
-s3://list-file-local/dir/c.PNG 11      true    \N      image/png       
ETAG:list-11-2  false
-s3://list-file-local/dir/no_extension  13      true    \N      
application/octet-stream        \N      false
-s3://list-file-local/dir/sub/deep/payload.parquet      19      true    \N      
application/x-parquet   ETAG:list-19-2  false
-s3://list-file-local/dir/sub/nested.csv        17      true    \N      
text/csv        ETAG:list-17-2  false
-
--- !recursive_protocol --
-6      true    true
-
--- !root --
-s3://list-file-local/root.txt  23
-
--- !root_slash --
-s3://list-file-local/root.txt  23
-
--- !empty_directory --
-
--- !count --
-6      6       67
-
--- !parallel_count --
-6      6       67
-
--- !parallel_requests --
-6
-
--- !parallel_count_repeat --
-6      6       67
-
--- !parallel_requests_repeat --
-6
-
--- !parallel_paths --
-s3://list-file-local/dir/a-empty.txt
-s3://list-file-local/dir/b.json
-s3://list-file-local/dir/c.PNG
-s3://list-file-local/dir/no_extension
-s3://list-file-local/dir/sub/deep/payload.parquet
-s3://list-file-local/dir/sub/nested.csv
-
--- !limit --
-s3://list-file-local/dir/a-empty.txt   
{"uri":"s3://list-file-local/dir/a-empty.txt","offset":null,"size":0,"content_type":"text/plain","checksum":null,"inline":null}
-
--- !limit_requests --
-1
-
--- !limit_zero --
-
--- !limit_zero_requests --
-0
-
--- !canonical_list --
-s3://list-file-local/dir/a-empty.txt   
{"uri":"s3://list-file-local/dir/a-empty.txt","offset":null,"size":0,"content_type":"text/plain","checksum":null,"inline":null}
-s3://list-file-local/dir/b.json        
{"uri":"s3://list-file-local/dir/b.json","offset":null,"size":7,"content_type":"application/json","checksum":"ETAG:list-7-2","inline":null}
-s3://list-file-local/dir/c.PNG 
{"uri":"s3://list-file-local/dir/c.PNG","offset":null,"size":11,"content_type":"image/png","checksum":"ETAG:list-11-2","inline":null}
-s3://list-file-local/dir/no_extension  
{"uri":"s3://list-file-local/dir/no_extension","offset":null,"size":13,"content_type":"application/octet-stream","checksum":null,"inline":null}
-
--- !canonical_list_protocol --
-true
-
--- !canonical_to_file_head --
-{"uri":"s3://list-file-local/dir/b.json","offset":null,"size":7,"content_type":"application/json","checksum":"ETAG:canonical-head-etag","inline":null}
-
--- !canonical_head_protocol --
-1      true
-
--- !canonical_mixed_buckets --
-1      
{"uri":"s3://list-file-local/dir/b.json","offset":null,"size":7,"content_type":"application/json","checksum":"ETAG:canonical-head-etag","inline":null}
-2      
{"uri":"s3://list-file-other/dir/b.json","offset":null,"size":31,"content_type":"application/json","checksum":"ETAG:other-bucket-head-etag","inline":null}
-3      
{"uri":"s3://list-file-local/dir/b.json","offset":null,"size":7,"content_type":"application/json","checksum":"ETAG:canonical-head-etag","inline":null}
-
--- !list_uri_bucket --
-s3://other-bucket/dir/a-empty.txt      0
-s3://other-bucket/dir/b.json   7
-s3://other-bucket/dir/c.PNG    11
-s3://other-bucket/dir/no_extension     13
-
--- !invalid_no_io --
-0
-
--- !denied_no_io --
-0
-
--- !granted_count --
-4
-
--- !raw_uri_roundtrip --
-s3://list-file-local/roundtrip/a b.txt 101     
s3://list-file-local/roundtrip/a b.txt  101
-s3://list-file-local/roundtrip/a%20b.txt       102     
s3://list-file-local/roundtrip/a%20b.txt        102
-s3://list-file-local/roundtrip/a%2Fb.txt       105     
s3://list-file-local/roundtrip/a%2Fb.txt        105
-s3://list-file-local/roundtrip/a+b.txt 103     
s3://list-file-local/roundtrip/a+b.txt  103
-s3://list-file-local/roundtrip/a/b.txt 106     
s3://list-file-local/roundtrip/a/b.txt  106
-s3://list-file-local/roundtrip/中文.txt  104     
s3://list-file-local/roundtrip/中文.txt   104
-
--- !raw_uri_request_protocol --
-6      true    true
-
--- !literal_plus --
-{"uri":"s3://list-file-local/roundtrip/a+b.txt","offset":null,"size":103,"content_type":"application/json","checksum":"ETAG:roundtrip-103","inline":null}
-
--- !listing_head_checksum_consistency --
-s3://list-file-local/etag-consistency/absent.txt       \N      \N      true
-s3://list-file-local/etag-consistency/bare-multipart.txt       
ETAG:MiXeD:opaque-2     ETAG:MiXeD:opaque-2     true
-s3://list-file-local/etag-consistency/empty.txt        \N      \N      true
-s3://list-file-local/etag-consistency/quoted-empty.txt \N      \N      true
-s3://list-file-local/etag-consistency/quoted-multipart.txt     
ETAG:MiXeD:opaque-2     ETAG:MiXeD:opaque-2     true
-
--- !listing_head_checksum_protocol --
-5      true    true
-
--- !handler_errors --
-0
-
diff --git a/regression-test/data/datatype_p0/complex_types/test_list_files.out 
b/regression-test/data/datatype_p0/complex_types/test_list_files.out
new file mode 100644
index 00000000000..2ca60063c11
--- /dev/null
+++ b/regression-test/data/datatype_p0/complex_types/test_list_files.out
@@ -0,0 +1,88 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !schema --
+path   text    No      false   \N      NONE
+size   bigint  No      false   \N      NONE
+file   FILE    Yes     false   \N      NONE
+
+-- !direct --
+a.json 2       false   2       application/json
+empty.txt      0       false   0       text/plain
+empty/ 0       true    \N      \N
+marker/        0       true    \N      \N
+sub/   0       true    \N      \N
+
+-- !direct_counts --
+5      2       2
+
+-- !recursive --
+a.json 2       false   2       application/json
+empty.txt      0       false   0       text/plain
+sub/c.png      3       false   3       image/png
+sub/deep/d.txt 4       false   4       text/plain
+
+-- !recursive_counts --
+4      4       9
+
+-- !defaults --
+a.json 2       false   2       application/json
+empty.txt      0       false   0       text/plain
+sub/c.png      3       false   3       image/png
+sub/deep/d.txt 4       false   4       text/plain
+
+-- !defaults_counts --
+4      4       9
+
+-- !size_only --
+2
+0
+0
+0
+0
+
+-- !path_only --
+a.json
+empty.txt
+empty/
+marker/
+sub/
+
+-- !file_only --
+false
+false
+true
+true
+true
+
+-- !limit --
+2
+
+-- !roundtrip --
+a.json true    true
+empty.txt      true    true
+sub/c.png      true    true
+sub/deep/d.txt true    true
+
+-- !no_trailing_slash --
+5      2       2
+
+-- !empty_direct --
+empty/deep/    0       true
+
+-- !empty_recursive --
+0
+
+-- !missing --
+0
+
+-- !directory_pages --
+1005   0       0       1005
+
+-- !directory_pages_recursive --
+0
+
+-- !file_pages --
+1005   1005    1005    1005
+
+-- !file_pages_direct --
+1005   1005    1005
+
diff --git 
a/regression-test/suites/datatype_p0/complex_types/test_list_file_resource_local.groovy
 
b/regression-test/suites/datatype_p0/complex_types/test_list_file_resource_local.groovy
deleted file mode 100644
index 097c4ab1870..00000000000
--- 
a/regression-test/suites/datatype_p0/complex_types/test_list_file_resource_local.groovy
+++ /dev/null
@@ -1,447 +0,0 @@
-// 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.
-
-import com.sun.net.httpserver.HttpExchange
-import com.sun.net.httpserver.HttpHandler
-import com.sun.net.httpserver.HttpServer
-import java.net.InetAddress
-import java.net.InetSocketAddress
-import java.net.NetworkInterface
-import java.net.URI
-import java.net.URLDecoder
-import java.util.concurrent.CopyOnWriteArrayList
-
-// The fixture runs in the regression JVM and requires a same-host FE/BE 
cluster.
-suite("test_list_file_resource_local") {
-    if 
(!context.config.otherConfigs.get("enableFileTypeLocalResource")?.toString()?.toBoolean())
 {
-        logger.info("Skip test_list_file_resource_local: 
enableFileTypeLocalResource requires a same-host cluster")
-        return
-    }
-    def backends = sql_return_maparray("SHOW BACKENDS").findAll { 
it.Alive.toString().toBoolean() }
-    if (URI.create(context.config.jdbcUrl.substring(5)).host != "127.0.0.1"
-            || backends.isEmpty() || backends.any {
-                
NetworkInterface.getByInetAddress(InetAddress.getByName(it.Host.toString())) == 
null
-            }) {
-        throw new IllegalStateException("enableFileTypeLocalResource requires 
JDBC on 127.0.0.1 and all live BEs on local network interfaces")
-    }
-
-    def requests = new CopyOnWriteArrayList<Map>()
-    def handlerErrors = new CopyOnWriteArrayList<String>()
-    def objects = [
-        "dir/": 0, "dir/a-empty.txt": 0, "dir/b.json": 7, "dir/c.PNG": 11,
-        "dir/no_extension": 13, "dir/sub/": 0, "dir/sub/nested.csv": 17,
-        "dir/sub/deep/": 0, "dir/sub/deep/payload.parquet": 19,
-        "dir2/": 0, "dir2/neighbor.txt": 21, "root.txt": 23
-    ]
-    def heads = [
-        "/list-file-local/dir/b.json": [size: objects["dir/b.json"], etag: 
'"canonical-head-etag"'],
-        "/list-file-other/dir/b.json": [size: 31, etag: 
'"other-bucket-head-etag"']
-    ]
-    def listingEtags = ["dir/a-empty.txt": "", "dir/no_extension": null]
-    HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 
0), 0)
-    server.createContext("/", { HttpExchange exchange ->
-        try {
-            def query = [:]
-            exchange.requestURI.rawQuery?.split("&")?.each { part ->
-                def pair = part.split("=", 2)
-                query[URLDecoder.decode(pair[0], "UTF-8")] =
-                        pair.length == 2 ? URLDecoder.decode(pair[1], "UTF-8") 
: ""
-            }
-            requests.add([method: exchange.requestMethod, path: 
exchange.requestURI.rawPath,
-                          decodedPath: exchange.requestURI.path, query: query])
-            exchange.responseHeaders.set("x-amz-request-id", 
"list-file-local-request")
-            if (exchange.requestMethod == "HEAD"
-                    && heads.containsKey(exchange.requestURI.path) && 
query.isEmpty()) {
-                // The same key in different buckets has distinct metadata; 
routing must follow the URI.
-                def metadata = heads[exchange.requestURI.path]
-                exchange.responseHeaders.set("Content-Length", 
metadata.size.toString())
-                exchange.responseHeaders.set("Content-Type", 
"application/json")
-                if (metadata.etag != null) {
-                    exchange.responseHeaders.set("ETag", metadata.etag)
-                }
-                exchange.sendResponseHeaders(200, -1)
-                return
-            }
-            if (exchange.requestMethod != "GET"
-                    || !(exchange.requestURI.rawPath in ["/list-file-local", 
"/list-file-local/", "/other-bucket", "/other-bucket/"])
-                    || query["list-type"] != "2") {
-                handlerErrors.add("Unexpected request: 
${exchange.requestMethod} ${exchange.requestURI}".toString())
-                exchange.sendResponseHeaders(400, -1)
-                return
-            }
-
-            def prefix = query["prefix"] ?: ""
-            def delimiter = query["delimiter"] ?: ""
-            def entries = [:]
-            objects.each { key, size ->
-                if (key.startsWith(prefix)) {
-                    def relative = key.substring(prefix.length())
-                    int separator = delimiter ? relative.indexOf(delimiter) : 
-1
-                    if (separator >= 0) {
-                        def commonPrefix = prefix + relative.substring(0, 
separator + delimiter.length())
-                        entries[commonPrefix] = [key: commonPrefix, common: 
true]
-                    } else {
-                        entries[key] = [key: key, size: size, common: false]
-                    }
-                }
-            }
-            def ordered = entries.values().sort { a, b -> a.key <=> b.key }
-            def token = query["continuation-token"] ?: "page:0"
-            int offset = token.substring(token.indexOf(":") + 1).toInteger()
-            int pageSize = Math.min(2, (query["max-keys"] ?: 
"1000").toInteger())
-            // An empty truncated page proves callers follow tokens rather 
than stopping on zero objects.
-            def page = token.startsWith("empty:") ? [] : 
ordered.drop(offset).take(pageSize)
-            int nextOffset = offset + page.size()
-            boolean truncated = nextOffset < ordered.size()
-            def nextToken = offset == 0 && !page.isEmpty() ? 
"empty:${nextOffset}" : "page:${nextOffset}"
-            def xml = new StringBuilder('<?xml version="1.0" 
encoding="UTF-8"?>')
-            xml.append('<ListBucketResult 
xmlns="http://s3.amazonaws.com/doc/2006-03-01/";>')
-            
xml.append("<Name>list-file-local</Name><Prefix>${prefix}</Prefix>")
-            
xml.append("<MaxKeys>${pageSize}</MaxKeys><KeyCount>${page.size()}</KeyCount>")
-            xml.append("<IsTruncated>${truncated}</IsTruncated>")
-            page.each { entry ->
-                if (entry.common) {
-                    
xml.append("<CommonPrefixes><Prefix>${entry.key}</Prefix></CommonPrefixes>")
-                } else {
-                    
xml.append("<Contents><Key>${entry.key}</Key><Size>${entry.size}</Size>")
-                    // Exercise present, empty and absent ETags through real 
AWS SDK XML parsing.
-                    def etag = listingEtags.containsKey(entry.key) ? 
listingEtags[entry.key]
-                            : '"list-' + entry.size + '-2"'
-                    if (etag != null) {
-                        xml.append("<ETag>${etag}</ETag>")
-                    }
-                    
xml.append('<StorageClass>STANDARD</StorageClass></Contents>')
-                }
-            }
-            if (truncated) {
-                
xml.append("<NextContinuationToken>${nextToken}</NextContinuationToken>")
-            }
-            xml.append('</ListBucketResult>')
-            byte[] body = xml.toString().getBytes("UTF-8")
-            exchange.responseHeaders.set("Content-Type", "application/xml")
-            exchange.sendResponseHeaders(200, body.length)
-            exchange.responseBody.write(body)
-        } catch (Exception e) {
-            handlerErrors.add(e.toString())
-        } finally {
-            exchange.close()
-        }
-    } as HttpHandler)
-    server.start()
-
-    try {
-        sql "DROP RESOURCE IF EXISTS 'list_file_local_s3'"
-        sql "DROP RESOURCE IF EXISTS 'list_file_local_canonical_s3'"
-        sql "DROP RESOURCE IF EXISTS 'list_file_local_hdfs'"
-        sql """
-            CREATE RESOURCE 'list_file_local_s3' PROPERTIES(
-                "type"="s3", 
"AWS_ENDPOINT"="http://127.0.0.1:${server.address.port}";,
-                "AWS_REGION"="us-east-1", "AWS_BUCKET"="list-file-local", 
"AWS_ROOT_PATH"="unused-root",
-                "AWS_ACCESS_KEY"="fake-list-file-key", 
"AWS_SECRET_KEY"="fake-list-file-secret",
-                "AWS_REQUEST_TIMEOUT_MS"="2000", 
"AWS_CONNECTION_TIMEOUT_MS"="500",
-                "use_path_style"="true", "s3_validity_check"="false")
-        """
-        sql """
-            CREATE RESOURCE 'list_file_local_canonical_s3' PROPERTIES(
-                "type"="s3", 
"s3.endpoint"="http://127.0.0.1:${server.address.port}";,
-                "s3.region"="us-east-1", "s3.bucket"="list-file-local", 
"s3.root.path"="unused-root",
-                "s3.access_key"="fake-list-file-key", 
"s3.secret_key"="fake-list-file-secret",
-                "s3.connection.request.timeout"="2000", 
"s3.connection.timeout"="500",
-                "use_path_style"="true", "s3_validity_check"="false")
-        """
-        sql """
-            CREATE RESOURCE 'list_file_local_hdfs' PROPERTIES(
-                "type"="hdfs", "fs.defaultFS"="hdfs://127.0.0.1:1",
-                "hadoop.username"="list_file_test", 
"ipc.client.connect.timeout"="100",
-                "ipc.client.connect.max.retries"="0")
-        """
-        qt_creation_no_io "SELECT ${requests.size()}"
-        qt_schema 'DESC FUNCTION list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/")'
-        qt_schema_no_io "SELECT ${requests.size()}"
-
-        qt_default """
-            SELECT * FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/")
-            ORDER BY path
-        """
-        qt_default_protocol """
-            SELECT ${requests.size()}, ${requests.every { it.method == "GET"
-                && it.path in ["/list-file-local", "/list-file-local/", 
"/other-bucket", "/other-bucket/"]
-                && it.query["list-type"] == "2" && it.query["prefix"] == "dir/"
-                && it.query["delimiter"] == "/" && 
it.query["max-keys"].toInteger() > 0 }},
-                ${requests.any { 
it.query["continuation-token"]?.startsWith("empty:") }}
-        """
-
-        requests.clear()
-        qt_listing_checksums """
-            SELECT path, ELEMENT_AT(file, 'checksum')
-            FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/")
-            ORDER BY path
-        """
-        assertTrue(!requests.isEmpty() && requests.every { it.method == "GET" 
},
-                "Listing ETags must not require per-object HEAD requests")
-
-        qt_explicit_false """
-            SELECT * FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir",
-                                    "recursive"="false") ORDER BY path
-        """
-        qt_path_only """
-            SELECT path FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/")
-            ORDER BY path
-        """
-        qt_size_only """
-            SELECT ELEMENT_AT(file, 'size') AS size FROM 
list_file("resource"="list_file_local_s3", "uri"="s3://list-file-local/dir/")
-            ORDER BY size
-        """
-        qt_file_only """
-            SELECT file FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/")
-            ORDER BY ELEMENT_AT(file, 'uri')
-        """
-        requests.clear()
-        qt_recursive_getters """
-            SELECT path, ELEMENT_AT(file, 'size') AS size, path = 
ELEMENT_AT(file, 'uri'),
-                   ELEMENT_AT(file, 'offset'), ELEMENT_AT(file, 
'content_type'), ELEMENT_AT(file, 'checksum'), file IS NULL
-            FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/", "recursive"="true")
-            ORDER BY path
-        """
-        qt_recursive_protocol """
-            SELECT ${requests.size()}, ${requests.every { it.method == "GET" 
&& it.query["prefix"] == "dir/"
-                && !it.query.containsKey("delimiter") }},
-                ${requests.any { 
it.query["continuation-token"]?.startsWith("empty:") }}
-        """
-        qt_root """
-            SELECT path, ELEMENT_AT(file, 'size') AS size
-            FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local")
-            ORDER BY path
-        """
-        qt_root_slash """
-            SELECT path, ELEMENT_AT(file, 'size') AS size
-            FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/")
-            ORDER BY path
-        """
-        qt_empty_directory """
-            SELECT * FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/missing/")
-            ORDER BY path
-        """
-        qt_count """
-            SELECT COUNT(*), COUNT(file), SUM(ELEMENT_AT(file, 'size'))
-            FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir", "recursive"="true")
-        """
-
-        def originalParallelSettings = sql("SELECT 
@@ignore_storage_data_distribution, @@parallel_pipeline_task_num")[0]
-        try {
-            sql "SET ignore_storage_data_distribution = true"
-            sql "SET parallel_pipeline_task_num = 2"
-            requests.clear()
-            qt_parallel_count """
-                SELECT COUNT(*), COUNT(DISTINCT path), SUM(ELEMENT_AT(file, 
'size'))
-                FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/", "recursive"="true")
-            """
-            qt_parallel_requests "SELECT ${requests.size()}"
-            requests.clear()
-            qt_parallel_count_repeat """
-                SELECT COUNT(*), COUNT(DISTINCT path), SUM(ELEMENT_AT(file, 
'size'))
-                FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/", "recursive"="true")
-            """
-            qt_parallel_requests_repeat "SELECT ${requests.size()}"
-            qt_parallel_paths """
-                SELECT path FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/", "recursive"="true")
-                ORDER BY path
-            """
-        } finally {
-            sql "SET ignore_storage_data_distribution = 
${originalParallelSettings[0]}"
-            sql "SET parallel_pipeline_task_num = 
${originalParallelSettings[1]}"
-        }
-
-        requests.clear()
-        // The first page contains one marker and one empty file. LIMIT must 
stop before fetching the next page.
-        qt_limit """
-            SELECT path, file
-            FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/", "recursive"="true")
-            LIMIT 1
-        """
-        qt_limit_requests "SELECT ${requests.size()}"
-        requests.clear()
-        qt_limit_zero """
-            SELECT * FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/")
-            LIMIT 0
-        """
-        qt_limit_zero_requests "SELECT ${requests.size()}"
-
-        sql "DROP TABLE IF EXISTS test_list_file_resource_values"
-        test {
-            sql """
-                CREATE TABLE test_list_file_resource_values (
-                    path VARCHAR(65533) NOT NULL, f FILE NOT NULL)
-                DUPLICATE KEY(path) DISTRIBUTED BY HASH(path) BUCKETS 1
-                PROPERTIES("replication_num"="1")
-            """
-            exception "FILE columns are not supported in internal tables"
-        }
-
-        requests.clear()
-        qt_canonical_list """
-            SELECT path, file
-            FROM list_file("resource"="list_file_local_canonical_s3", 
"uri"="s3://list-file-local/dir/")
-            ORDER BY path
-        """
-        qt_canonical_list_protocol "SELECT ${requests.every { it.method == 
'GET' }}"
-        requests.clear()
-        qt_canonical_to_file_head """
-            SELECT TO_FILE('list_file_local_canonical_s3', 
's3://list-file-local/dir/b.json')
-        """
-        qt_canonical_head_protocol """
-            SELECT ${requests.size()}, ${requests.every { it.method == "HEAD"
-                    && it.path == "/list-file-local/dir/b.json" && 
it.query.isEmpty() }}
-        """
-
-        sql "DROP TABLE IF EXISTS test_list_file_canonical_bucket_uris"
-        sql """
-            CREATE TABLE test_list_file_canonical_bucket_uris (id INT NOT 
NULL, uri STRING NOT NULL)
-            DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
-            PROPERTIES("replication_num"="1")
-        """
-        sql """
-            INSERT INTO test_list_file_canonical_bucket_uris VALUES
-            (1, 's3://list-file-local/dir/b.json'),
-            (2, 's3://list-file-other/dir/b.json'),
-            (3, 's3://list-file-local/dir/b.json')
-        """
-        requests.clear()
-        // One TO_FILE expression reads primary -> other -> primary in the 
same batch.
-        // Its returned FILE rows must retain the corresponding 7 -> 31 -> 7 
sizes.
-        qt_canonical_mixed_buckets """
-            SELECT id, TO_FILE('list_file_local_canonical_s3', uri)
-            FROM test_list_file_canonical_bucket_uris ORDER BY id
-        """
-        assertEquals(["/list-file-local/dir/b.json", 
"/list-file-other/dir/b.json",
-                      "/list-file-local/dir/b.json"], requests.collect { 
it.path },
-                "TO_FILE must HEAD each URI's bucket and safely return to the 
first bucket")
-        assertTrue(requests.every { it.method == "HEAD" && it.query.isEmpty() 
},
-                "Mixed-bucket TO_FILE must use only HEAD requests")
-
-        qt_list_uri_bucket """
-            SELECT path, ELEMENT_AT(file, 'size') FROM list_file(
-                "resource"="list_file_local_s3", 
"uri"="s3://other-bucket/dir/") ORDER BY path
-        """
-        requests.clear()
-        for (def invalid in [
-            [properties: '"uri"="s3://list-file-local/dir/"', message: 
"resource"],
-            [properties: '"resource"="list_file_local_s3"', message: "uri"],
-            [properties: '"resource"="missing_list_file_resource", 
"uri"="s3://list-file-local/dir/"', message: "Can not find resource"],
-            [properties: '"resource"="list_file_local_hdfs", 
"uri"="s3://list-file-local/dir/"', message: "S3"],
-            [properties: '"resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/", "unknown"="value"', message: "unknown"],
-            [properties: '"resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/", "recursive"="yes"', message: "recursive"],
-        ]) {
-            test {
-                sql "SELECT * FROM list_file(${invalid.properties})"
-                exception invalid.message
-            }
-        }
-        for (String uri : ["NULL", "'hdfs://127.0.0.1:1/file'"]) {
-            test {
-                sql "SELECT TO_FILE('list_file_local_hdfs', ${uri})"
-                exception "S3 resource"
-            }
-        }
-        qt_invalid_no_io "SELECT ${requests.size()}"
-
-        sql "DROP USER IF EXISTS 'list_file_local_user'"
-        sql "CREATE USER 'list_file_local_user' IDENTIFIED BY 'ListFile_123'"
-        sql "GRANT SELECT_PRIV ON ${context.dbName}.* TO 
'list_file_local_user'"
-        def userJdbcUrl = context.connection.metaData.URL
-        connect("list_file_local_user", "ListFile_123", userJdbcUrl) {
-            test {
-                sql 'SELECT * FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/dir/")'
-                exception "denied"
-            }
-        }
-        qt_denied_no_io "SELECT ${requests.size()}"
-        sql "GRANT USAGE_PRIV ON RESOURCE list_file_local_s3 TO 
'list_file_local_user'"
-        connect("list_file_local_user", "ListFile_123", userJdbcUrl) {
-            qt_granted_count """
-                SELECT COUNT(*) FROM 
list_file("resource"="list_file_local_s3", "uri"="s3://list-file-local/dir/")
-            """
-        }
-        // Distinct raw keys have distinct sizes; neither LIST_FILE nor 
TO_FILE decodes them.
-        def rawObjects = [
-            "roundtrip/a b.txt": 101, "roundtrip/a%20b.txt": 102,
-            "roundtrip/a+b.txt": 103, "roundtrip/中文.txt": 104,
-            "roundtrip/a%2Fb.txt": 105, "roundtrip/a/b.txt": 106
-        ]
-        rawObjects.each { key, size ->
-            objects[key] = size
-            heads["/list-file-local/${key}".toString()] = [size: size, etag: 
'"roundtrip-' + size + '"']
-        }
-        requests.clear()
-        qt_raw_uri_roundtrip """
-            SELECT path, size, ELEMENT_AT(f, 'uri'), ELEMENT_AT(f, 'size')
-            FROM (
-                SELECT path, ELEMENT_AT(file, 'size') AS size, 
TO_FILE('list_file_local_s3', path) AS f
-                FROM list_file("resource"="list_file_local_s3",
-                               "uri"="s3://list-file-local/roundtrip/", 
"recursive"="true")
-            ) t ORDER BY path
-        """
-        def headRequests = requests.findAll { it.method == "HEAD" }
-        def expectedPaths = rawObjects.keySet().collect { 
"/list-file-local/${it}".toString() }.sort()
-        qt_raw_uri_request_protocol """
-            SELECT ${headRequests.size()},
-                   ${headRequests.collect { it.decodedPath }.sort() == 
expectedPaths},
-                   ${headRequests.every { it.query.isEmpty() }}
-        """
-        qt_literal_plus """
-            SELECT TO_FILE('list_file_local_s3', 
's3://list-file-local/roundtrip/a+b.txt')
-        """
-        // Feed identical provider bytes through LIST XML and HEAD headers for 
both FILE entry points.
-        def consistencyEtags = [
-            "etag-consistency/absent.txt": null,
-            "etag-consistency/empty.txt": "",
-            "etag-consistency/quoted-empty.txt": '""',
-            "etag-consistency/bare-multipart.txt": "MiXeD:opaque-2",
-            "etag-consistency/quoted-multipart.txt": '"MiXeD:opaque-2"'
-        ]
-        consistencyEtags.each { key, etag ->
-            objects[key] = 7
-            listingEtags[key] = etag
-            heads["/list-file-local/${key}".toString()] = [size: 7, etag: etag]
-        }
-        requests.clear()
-        qt_listing_head_checksum_consistency """
-            SELECT path, listing_checksum, head_checksum, listing_checksum <=> 
head_checksum
-            FROM (
-                SELECT path, ELEMENT_AT(file, 'checksum') AS listing_checksum,
-                       ELEMENT_AT(TO_FILE('list_file_local_s3', path), 
'checksum') AS head_checksum
-                FROM list_file("resource"="list_file_local_s3", 
"uri"="s3://list-file-local/etag-consistency/")
-            ) t ORDER BY path
-        """
-        def consistencyHeads = requests.findAll { it.method == "HEAD" }
-        def consistencyPaths = consistencyEtags.keySet().collect { 
"/list-file-local/${it}".toString() }.sort()
-        qt_listing_head_checksum_protocol """
-            SELECT ${consistencyHeads.size()},
-                   ${consistencyHeads.collect { it.decodedPath }.sort() == 
consistencyPaths},
-                   ${requests.every { it.method == "HEAD" ? it.query.isEmpty()
-                           : it.method == "GET" && it.query["list-type"] == "2"
-                             && it.query["prefix"] == "etag-consistency/" }}
-        """
-        qt_handler_errors "SELECT ${handlerErrors.size()}"
-    } finally {
-        server.stop(0)
-        sql "DROP USER IF EXISTS 'list_file_local_user'"
-        sql "DROP RESOURCE IF EXISTS 'list_file_local_hdfs'"
-        sql "DROP RESOURCE IF EXISTS 'list_file_local_s3'"
-        sql "DROP RESOURCE IF EXISTS 'list_file_local_canonical_s3'"
-    }
-}
diff --git 
a/regression-test/suites/datatype_p0/complex_types/test_list_files.groovy 
b/regression-test/suites/datatype_p0/complex_types/test_list_files.groovy
new file mode 100644
index 00000000000..fc963c96f6a
--- /dev/null
+++ b/regression-test/suites/datatype_p0/complex_types/test_list_files.groovy
@@ -0,0 +1,101 @@
+// 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.
+
+import com.amazonaws.auth.AWSStaticCredentialsProvider
+import com.amazonaws.auth.BasicAWSCredentials
+import com.amazonaws.client.builder.AwsClientBuilder
+import com.amazonaws.services.s3.AmazonS3ClientBuilder
+import com.amazonaws.services.s3.model.ObjectMetadata
+
+suite("test_list_files", "p0,external") {
+    def client = AmazonS3ClientBuilder.standard()
+            .withEndpointConfiguration(new 
AwsClientBuilder.EndpointConfiguration(getS3Endpoint(), getS3Region()))
+            .withCredentials(new AWSStaticCredentialsProvider(new 
BasicAWSCredentials(getS3AK(), getS3SK())))
+            .withPathStyleAccessEnabled(true).build()
+    String bucket = getS3BucketName()
+    String prefix = "list-files/${UUID.randomUUID()}/"
+    String root = "s3://${bucket}/${prefix}"
+    def keys = []
+    def put = { String key, String contents ->
+        byte[] data = contents.getBytes("UTF-8")
+        def metadata = new ObjectMetadata()
+        metadata.setContentLength(data.length)
+        client.putObject(bucket, prefix + key, new ByteArrayInputStream(data), 
metadata)
+        keys.add(prefix + key)
+    }
+    try {
+        ["": "", "empty.txt": "", "a.json": "{}", "sub/": "", "sub/c.png": 
"abc",
+         "sub/deep/d.txt": "abcd", "empty/": "", "empty/deep/": "", "marker/": 
"marker"]
+                .each { key, contents -> put(key, contents) }
+        sql "DROP RESOURCE IF EXISTS list_files_s3"
+        sql """CREATE RESOURCE list_files_s3 PROPERTIES(
+            "type"="s3", "s3.endpoint"="${getS3Endpoint()}", 
"s3.region"="${getS3Region()}",
+            "s3.access_key"="${getS3AK()}", "s3.secret_key"="${getS3SK()}",
+            "s3.bucket"="${bucket}", "s3.root.path"="unused", 
"use_path_style"="true")"""
+        def listing = { String path, String recursive ->
+            """list_files("resource"="list_files_s3", 
"uri"="${path}"${recursive})"""
+        }
+        String direct = listing(root, ', "recursive"="false"')
+        String recursive = listing(root, ', "recursive"="true"')
+        String defaults = listing(root, '')
+        qt_schema "DESC FUNCTION ${direct}"
+        for (def entry : [direct: direct, recursive: recursive, defaults: 
defaults]) {
+            "qt_${entry.key}" """SELECT substring(path, ${root.length() + 1}), 
size,
+                file IS NULL, ELEMENT_AT(file, 'size'), ELEMENT_AT(file, 
'content_type')
+                FROM ${entry.value} ORDER BY path"""
+            "qt_${entry.key}_counts" "SELECT count(*), count(file), sum(size) 
FROM ${entry.value}"
+        }
+        qt_size_only "SELECT size FROM ${direct} ORDER BY path"
+        qt_path_only "SELECT substring(path, ${root.length() + 1}) FROM 
${direct} ORDER BY path"
+        qt_file_only "SELECT file IS NULL FROM ${direct} ORDER BY path"
+        qt_limit "SELECT count(*) FROM (SELECT * FROM ${direct} LIMIT 2) t"
+        qt_roundtrip """SELECT substring(path, ${root.length() + 1}),
+            ELEMENT_AT(TO_FILE('list_files_s3', path), 'size') = size,
+            ELEMENT_AT(file, 'checksum') = ELEMENT_AT(TO_FILE('list_files_s3', 
path), 'checksum')
+            FROM ${recursive} ORDER BY path"""
+        qt_no_trailing_slash """SELECT count(*), count(file), sum(size)
+            FROM ${listing(root.substring(0, root.length() - 1), ', 
"recursive"="false"')}"""
+        qt_empty_direct """SELECT substring(path, ${root.length() + 1}), size, 
file IS NULL
+            FROM ${listing(root + 'empty/', ', "recursive"="false"')} ORDER BY 
path"""
+        qt_empty_recursive "SELECT count(*) FROM ${listing(root + 'empty/', 
'')}"
+        qt_missing "SELECT count(*) FROM ${listing(root + 'missing/', '')}"
+        for (int i = 0; i < 1005; ++i) {
+            put(String.format("pages/d%04d/", i), "")
+        }
+        qt_directory_pages """SELECT count(*), count(file), sum(size),
+            count(distinct path) FROM ${listing(root + 'pages/', ', 
"recursive"="false"')}"""
+        qt_directory_pages_recursive "SELECT count(*) FROM ${listing(root + 
'pages/', '')}"
+        for (int i = 0; i < 1005; ++i) {
+            put(String.format("files/f%04d.txt", i), "x")
+        }
+        qt_file_pages """SELECT count(*), count(file), sum(size), 
count(distinct path)
+            FROM ${listing(root + 'files/', '')}"""
+        qt_file_pages_direct """SELECT count(*), count(file), sum(size)
+            FROM ${listing(root + 'files/', ', "recursive"="false"')}"""
+        test {
+            sql """SELECT * FROM list_file("resource"="list_files_s3", 
"uri"="${root}")"""
+            exception "Can not found function"
+        }
+        test {
+            sql """SELECT * FROM list_files("resource"="list_files_s3", 
"uri"="${root}", "recursive"="yes")"""
+            exception "recursive must be true or false"
+        }
+    } finally {
+        keys.each { key -> client.deleteObject(bucket, key) }
+        client.shutdown()
+    }
+}


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

Reply via email to