github-actions[bot] commented on code in PR #64768:
URL: https://github.com/apache/doris/pull/64768#discussion_r4033173480


##########
fe/fe-foundation/src/main/java/org/apache/doris/foundation/util/PathUtils.java:
##########
@@ -85,8 +168,11 @@ private static String normalize(String s) {
         if (s == null) {
             return "";
         }
-        // Remove trailing slashes for consistent comparison
-        String trimmed = s.replaceAll("/+$", "");
-        return trimmed.isEmpty() ? "" : trimmed;
+        // Skip the Matcher allocation when there is no trailing slash (the 
overwhelmingly common
+        // case for real object-storage paths), letting the JIT avoid the 
regex engine entirely.
+        if (s.isEmpty() || s.charAt(s.length() - 1) != '/') {
+            return s;

Review Comment:
   [P1] Preserve repeated trailing slashes in object-store prefixes
   
   This collapses `s3://bucket/data` and `s3://bucket/data//`, but BE does not: 
the writer appends `"/" + fileName`, `S3URI::parse` preserves the repeated 
slashes, and listing a target ending in `data//` uses `data//` as its prefix. A 
valid trigger is an insert planned against `.../data` while an external HMS 
update changes the table location to `.../data//` before `finishInsertTable` 
re-fetches it. The helper now returns true, so `prepareInsertExistingTable` 
completes the MPU at `data/<file>` instead of rejecting/relocating it, and the 
current table cannot list that file. Please only normalize the single optional 
directory delimiter (or compare prefixes with the BE join/list semantics) and 
cover this commit-time location-change case.



##########
fe/fe-foundation/src/main/java/org/apache/doris/foundation/util/PathUtils.java:
##########
@@ -43,40 +62,104 @@ public static boolean equalsIgnoreSchemeIfOneIsS3(String 
p1, String p2) {
             return p1 == null && p2 == null;
         }
 
+        // Fast path: identical raw strings are equal under every 
normalization rule below, so we
+        // can skip the two URI parses (and their Matcher/substring 
allocations) on the common
+        // unchanged-location case. This also makes the identity property hold 
for inputs that would
+        // otherwise fall back to exact string comparison (opaque, 
triple-slash, network-path).
+        if (p1.equals(p2)) {
+            return true;
+        }
+
         try {
             URI uri1 = new URI(p1);
             URI uri2 = new URI(p2);
 
-            String scheme1 = uri1.getScheme();
-            String scheme2 = uri2.getScheme();
+            // Opaque URIs (e.g. "s3:bucket/key" with no "//") have null 
authority and path, so they
+            // would all normalize to "" and compare equal regardless of 
content. Such URIs are
+            // malformed for object-storage purposes; fall back to exact 
string comparison.
+            if (uri1.isOpaque() || uri2.isOpaque()) {
+                return p1.equals(p2);
+            }
 
-            // If schemes are equal, compare the full URI strings ignoring case
-            if (scheme1 != null && scheme1.equalsIgnoreCase(scheme2)) {
-                return p1.equalsIgnoreCase(p2);
+            // Two classes of inputs are malformed for object-storage purposes 
and fall back to
+            // exact string comparison so they cannot spuriously match via the 
structural path:
+            //   1. A URI with a scheme but a null authority (e.g. the 
triple-slash form
+            //      "s3:///path" or "file:///path"): its absent authority 
would normalize to "" and
+            //      could spuriously match another null-authority URI across 
schemes.
+            //   2. A network-path reference: a URI with an authority but a 
null scheme
+            //      (e.g. "//bucket/path"). java.net.URI parses this as 
scheme=null,
+            //      authority="bucket". It is not a valid object-storage 
location, so without this
+            //      guard it would fall through to the structural comparison 
and spuriously match a
+            //      fully-qualified s3 URI ("s3://bucket/path").
+            // Schemeless bare paths (null scheme AND null authority, e.g. 
"/path") are intentionally
+            // left to the structural comparison below so identical bare paths 
still compare equal.
+            if ((uri1.getScheme() != null && uri1.getRawAuthority() == null)
+                    || (uri2.getScheme() != null && uri2.getRawAuthority() == 
null)
+                    || (uri1.getScheme() == null && uri1.getRawAuthority() != 
null)
+                    || (uri2.getScheme() == null && uri2.getRawAuthority() != 
null)) {
+                return p1.equals(p2);
             }
 
-            // If schemes differ but one is "s3", compare only authority and 
path ignoring scheme
-            if ("s3".equalsIgnoreCase(scheme1) || 
"s3".equalsIgnoreCase(scheme2)) {
-                String auth1 = normalize(uri1.getAuthority());
-                String auth2 = normalize(uri2.getAuthority());
-                String path1 = normalize(uri1.getPath());
-                String path2 = normalize(uri2.getPath());
+            // Distinguish the schemeless filesystem root "/" from the 
empty/current path "". With no
+            // scheme and no authority, both normalize() to "" (the 
trailing-slash strip collapses
+            // "/" to ""), which would make them spuriously equal even though 
they are distinct
+            // locations. This must NOT fire when an authority is present 
(e.g. "s3://bucket/" vs
+            // "s3://bucket"), where the trailing slash on the bucket root IS 
insignificant. The
+            // exact-string fallback returns false for "/" vs "" (the fast 
path above already handled
+            // "/" == "/" and "" == "").
+            if (uri1.getScheme() == null && uri1.getRawAuthority() == null) {
+                String rawPath1 = uri1.getRawPath();
+                String rawPath2 = uri2.getRawPath();
+                if (("/".equals(rawPath1) && "".equals(rawPath2))
+                        || ("".equals(rawPath1) && "/".equals(rawPath2))) {
+                    return p1.equals(p2);
+                }
+            }
+
+            String scheme1 = uri1.getScheme();
+            String scheme2 = uri2.getScheme();
+
+            // Null-safe, case-insensitive scheme equality (RFC 3986 section 
3.1). Two null schemes
+            // (e.g. bare paths) are treated as the same scheme so identical 
schemeless locations
+            // compare equal. The explicit scheme2 != null guard makes the 
null handling obvious
+            // without relying on String.equalsIgnoreCase(null)'s documented 
behavior.
+            boolean sameScheme = scheme1 == null
+                    ? scheme2 == null
+                    : (scheme2 != null && scheme1.equalsIgnoreCase(scheme2));
+            boolean oneIsS3 = "s3".equalsIgnoreCase(scheme1) || 
"s3".equalsIgnoreCase(scheme2);
 
-                return Objects.equals(auth1, auth2) && Objects.equals(path1, 
path2);
+            // Different schemes and neither is "s3": treat as different 
locations.
+            if (!sameScheme && !oneIsS3) {
+                return false;
             }
 
-            // Otherwise, URIs are not equal
-            return false;
+            // Same scheme, or cross-scheme where one side is "s3" (object 
stores are unified under
+            // the s3 scheme on the BE): the scheme is irrelevant -- compare 
only the authority
+            // (bucket/host) and the path. The raw (still percent-encoded) 
components are used so the
+            // comparison is byte-for-byte and case-sensitive (e.g. "a%2Fb" 
differs from "a/b"), since
+            // object-storage keys are case-sensitive. Both are normalized so 
a directory location
+            // compares equal with or without a trailing slash. Query strings 
and fragments are also
+            // compared so two locations that differ only there are not 
treated as identical.
+            // The authority is compared as-is (null -> ""); java.net.URI 
never places trailing
+            // slashes in the authority (the "/" delimiter always belongs to 
the path), so the
+            // trailing-slash normalize() is meaningful only for the path.
+            return Objects.equals(Objects.toString(uri1.getRawAuthority(), ""),
+                            Objects.toString(uri2.getRawAuthority(), ""))
+                    && Objects.equals(normalize(uri1.getRawPath()), 
normalize(uri2.getRawPath()))

Review Comment:
   [P2] Match query/fragment handling to the BE object identity
   
   These components are not part of the authority-plus-path contract stated in 
the PR, and BE's `S3URI::parse` strips everything from `?` or `#` before 
constructing the object key. FE normalization currently accepts and preserves 
such locations. If an insert is planned against `oss://bucket/data?v=1` and the 
live HMS location is represented as `s3://bucket/data?v=2` at commit, both 
resolve to the same BE bucket/key, but this comparison returns false. 
`prepareInsertExistingTable` then takes rename and skips `objCommit`; the 
object is still a pending MPU, so the insert cannot materialize it. Please 
either reject query/fragment-bearing storage locations during normalization or 
exclude these components here, and make the production commit-decision test pin 
the chosen contract.



-- 
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]

Reply via email to