924060929 commented on code in PR #68998:
URL: https://github.com/apache/doris/pull/68998#discussion_r4256354409
##########
fe/fe-connector/fe-connector-hudi/src/main/java/org/apache/doris/connector/hudi/HudiConnectorMetadata.java:
##########
@@ -922,6 +928,52 @@ private List<ConnectorPartitionInfo>
restrictToPin(HudiTableHandle handle, List<
return atPin;
}
+ private List<String> hmsPartitionPaths(HudiTableHandle handle,
List<String> names) {
+ List<String> encodedNames = names.stream().filter(name ->
name.indexOf('%') >= 0)
+ .collect(Collectors.toList());
+ if (encodedNames.isEmpty()) {
+ return names;
+ }
+ // Hive sync can register an already escaped Hudi value, which HMS
escapes again in its name.
+ // The storage location identifies the physical path regardless of the
sync extractor's decoding
+ // policy. Decode that path only in parsePartitionValues, preserving
literal percent sequences.
+ // Name and object caches expire independently. A deleted encoded
partition must not fail
+ // a query before pruning; surviving objects must be associated by
values, not list position.
+ List<HmsPartitionInfo> partitions = hmsClient.getExistingPartitions(
+ handle.getDbName(), handle.getTableName(), encodedNames);
+ Map<List<String>, String> namesByValues = new HashMap<>();
+ for (String name : encodedNames) {
+ List<String> values = Arrays.stream(name.split("/"))
+ .map(part ->
HudiScanPlanProvider.unescapePathName(part.substring(part.indexOf('=') + 1)))
+ .collect(Collectors.toList());
+ namesByValues.put(values, name);
+ }
+ Map<String, String> paths = new HashMap<>();
+ for (HmsPartitionInfo partition : partitions) {
+ String name = namesByValues.get(partition.getValues());
+ paths.put(name, relativePartitionPath(handle.getBasePath(),
partition.getLocation()));
+ }
+ return names.stream().filter(name -> name.indexOf('%') < 0 ||
paths.containsKey(name))
+ .map(name -> paths.getOrDefault(name,
name)).collect(Collectors.toList());
+ }
+
+ private static String relativePartitionPath(String basePath, String
location) {
+ if (location == null || location.isEmpty()) {
+ throw new DorisConnectorException("Hudi HMS partition has no
storage location");
+ }
+ // Path quotes literal percent signs when creating its URI; getPath
retains the Hudi path's
+ // own escapes. URI.create(location).getPath() would incorrectly
consume that escaping layer.
+ URI base = new Path(basePath).toUri();
+ URI partition = new Path(location).toUri();
+ String prefix = base.getPath().endsWith("/") ? base.getPath() :
base.getPath() + "/";
+ if (!Objects.equals(base.getScheme(), partition.getScheme())
Review Comment:
[P2] Independently confirmed at `2cfee92005dbc607c8ac31897868be045670ef6f`.
`getTableHandle` takes the base path from HMS, and both partition listing and
`applyFilter` resolve encoded partition locations before pruning. A table at
`s3a://b/t` with a live partition at `s3://b/t/dt=a%3Ab` therefore fails this
check, including queries targeting another plain partition. This can arise from
normal Hive sync: table-location comparison ignores the scheme, while new
partitions use the current sync base path. Doris also maps `fs.s3.impl` and
`fs.s3a.impl` to `S3AFileSystem`. Please compare supported equivalent storage
schemes here, retaining authority/bucket and directory containment checks, and
add mixed-scheme listing and pruning tests in both directions.
##########
fe/fe-filesystem/fe-filesystem-spi/src/main/java/org/apache/doris/filesystem/spi/GlobPattern.java:
##########
@@ -0,0 +1,151 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.filesystem.spi;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Numeric-range normalization shared by object storage and Hadoop glob
listing. */
+public final class GlobPattern {
+ private static final int MAX_EXPANSIONS = 10000;
+ private static final Pattern RANGE =
Pattern.compile("(-?\\d+)\\.\\.(-?\\d+)");
+ private static final Pattern SIMPLE_RANGE =
Pattern.compile("\\d+\\.\\.\\d+");
+
+ private GlobPattern() {
+ }
+
+ /**
+ * Expands {@code {N..M}} numeric range syntax in a glob pattern to the
equivalent
+ * comma-separated alternation {@code {N,N+1,...,M}} understood by Hadoop
and object-storage globs.
+ * Supports simple non-negative ranges like {@code {1..3}}, reverse ranges
like {@code {3..1}},
+ * and mixed comma-separated patterns like {@code {1..2,3,1..3}} or {@code
{Refrain,1..3}}.
+ * For simple (no-comma) brace groups, only non-negative ranges are
expanded;
+ * negative-start ranges like {@code {-1..1}} are left unchanged.
+ * Duplicate values are removed. For example:
+ * <ul>
+ * <li>{@code data_{1..3}.csv} → {@code data_{1,2,3}.csv}</li>
+ * <li>{@code data_{1..2,3,1..3}.csv} → {@code data_{1,2,3}.csv}</li>
+ * <li>{@code data_{Refrain,1..3}.csv} → {@code
data_{Refrain,1,2,3}.csv}</li>
+ * <li>{@code data_{-1..1}.csv} → unchanged (no expansion)</li>
+ * </ul>
+ */
+ public static String expandNumericRanges(String pattern) {
+ StringBuilder result = new StringBuilder();
+ Deque<BraceGroup> groups = new ArrayDeque<>();
+ ExpansionBudget budget = new ExpansionBudget();
+ boolean characterClass = false;
+ for (int i = 0; i < pattern.length(); i++) {
+ char c = pattern.charAt(i);
+ StringBuilder output = groups.isEmpty() ? result :
groups.peek().segment;
+ // Escaped braces and commas are literal characters, even inside
an alternative.
+ // Character classes likewise must not change the surrounding
brace structure.
+ if (c == '\\') {
+ output.append(c);
+ if (i + 1 < pattern.length()) {
+ output.append(pattern.charAt(++i));
+ }
+ } else if (characterClass) {
+ output.append(c);
+ if (c == ']') {
+ characterClass = false;
+ }
+ } else if (c == '[') {
+ characterClass = true;
+ output.append(c);
+ } else if (c == '{') {
+ groups.push(new BraceGroup());
+ } else if (c == ',' && !groups.isEmpty()) {
+ groups.peek().finishSegment();
+ } else if (c == '}' && !groups.isEmpty()) {
+ BraceGroup group = groups.pop();
+ group.finishSegment();
+ String expanded = group.expand(budget);
+ (groups.isEmpty() ? result :
groups.peek().segment).append('{').append(expanded).append('}');
+ } else {
+ output.append(c);
+ }
+ }
+ while (!groups.isEmpty()) {
+ BraceGroup group = groups.pop();
+ group.finishSegment();
+ (groups.isEmpty() ? result : groups.peek().segment)
+ .append('{').append(String.join(",", group.segments));
+ }
+ return result.toString();
+ }
+
+ private static final class ExpansionBudget {
+ private long combinations = 1;
+ private long expandedValues;
+ }
+
+ private static final class BraceGroup {
+ private final List<String> segments = new ArrayList<>();
+ private final StringBuilder segment = new StringBuilder();
+
+ private void finishSegment() {
+ segments.add(segment.toString());
+ segment.setLength(0);
+ }
+
+ private String expand(ExpansionBudget budget) {
+ if (segments.size() == 1 &&
!SIMPLE_RANGE.matcher(segments.get(0)).matches()) {
+ return segments.get(0);
+ }
+ Set<String> values = new LinkedHashSet<>();
+ boolean expanded = false;
+ for (String alternative : segments) {
+ Matcher range = RANGE.matcher(alternative.trim());
+ if (range.matches()) {
+ // Count before materializing, and use long arithmetic so
an int endpoint
+ // cannot wrap the loop or turn a huge requested range
into a small count.
+ long from = Integer.parseInt(range.group(1));
+ long to = Integer.parseInt(range.group(2));
+ budget.expandedValues += Math.abs(to - from) + 1;
+ checkLimit(budget.expandedValues);
+ long step = from <= to ? 1 : -1;
+ for (long value = from; step > 0 ? value <= to : value >=
to; value += step) {
+ values.add(Long.toString(value));
+ }
+ expanded = true;
+ } else {
+ values.add(alternative.trim());
+ }
+ }
+ if (expanded) {
+ budget.combinations *= values.size();
Review Comment:
[P2] Independently confirmed at `2cfee92005dbc607c8ac31897868be045670ef6f`.
For `data_{a{1..101},b{1..101}}.csv`, the two inner groups share this budget:
closing the first sets `combinations=101`, and closing the second sets it to
`10201`, although the branches are mutually exclusive and produce only 202
candidates (`expandedValues` is also 202). Both DFS discovery entry points fail
before Hadoop sees the pattern. Please track candidate counts per expression,
summing alternative branches and multiplying independent concatenated groups,
while retaining the global allocation limit. Add this 202-candidate success
case and keep the `{1..101}/{1..101}` Cartesian-product rejection test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]