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

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


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 84fc354a861 branch-4.1: [perf](s3) push down expandable S3 glob 
prefixes (#65621)
84fc354a861 is described below

commit 84fc354a861c943e6c8c4654a77731a7c4c80264
Author: hui lai <[email protected]>
AuthorDate: Thu Jul 16 11:05:02 2026 +0800

    branch-4.1: [perf](s3) push down expandable S3 glob prefixes (#65621)
    
    pick https://github.com/apache/doris/pull/64684
    
    Backport the expandable S3 glob-prefix pushdown to branch-4.1's legacy
    S3 object-storage implementation.
    
    Validation:
    - `git diff --check`
    - `mvn -pl fe-core -DskipTests validate`
    
    Targeted unit tests require the release branch's generated Thrift
    classes and locally built FE SNAPSHOT modules, so CI `buildall` will run
    the complete validation.
---
 .../java/org/apache/doris/common/util/S3Util.java  | 268 +++++++++++++++++++++
 .../java/org/apache/doris/fs/obj/S3ObjStorage.java | 156 ++++++------
 .../org/apache/doris/common/util/S3UtilTest.java   |  31 ++-
 .../org/apache/doris/fs/obj/S3ObjStorageTest.java  |  53 ++++
 4 files changed, 431 insertions(+), 77 deletions(-)

diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/util/S3Util.java 
b/fe/fe-core/src/main/java/org/apache/doris/common/util/S3Util.java
index 3e4f4e7a62f..f63f0be3c7a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/util/S3Util.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/S3Util.java
@@ -54,15 +54,20 @@ import 
software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider
 import java.net.HttpURLConnection;
 import java.net.URI;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public class S3Util {
     private static final Logger LOG = LogManager.getLogger(Util.class);
+    private static final int MAX_EXPANDED_GLOB_LIST_PREFIXES = 256;
+    private static final Comparator<String> UTF8_BINARY_ORDER = 
S3Util::compareUtf8Binary;
 
     private static AwsCredentialsProvider 
getAwsCredencialsProvider(CloudCredential credential) {
         AwsCredentials awsCredential;
@@ -302,6 +307,269 @@ public class S3Util {
         return globPattern.substring(0, earliestSpecialCharIndex);
     }
 
+    /**
+     * Returns object-store list prefixes that are safe to push down for a 
glob pattern.
+     * Bounded brace alternatives and positive character classes are expanded 
before the first
+     * unbounded wildcard. Unsafe or oversized expansions fall back to the 
longest static prefix.
+     */
+    public static List<String> getGlobListPrefixes(String globPattern) {
+        List<String> prefixes = expandGlobListPrefixes(globPattern, true);
+        return prefixes == null ? 
Collections.singletonList(getLongestPrefix(globPattern)) : prefixes;
+    }
+
+    private static List<String> expandGlobListPrefixes(String globPattern, 
boolean allowPartialPrefix) {
+        List<String> prefixes = new ArrayList<>();
+        prefixes.add("");
+        int i = 0;
+        while (i < globPattern.length()) {
+            char c = globPattern.charAt(i);
+            if (c == '*' || c == '?') {
+                return allowPartialPrefix ? compactPrefixes(prefixes) : null;
+            }
+            if (c == '\\') {
+                if (i + 1 < globPattern.length()) {
+                    appendLiteral(prefixes, globPattern.charAt(i + 1));
+                    i += 2;
+                } else {
+                    appendLiteral(prefixes, c);
+                    i++;
+                }
+                continue;
+            }
+            if (c == '[') {
+                PrefixExpansion charClass = expandCharacterClass(globPattern, 
i);
+                if (charClass == null) {
+                    return allowPartialPrefix ? compactPrefixes(prefixes) : 
null;
+                }
+                prefixes = appendAlternatives(prefixes, charClass.values);
+                if (prefixes == null) {
+                    return null;
+                }
+                i = charClass.nextIndex;
+                continue;
+            }
+            if (c == '{') {
+                PrefixExpansion brace = expandBraceGroup(globPattern, i);
+                if (brace == null) {
+                    return allowPartialPrefix ? compactPrefixes(prefixes) : 
null;
+                }
+                prefixes = appendAlternatives(prefixes, brace.values);
+                if (prefixes == null) {
+                    return null;
+                }
+                i = brace.nextIndex;
+                continue;
+            }
+            appendLiteral(prefixes, c);
+            i++;
+        }
+        return compactPrefixes(prefixes);
+    }
+
+    private static void appendLiteral(List<String> prefixes, char c) {
+        for (int i = 0; i < prefixes.size(); i++) {
+            prefixes.set(i, prefixes.get(i) + c);
+        }
+    }
+
+    private static List<String> appendAlternatives(List<String> prefixes, 
List<String> alternatives) {
+        long expandedSize = (long) prefixes.size() * alternatives.size();
+        if (expandedSize > MAX_EXPANDED_GLOB_LIST_PREFIXES) {
+            return null;
+        }
+        List<String> expanded = new ArrayList<>((int) expandedSize);
+        for (String prefix : prefixes) {
+            for (String alternative : alternatives) {
+                expanded.add(prefix + alternative);
+            }
+        }
+        return expanded;
+    }
+
+    private static PrefixExpansion expandCharacterClass(String globPattern, 
int openIndex) {
+        int closeIndex = findClosingBracket(globPattern, openIndex);
+        if (closeIndex < 0 || closeIndex == openIndex + 1) {
+            return null;
+        }
+        int i = openIndex + 1;
+        char first = globPattern.charAt(i);
+        if (first == '!' || first == '^') {
+            return null;
+        }
+        if (containsSurrogate(globPattern, i, closeIndex)) {
+            return null;
+        }
+        List<String> values = new ArrayList<>();
+        while (i < closeIndex) {
+            char current = globPattern.charAt(i);
+            if (current == '\\') {
+                if (i + 1 >= closeIndex) {
+                    return null;
+                }
+                values.add(String.valueOf(globPattern.charAt(i + 1)));
+                i += 2;
+                continue;
+            }
+            if (i + 2 < closeIndex && globPattern.charAt(i + 1) == '-') {
+                char rangeEnd = globPattern.charAt(i + 2);
+                int step = current <= rangeEnd ? 1 : -1;
+                for (char ch = current; step > 0 ? ch <= rangeEnd : ch >= 
rangeEnd; ch += step) {
+                    values.add(String.valueOf(ch));
+                    if (values.size() > MAX_EXPANDED_GLOB_LIST_PREFIXES) {
+                        return null;
+                    }
+                }
+                i += 3;
+                continue;
+            }
+            values.add(String.valueOf(current));
+            i++;
+        }
+        return new PrefixExpansion(values, closeIndex + 1);
+    }
+
+    private static int findClosingBracket(String globPattern, int openIndex) {
+        for (int i = openIndex + 1; i < globPattern.length(); i++) {
+            char c = globPattern.charAt(i);
+            if (c == '\\') {
+                i++;
+                continue;
+            }
+            if (c == ']') {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private static boolean containsSurrogate(String text, int start, int end) {
+        for (int i = start; i < end; i++) {
+            if (Character.isSurrogate(text.charAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static PrefixExpansion expandBraceGroup(String globPattern, int 
openIndex) {
+        int closeIndex = findClosingBrace(globPattern, openIndex);
+        if (closeIndex < 0) {
+            return null;
+        }
+        List<String> alternatives = splitBraceAlternatives(
+                globPattern.substring(openIndex + 1, closeIndex));
+        if (alternatives.isEmpty()) {
+            return null;
+        }
+        List<String> values = new ArrayList<>();
+        for (String alternative : alternatives) {
+            List<String> expandedAlternative = 
expandGlobListPrefixes(alternative, false);
+            if (expandedAlternative == null) {
+                return null;
+            }
+            values.addAll(expandedAlternative);
+            if (values.size() > MAX_EXPANDED_GLOB_LIST_PREFIXES) {
+                return null;
+            }
+        }
+        return new PrefixExpansion(values, closeIndex + 1);
+    }
+
+    private static int findClosingBrace(String globPattern, int openIndex) {
+        int depth = 0;
+        for (int i = openIndex; i < globPattern.length(); i++) {
+            char c = globPattern.charAt(i);
+            if (c == '\\') {
+                i++;
+                continue;
+            }
+            if (c == '{') {
+                depth++;
+            } else if (c == '}') {
+                depth--;
+                if (depth == 0) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    private static List<String> splitBraceAlternatives(String content) {
+        List<String> alternatives = new ArrayList<>();
+        int depth = 0;
+        int start = 0;
+        for (int i = 0; i < content.length(); i++) {
+            char c = content.charAt(i);
+            if (c == '\\') {
+                i++;
+                continue;
+            }
+            if (c == '{') {
+                depth++;
+            } else if (c == '}') {
+                if (depth == 0) {
+                    return Collections.emptyList();
+                }
+                depth--;
+            } else if (c == ',' && depth == 0) {
+                alternatives.add(content.substring(start, i));
+                start = i + 1;
+            }
+        }
+        if (depth != 0) {
+            return Collections.emptyList();
+        }
+        alternatives.add(content.substring(start));
+        return alternatives;
+    }
+
+    private static List<String> compactPrefixes(List<String> prefixes) {
+        List<String> sorted = new ArrayList<>(prefixes);
+        sorted.sort(UTF8_BINARY_ORDER);
+        List<String> compact = new ArrayList<>();
+        for (String prefix : sorted) {
+            if (prefix.isEmpty()) {
+                return Collections.singletonList("");
+            }
+            boolean covered = false;
+            for (String existing : compact) {
+                if (prefix.startsWith(existing)) {
+                    covered = true;
+                    break;
+                }
+            }
+            if (!covered) {
+                compact.add(prefix);
+            }
+        }
+        return compact;
+    }
+
+    private static int compareUtf8Binary(String left, String right) {
+        byte[] leftBytes = left.getBytes(StandardCharsets.UTF_8);
+        byte[] rightBytes = right.getBytes(StandardCharsets.UTF_8);
+        int commonLength = Math.min(leftBytes.length, rightBytes.length);
+        for (int i = 0; i < commonLength; i++) {
+            int result = Integer.compare(
+                    Byte.toUnsignedInt(leftBytes[i]), 
Byte.toUnsignedInt(rightBytes[i]));
+            if (result != 0) {
+                return result;
+            }
+        }
+        return Integer.compare(leftBytes.length, rightBytes.length);
+    }
+
+    private static class PrefixExpansion {
+        private final List<String> values;
+        private final int nextIndex;
+
+        private PrefixExpansion(List<String> values, int nextIndex) {
+            this.values = values;
+            this.nextIndex = nextIndex;
+        }
+    }
+
     // Apply some rules to extend the globs parsing behavior
     public static String extendGlobs(String pathPattern) {
         return extendGlobNumberRange(pathPattern);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java 
b/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java
index a92f5549285..5f14506c4de 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java
@@ -88,6 +88,7 @@ import java.nio.file.FileSystems;
 import java.nio.file.PathMatcher;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -683,9 +684,11 @@ public class S3ObjStorage implements ObjStorage<S3Client> {
                 LOG.debug("globList listPrefix: '{}' (from globPath: '{}')", 
listPrefix, globPath);
             }
 
-            // For Directory Buckets, ensure proper prefix handling using 
standardized approach
+            // For Directory Buckets, ensure proper prefix handling using 
standardized approach.
+            // Other S3-compatible stores can safely expand bounded glob 
fragments into narrower
+            // list prefixes, while still applying the complete glob matcher 
to returned keys.
             finalPrefix = listPrefix;
-
+            List<String> listPrefixes;
             if (!hasLimits && uri.useS3DirectoryBucket()) {
                 String adjustedPrefix = 
S3URI.getDirectoryPrefixForGlob(listPrefix);
                 if (LOG.isDebugEnabled() && 
!adjustedPrefix.equals(listPrefix)) {
@@ -693,92 +696,93 @@ public class S3ObjStorage implements ObjStorage<S3Client> 
{
                             listPrefix, adjustedPrefix);
                 }
                 finalPrefix = adjustedPrefix;
+                listPrefixes = Collections.singletonList(adjustedPrefix);
+            } else {
+                listPrefixes = S3Util.getGlobListPrefixes(globPath);
             }
 
-            Builder builder = ListObjectsV2Request.builder()
-                    .bucket(bucket)
-                    .prefix(finalPrefix);
-
-            if (startFile != null) {
-                builder.startAfter(startFile);
-            }
-
-            ListObjectsV2Request request = builder.build();
-
-            boolean isTruncated = false;
             boolean reachLimit = false;
             String lastMatchedKey = "";
-            do {
-                roundCnt++;
-                ListObjectsV2Response response = listObjectsV2(request);
-                for (S3Object obj : response.contents()) {
-                    elementCnt++;
-
-                    // Limit already reached: scan remaining objects in this 
page to find
-                    // the next glob-matching key, so hasMoreDataToConsume() 
returns true
-                    // correctly without recording a non-matching raw S3 key 
as currentMaxFile.
-                    if (reachLimit) {
-                        java.nio.file.Path checkPath = Paths.get(obj.key());
-                        if (matcher.matches(checkPath)) {
-                            currentMaxFile = obj.key();
-                            break;
-                        }
-                        continue;
-                    }
-
-                    java.nio.file.Path objPath = Paths.get(obj.key());
-
-                    boolean isPrefix = false;
-                    while (objPath != null && 
objPath.normalize().toString().startsWith(listPrefix)) {
-                        if (!matcher.matches(objPath)) {
-                            isPrefix = true;
-                            objPath = objPath.getParent();
+            listPrefixesLoop:
+            for (String currentListPrefix : listPrefixes) {
+                Builder builder = ListObjectsV2Request.builder()
+                        .bucket(bucket)
+                        .prefix(currentListPrefix);
+                if (startFile != null) {
+                    builder.startAfter(startFile);
+                }
+                ListObjectsV2Request request = builder.build();
+
+                boolean isTruncated;
+                do {
+                    roundCnt++;
+                    ListObjectsV2Response response = listObjectsV2(request);
+                    for (S3Object obj : response.contents()) {
+                        elementCnt++;
+
+                        // After reaching the page limit, find the next 
matching key across the
+                        // remaining page(s) and expanded prefixes so 
pagination remains correct.
+                        if (reachLimit) {
+                            java.nio.file.Path checkPath = 
Paths.get(obj.key());
+                            if (matcher.matches(checkPath)) {
+                                currentMaxFile = obj.key();
+                                break listPrefixesLoop;
+                            }
                             continue;
                         }
-                        if 
(directorySet.contains(objPath.normalize().toString())) {
-                            break;
-                        }
-                        if (isPrefix) {
-                            directorySet.add(objPath.normalize().toString());
-                        }
 
-                        matchCnt++;
-                        matchFileSize += obj.size();
-                        RemoteFile remoteFile = new RemoteFile(
-                                fileNameOnly ? 
objPath.getFileName().toString() :
-                                        "s3://" + bucket + "/" + 
objPath.toString(),
-                                !isPrefix,
-                                isPrefix ? -1 : obj.size(),
-                                isPrefix ? -1 : obj.size(),
-                                isPrefix ? 0 : 
obj.lastModified().toEpochMilli()
-                        );
-                        result.add(remoteFile);
-                        lastMatchedKey = obj.key();
+                        java.nio.file.Path objPath = Paths.get(obj.key());
+
+                        boolean isPrefix = false;
+                        while (objPath != null && 
objPath.normalize().toString().startsWith(listPrefix)) {
+                            if (!matcher.matches(objPath)) {
+                                isPrefix = true;
+                                objPath = objPath.getParent();
+                                continue;
+                            }
+                            if 
(directorySet.contains(objPath.normalize().toString())) {
+                                break;
+                            }
+                            if (isPrefix) {
+                                
directorySet.add(objPath.normalize().toString());
+                            }
+
+                            matchCnt++;
+                            matchFileSize += obj.size();
+                            RemoteFile remoteFile = new RemoteFile(
+                                    fileNameOnly ? 
objPath.getFileName().toString() :
+                                            "s3://" + bucket + "/" + 
objPath.toString(),
+                                    !isPrefix,
+                                    isPrefix ? -1 : obj.size(),
+                                    isPrefix ? -1 : obj.size(),
+                                    isPrefix ? 0 : 
obj.lastModified().toEpochMilli()
+                            );
+                            result.add(remoteFile);
+                            lastMatchedKey = obj.key();
+
+                            if (hasLimits && reachLimit(result.size(), 
matchFileSize,
+                                    fileSizeLimit, fileNumLimit)) {
+                                reachLimit = true;
+                                break;
+                            }
 
-                        if (hasLimits && reachLimit(result.size(), 
matchFileSize, fileSizeLimit, fileNumLimit)) {
-                            reachLimit = true;
-                            break;
+                            objPath = objPath.getParent();
+                            isPrefix = true;
                         }
-
-                        objPath = objPath.getParent();
-                        isPrefix = true;
                     }
-                }
 
-                // If no next matching file was found after the limit in the 
current page,
-                // fall back to lastMatchedKey to avoid a non-matching raw S3 
key
-                // (e.g. a sibling file like .lz4) being recorded as 
currentMaxFile.
-                if (currentMaxFile.isEmpty()) {
-                    currentMaxFile = lastMatchedKey;
-                }
+                    isTruncated = response.isTruncated();
+                    if (isTruncated) {
+                        request = request.toBuilder()
+                                
.continuationToken(response.nextContinuationToken())
+                                .build();
+                    }
+                } while (isTruncated && (!reachLimit || 
currentMaxFile.isEmpty()));
+            }
 
-                isTruncated = response.isTruncated();
-                if (isTruncated) {
-                    request = request.toBuilder()
-                            
.continuationToken(response.nextContinuationToken())
-                            .build();
-                }
-            } while (isTruncated && !reachLimit);
+            if (currentMaxFile.isEmpty()) {
+                currentMaxFile = lastMatchedKey;
+            }
 
             if (LOG.isDebugEnabled()) {
                 LOG.debug("remotePath:{}, result:{}", remotePath, result);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/util/S3UtilTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/common/util/S3UtilTest.java
index 4b976ed86cd..6e61893ed3b 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/common/util/S3UtilTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/common/util/S3UtilTest.java
@@ -456,5 +456,34 @@ public class S3UtilTest {
         // Malformed bracket (no closing ]) - [ kept as literal
         Assert.assertEquals("file[abc.csv", 
S3Util.expandBracketPatterns("file[abc.csv"));
     }
-}
 
+    @Test
+    public void testGetGlobListPrefixes_expandsBoundedPatterns() {
+        Assert.assertEquals(Arrays.asList(
+                        "data/date=2025-03-01/", "data/date=2025-04-01/",
+                        "data/date=2025-10-01/", "data/date=2025-11-01/",
+                        "data/date=2025-12-01/"),
+                
S3Util.getGlobListPrefixes("data/date=2025-{0[3-4],1[0-2]}-01/*"));
+    }
+
+    @Test
+    public void testGetGlobListPrefixes_fallsBackForWildcardBraceArm() {
+        Assert.assertEquals(Arrays.asList("data/"),
+                S3Util.getGlobListPrefixes("data/{foo*,bar*}/part.parquet"));
+    }
+
+    @Test
+    public void testGetGlobListPrefixes_fallsBackForSurrogateCharacterClass() {
+        String emoji = new String(Character.toChars(0x1F600));
+        Assert.assertEquals(Arrays.asList("data/"),
+                S3Util.getGlobListPrefixes("data/[" + emoji + 
"]/part.parquet"));
+    }
+
+    @Test
+    public void testGetGlobListPrefixes_usesUtf8BinaryOrder() {
+        String emoji = new String(Character.toChars(0x1F600));
+        String privateUse = new String(Character.toChars(0xE000));
+        Assert.assertEquals(Arrays.asList("data/" + privateUse + "/", "data/" 
+ emoji + "/"),
+                S3Util.getGlobListPrefixes("data/{" + emoji + "," + privateUse 
+ "}/*"));
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java
index d9ff61500fb..018113841ed 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java
@@ -22,6 +22,7 @@ import org.apache.doris.common.DdlException;
 import org.apache.doris.common.UserException;
 import 
org.apache.doris.datasource.property.storage.AbstractS3CompatibleProperties;
 import org.apache.doris.datasource.property.storage.StorageProperties;
+import org.apache.doris.fs.GlobListResult;
 import org.apache.doris.fs.remote.RemoteFile;
 
 import org.apache.commons.lang3.tuple.Triple;
@@ -64,6 +65,7 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -600,6 +602,57 @@ public class S3ObjStorageTest {
         Assertions.assertThrows(DdlException.class, () -> 
storage.getStsToken());
     }
 
+    @Test
+    public void testGlobListPushesDownExpandedPrefixes() {
+        List<String> requestedPrefixes = new ArrayList<>();
+        
Mockito.when(mockClient.listObjectsV2(Mockito.any(ListObjectsV2Request.class)))
+                .thenAnswer(invocation -> {
+                    ListObjectsV2Request request = invocation.getArgument(0);
+                    requestedPrefixes.add(request.prefix());
+                    return ListObjectsV2Response.builder()
+                            .contents(S3Object.builder()
+                                    .key(request.prefix() + "part.parquet")
+                                    .size(1L)
+                                    .lastModified(Instant.now())
+                                    .build())
+                            .isTruncated(false)
+                            .build();
+                });
+
+        List<RemoteFile> result = new ArrayList<>();
+        Status status = storage.globList(
+                "s3://bucket/data/date=2025-0[3-4]-01/*.parquet", result, 
false);
+
+        Assertions.assertEquals(Status.OK, status);
+        Assertions.assertEquals(Arrays.asList(
+                "data/date=2025-03-01/", "data/date=2025-04-01/"), 
requestedPrefixes);
+        Assertions.assertEquals(2, result.size());
+    }
+
+    @Test
+    public void testGlobListWithLimitFindsNextMatchAcrossExpandedPrefixes() {
+        
Mockito.when(mockClient.listObjectsV2(Mockito.any(ListObjectsV2Request.class)))
+                .thenAnswer(invocation -> {
+                    ListObjectsV2Request request = invocation.getArgument(0);
+                    return ListObjectsV2Response.builder()
+                            .contents(S3Object.builder()
+                                    .key(request.prefix() + "part.parquet")
+                                    .size(1L)
+                                    .lastModified(Instant.now())
+                                    .build())
+                            .isTruncated(false)
+                            .build();
+                });
+
+        List<RemoteFile> result = new ArrayList<>();
+        GlobListResult globResult = storage.globListWithLimit(
+                "s3://bucket/data/date=2025-0[3-4]-01/*.parquet", result, 
null, 0, 1);
+
+        Assertions.assertEquals(Status.OK, globResult.getStatus());
+        Assertions.assertEquals(1, result.size());
+        Assertions.assertEquals("data/date=2025-04-01/part.parquet", 
globResult.getMaxFile());
+    }
+
     private static class TestableRoleBasedS3ObjStorage extends S3ObjStorage {
         private final StsClient stsClient;
 


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

Reply via email to