sunchao commented on code in PR #5314:
URL: https://github.com/apache/datafusion-comet/pull/5314#discussion_r3876130764


##########
spark/src/main/scala/org/apache/comet/objectstore/NativeConfig.scala:
##########
@@ -42,6 +44,55 @@ object NativeConfig {
     "abfs" -> Seq("fs.azure.", "fs.abfs."),
     "abfss" -> Seq("fs.azure.", "fs.abfss.", "fs.abfs."))
 
+  private val blobKeyPattern = "^fs\\.blob\\.([^.]+)\\.(.+)$".r
+
+  // Some blob:// filesystem implementations fall back to the literal string 
"default" as the
+  // authority when the URI has none. For `blob:///bucket/key`, the filesystem 
therefore looks
+  // up `fs.blob.default.*`, while the actual S3 bucket comes from the URL 
path. Translate
+  // `fs.blob.default.*` to the GLOBAL `fs.s3a.*` key (not the per-bucket
+  // `fs.s3a.bucket.default.*`) so the credentials/endpoint apply to whichever 
bucket the URL
+  // path resolves to, matching those implementations' semantics.
+  private val blobDefaultAuthority = "default"
+
+  /**
+   * Translates vendor-style `fs.blob.<authority>.<property>` keys into the 
`fs.s3a.*` shape that
+   * object_store's AmazonS3Builder reads. Some blob:// connectors use 
per-authority keys and
+   * never set a region -- an endpoint alone is enough for the AWS SDK v1 
client they build -- and
+   * their endpoints are typically path-style against non-AWS services, so an 
`endpoint` key also
+   * enables `path.style.access` on the same scope.
+   *
+   * `fs.blob.<authority>.*` is the authoritative source for `blob://` URLs, 
so callers should
+   * apply these translations AFTER a plain `fs.s3a.*` pass so blob-supplied 
values override any
+   * unrelated `fs.s3a.*` the user set for a different workload (see 403 
misdirect in the class
+   * docstring).
+   */
+  private def translateBlobKeys(hadoopConf: Configuration): Map[String, 
String] = {
+    import scala.jdk.CollectionConverters._
+    val out = scala.collection.mutable.Map[String, String]()
+    hadoopConf.iterator().asScala.foreach { entry =>
+      entry.getKey match {
+        case blobKeyPattern(authority, property) =>
+          val s3aSuffix = property match {
+            case "endpoint" => Some("endpoint")
+            case "awsAccessKeyId" => Some("access.key")
+            case "awsSecretAccessKey" => Some("secret.key")
+            case _ => None
+          }
+          s3aSuffix.foreach { suffix =>
+            val scope =
+              if (authority == blobDefaultAuthority) "fs.s3a"
+              else s"fs.s3a.bucket.$authority"

Review Comment:
   **[P2] Resolve authority-specific blob settings before constructing Iceberg 
FileIO**
   
   For `blob://mybucket/...` configured through `fs.blob.mybucket.*`, these 
bucket-scoped keys become `s3.bucket.mybucket.*` in 
`hadoopToIcebergS3Properties`. The pinned Iceberg S3 parser only consumes 
global properties, so the endpoint, credentials and path style are ignored. A 
local OpenDAL request probe selected the unrelated global endpoint/identity; 
flattening the selected bucket properties fixed it. This parser limitation 
already existed for s3a, but the new blob support now depends on it. Resolve 
the relevant bucket settings to the properties FileIO actually consumes.



##########
spark/src/main/java/org/apache/comet/parquet/CometFileKeyUnwrapper.java:
##########
@@ -103,21 +103,27 @@ public class CometFileKeyUnwrapper {
 
   /**
    * Normalizes S3 URI schemes to a canonical form. S3 can be accessed via 
multiple schemes (s3://,
-   * s3a://, s3n://) that refer to the same logical filesystem. This method 
ensures consistent cache
-   * lookups regardless of which scheme is used.
+   * s3a://, s3n://, blob://) that refer to the same logical filesystem. This 
method ensures
+   * consistent cache lookups regardless of which scheme is used. The put and 
get sides must agree,
+   * because the JVM store side is called with the user-facing scheme (e.g. 
blob://) while the
+   * native side JNIs back with the scheme after 
`prepare_object_store_with_configs` has already
+   * rewritten aliases to s3://.
    *
    * @param filePath The file path that may contain an S3 URI
    * @return The file path with normalized S3 scheme (s3a://)
    */
   private String normalizeS3Scheme(final String filePath) {
-    // Normalize s3:// and s3n:// to s3a:// for consistent cache lookups
-    // This handles the case where ObjectStoreUrl uses s3:// but Spark uses 
s3a://
+    // Normalize s3://, s3n://, and blob:// to s3a:// for consistent cache 
lookups
+    // This handles the case where ObjectStoreUrl uses s3:// but Spark uses 
s3a:// or blob://
     String s3Prefix = "s3://";
     String s3nPrefix = "s3n://";
+    String blobPrefix = "blob://";
     if (filePath.startsWith(s3Prefix)) {
       return "s3a://" + filePath.substring(s3Prefix.length());
     } else if (filePath.startsWith(s3nPrefix)) {
       return "s3a://" + filePath.substring(s3nPrefix.length());
+    } else if (filePath.startsWith(blobPrefix)) {
+      return "s3a://" + filePath.substring(blobPrefix.length());

Review Comment:
   **[P2] Canonicalize single/triple-slash blob paths on both cache sides**
   
   When Spark's input paths retain `blob:/bucket/key` or `blob:///bucket/key`, 
native normalization promotes the bucket and calls back with `s3://bucket/key`. 
This method leaves the single-slash path unchanged and maps the triple-slash 
path to `s3a:///bucket/key`, so neither cache entry matches the callback's 
`s3a://bucket/key`. Production Java cache methods with Spark 4.1.3 path 
conversion reproduced `Failed to find DecryptionKeyRetriever` for both forms; 
double-slash blob and s3a controls passed. Apply the same authority 
normalization when storing and retrieving keys.



##########
spark/src/main/scala/org/apache/comet/objectstore/NativeConfig.scala:
##########
@@ -42,6 +44,55 @@ object NativeConfig {
     "abfs" -> Seq("fs.azure.", "fs.abfs."),
     "abfss" -> Seq("fs.azure.", "fs.abfss.", "fs.abfs."))
 
+  private val blobKeyPattern = "^fs\\.blob\\.([^.]+)\\.(.+)$".r

Review Comment:
   **[P2] Keep dots inside blob authorities when translating settings**
   
   For a valid dotted bucket such as `blob://my.bucket/data.parquet`, 
`fs.blob.my.bucket.endpoint` is parsed as authority `my` and property 
`bucket.endpoint`, which is discarded. The access-key and secret-key settings 
are lost the same way. Compiling and calling the actual `NativeConfig` returned 
an empty map for this case, while the `mybucket` control retained all settings. 
Match the supported property suffixes and preserve the full authority, 
otherwise native reads use missing or unrelated defaults.



##########
native/core/src/execution/planner.rs:
##########
@@ -4266,7 +4281,8 @@ fn parse_file_scan_tasks_from_common(
 
             Ok(iceberg::scan::FileScanTask {
                 file_size_in_bytes: proto_task.file_size_in_bytes,
-                data_file_path: proto_task.data_file_path.clone(),
+                // Normalize blob/s3a aliases so iceberg-rust routes through 
S3, not LocalFs.
+                data_file_path: 
normalize_object_store_url_string(&proto_task.data_file_path)?,

Review Comment:
   **[P1] Preserve blob file identity for positional deletes**
   
   Position-delete records retain the original `blob:` data-file path, but this 
changes the task's identity to `s3:`. iceberg-rust then looks up a different 
delete-vector key and silently returns deleted rows. With real Parquet and the 
pinned ArrowReader over memory FileIO, `[10,20,30]` with position 1 deleted 
returned `[10,30]` before normalization and `[10,20,30]` afterward for all 
three blob slash forms; s3a/file controls passed. Preserve the original task 
identity and normalize only storage access. This finding concerns positional 
deletes, not equality deletes.



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -1054,6 +1054,26 @@ object CometScanRule extends Logging {
   val SKIP_COMET_SCAN_TAG: 
org.apache.spark.sql.catalyst.trees.TreeNodeTag[Unit] =
     
org.apache.spark.sql.catalyst.trees.TreeNodeTag[Unit]("comet.skipCometScan")
 
+  /**
+   * Schemes readable by iceberg-rust's OpenDAL storage factory that 
`ObjectStoreScheme::parse`
+   * does NOT recognize, so `isNativelyReadableScheme` alone under-admits for 
iceberg scans.
+   * Mirror of the extra `match` arms in
+   * 
`native/core/src/execution/operators/iceberg_scan.rs::storage_factory_for` -- 
add here what
+   * you add there. Currently Aliyun OSS via `OpenDalStorageFactory::Oss`.
+   */
+  private val icebergExtraSchemes: Set[String] = Set("oss")
+
+  /**
+   * Scheme gate for the Iceberg scan path. Accepts anything the Parquet scan 
gate accepts, plus
+   * schemes iceberg-rust reads via OpenDAL that object_store's parser doesn't 
recognize.
+   */
+  private[rules] def isIcebergReadableScheme(uri: URI): Boolean = {
+    if (isNativelyReadableScheme(uri)) return true

Review Comment:
   **[P2] Match Iceberg admission to its actual storage factory**
   
   `object_store` accepts http/https, but `IcebergScanExec.storage_factory_for` 
rejects those schemes. An S3FileIO table whose metadata/effective location and 
data locations use `https://bucket/...` therefore changes from JVM fallback to 
`Unsupported storage scheme: https`. Unmodified Iceberg Java S3FileIO and its 
Parquet reader read `[10,20,30]` through this alias against a local S3 
endpoint; the actual JNI gate accepted it and the exact native factory rejected 
it. Keep the gate aligned with Iceberg's backends. HTTP data paths alone are 
not the trigger when metadata remains s3.



##########
spark/src/main/scala/org/apache/comet/objectstore/NativeConfig.scala:
##########
@@ -42,6 +44,55 @@ object NativeConfig {
     "abfs" -> Seq("fs.azure.", "fs.abfs."),
     "abfss" -> Seq("fs.azure.", "fs.abfss.", "fs.abfs."))
 
+  private val blobKeyPattern = "^fs\\.blob\\.([^.]+)\\.(.+)$".r
+
+  // Some blob:// filesystem implementations fall back to the literal string 
"default" as the
+  // authority when the URI has none. For `blob:///bucket/key`, the filesystem 
therefore looks
+  // up `fs.blob.default.*`, while the actual S3 bucket comes from the URL 
path. Translate
+  // `fs.blob.default.*` to the GLOBAL `fs.s3a.*` key (not the per-bucket
+  // `fs.s3a.bucket.default.*`) so the credentials/endpoint apply to whichever 
bucket the URL
+  // path resolves to, matching those implementations' semantics.
+  private val blobDefaultAuthority = "default"
+
+  /**
+   * Translates vendor-style `fs.blob.<authority>.<property>` keys into the 
`fs.s3a.*` shape that
+   * object_store's AmazonS3Builder reads. Some blob:// connectors use 
per-authority keys and
+   * never set a region -- an endpoint alone is enough for the AWS SDK v1 
client they build -- and
+   * their endpoints are typically path-style against non-AWS services, so an 
`endpoint` key also
+   * enables `path.style.access` on the same scope.
+   *
+   * `fs.blob.<authority>.*` is the authoritative source for `blob://` URLs, 
so callers should
+   * apply these translations AFTER a plain `fs.s3a.*` pass so blob-supplied 
values override any
+   * unrelated `fs.s3a.*` the user set for a different workload (see 403 
misdirect in the class
+   * docstring).
+   */
+  private def translateBlobKeys(hadoopConf: Configuration): Map[String, 
String] = {
+    import scala.jdk.CollectionConverters._
+    val out = scala.collection.mutable.Map[String, String]()
+    hadoopConf.iterator().asScala.foreach { entry =>
+      entry.getKey match {
+        case blobKeyPattern(authority, property) =>
+          val s3aSuffix = property match {
+            case "endpoint" => Some("endpoint")
+            case "awsAccessKeyId" => Some("access.key")
+            case "awsSecretAccessKey" => Some("secret.key")
+            case _ => None
+          }
+          s3aSuffix.foreach { suffix =>
+            val scope =
+              if (authority == blobDefaultAuthority) "fs.s3a"

Review Comment:
   **[P2] Preserve default-authority precedence after bucket promotion**
   
   For `blob:///mybucket/...`, translating `fs.blob.default.*` only to global 
`fs.s3a.*` does not override existing `fs.s3a.bucket.mybucket.*`: native 
`get_config` checks the bucket scope first. An exact Scala/native configuration 
probe selected an unrelated S3 endpoint and identity despite the configured 
blob defaults. Translating a separate `fs.blob.mybucket.*` also overrides the 
default authority for this authorityless URI. Resolve settings using the 
original blob authority and apply them at the scope actually consulted after 
normalization.



##########
native/core/src/execution/operators/iceberg_scan.rs:
##########
@@ -368,6 +368,22 @@ impl IcebergScanExec {
             }
         }
 
+        // Object-store's AmazonS3Builder defaults the SigV4 region to 
`us-east-1` when unset;
+        // iceberg-storage-opendal's S3 factory instead errors with `region is 
missing. Please
+        // find it by S3::detect_region() or set them in env.` Non-AWS 
S3-compliant storage
+        // services accept any region in the credential, so default to 
`us-east-1` when the
+        // catalog didn't ship one. Both key spellings iceberg-rust reads 
(`client.region`
+        // wins over `s3.region`).
+        let region_forwarded = catalog_properties.contains_key("s3.region")
+            || catalog_properties.contains_key("client.region");
+        let is_s3_family = matches!(
+            metadata_location.split_once("://"),
+            Some(("s3" | "s3a" | "blob", _))
+        );
+        if !region_forwarded && is_s3_family {
+            file_io_builder = file_io_builder.with_prop("s3.region", 
"us-east-1");

Review Comment:
   **[P2] Honor the environment region before supplying a default**
   
   When catalog properties omit a region, OpenDAL previously honored 
`AWS_REGION`/`AWS_DEFAULT_REGION`. This unconditional property overrides that 
behavior for existing s3/s3a tables too. A local HTTP probe using the exact 
base/head `load_file_io` functions and dummy credentials signed for `us-west-2` 
before this change and `us-east-1` afterward with `AWS_REGION=us-west-2`. AWS 
buckets outside east-1 can therefore fail authentication. Honor the existing 
region sources before defaulting, or restrict the fallback to the intended 
non-AWS case.



##########
native/core/src/parquet/objectstore/blob_alias.rs:
##########
@@ -0,0 +1,235 @@
+// 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.
+
+//! S3-compatible filesystem alias handling.
+//!
+//! On the **Parquet** scan path Comet treats `blob://` and `s3a://` as 
aliases for `s3://`: they
+//! route through the same `object_store::AmazonS3` store, but `object_store`'s
+//! `ObjectStoreScheme::parse` recognizes neither `blob` nor the `s3a` 
dispatch Comet needs, so
+//! [`normalize_object_store_url`] rewrites both to canonical 
`s3://bucket/key` (and promotes the
+//! single-slash `blob:/bucket/key` / three-slash `blob:///bucket/key` forms 
to a real authority).
+//!
+//! On the **Iceberg** path the picture is narrower. iceberg-storage-opendal's 
S3 backend is
+//! scheme-agnostic (it reads only `url.host_str()` for the bucket and strips 
a `{scheme}://{bucket}/`
+//! prefix using the path's own scheme), so `s3a://` needs no rewrite there. 
More importantly,
+//! iceberg-rust matches positional/equality deletes by comparing a delete 
file's raw stored
+//! `file_path` against the `data_file_path` Comet supplies, so rewriting a 
scheme silently drops
+//! deletes. [`normalize_object_store_url_string`] therefore rewrites ONLY the 
s3-compliant alias
+//! schemes that genuinely require it ([`S3_COMPLIANT_ALIAS_SCHEMES`], 
currently just `blob`) and
+//! returns every other input -- `file://`, `s3://`, `s3a://`, schemeless bare 
paths -- byte-for-byte.
+//!
+//! The actual S3 store construction lives in [`super::s3`]; this module is 
only about URL shape
+//! so all the alias code has one home instead of drifting across 
`parquet_support.rs`,
+//! `execution/planner.rs`, and `execution/operators/iceberg_scan.rs`.
+
+use std::collections::HashMap;
+
+use url::Url;
+
+use crate::execution::operators::ExecutionError;
+use crate::parquet::parquet_support::is_hdfs_scheme;
+
+/// Parses `url_str` and rewrites `blob`/`s3a` schemes (and the awkward 
three-slash
+/// `blob:///bucket/key` form that `ObjectStoreScheme::parse` rejects because 
host=None) to the
+/// canonical `s3://bucket/key`. Non-alias schemes are returned unchanged.
+///
+/// `object_store_configs` is consulted only via `is_hdfs_scheme`: if the user 
routed `s3a`
+/// through libhdfs via `fs.comet.libhdfs.schemes`, we must NOT rewrite it to 
`s3` -- HDFS
+/// handling takes over.
+pub(crate) fn normalize_object_store_url(
+    url_str: &str,
+    object_store_configs: &HashMap<String, String>,
+) -> Result<Url, ExecutionError> {
+    let url = Url::parse(url_str)
+        .map_err(|e| ExecutionError::GeneralError(format!("Error parsing URL 
{url_str}: {e}")))?;
+    if is_hdfs_scheme(&url, object_store_configs) {
+        return Ok(url);
+    }
+    // The Parquet scan path also rewrites `s3a` -> `s3` (pre-existing 
behavior:
+    // `prepare_object_store_with_configs` dispatches on `scheme == "s3"`). It 
only consumes
+    // `url.scheme()` / `url.path()` from the returned Url, never the 
re-serialized string, so the
+    // `file:/x` -> `file:///x` round-trip that would break iceberg delete 
matching is harmless
+    // here. See `normalize_object_store_url_string` for why the Iceberg path 
is narrower.
+    let scheme = url.scheme();
+    if scheme != "s3a" && scheme != "blob" {
+        return Ok(url);
+    }
+    rewrite_alias_to_s3(url)
+}
+
+/// s3-compliant alias schemes Comet rewrites to canonical `s3://` on the 
Iceberg path. Currently
+/// only `blob`. Kept deliberately narrow: iceberg-rust matches 
positional/equality deletes by
+/// comparing the delete file's recorded `file_path` (raw, as Iceberg wrote 
it) against the
+/// `data_file_path` Comet supplies (`delete_filter.rs::get_delete_vector` 
looks up a map keyed by
+/// the delete file's `file_path` column in `caching_delete_file_loader.rs`). 
Rewriting a scheme
+/// the delete files were NOT written with desyncs those two and silently 
drops deletes, so only
+/// schemes that genuinely need an `s3://` shape to reach an object store 
belong here.
+const S3_COMPLIANT_ALIAS_SCHEMES: &[&str] = &["blob"];
+
+/// Rewrites an already-parsed alias URL (`blob`/`s3a`) to canonical 
`s3://bucket/key`, promoting
+/// the first path segment into the host for the single-slash 
`blob:/bucket/key` (Java opaque
+/// form) and three-slash `blob:///bucket/key` (empty authority) shapes that 
report host=None.
+fn rewrite_alias_to_s3(mut url: Url) -> Result<Url, ExecutionError> {
+    let original = url.scheme().to_string();
+    let needs_host_promotion = url.host_str().is_none();
+    url.set_scheme("s3").map_err(|_| {
+        ExecutionError::GeneralError(format!("Could not convert scheme from 
{original} to s3"))
+    })?;
+    if needs_host_promotion {
+        // Some deployments emit `blob:///bucket/key` (three slashes, empty 
authority) or Java
+        // collapses that to `blob:/bucket/key` (opaque form) in Iceberg 
manifests. In both,
+        // `url::Url` reports host=None and path=`/bucket/key`, but 
`ObjectStoreScheme::parse`
+        // requires a non-empty host. Lift the first path segment into the 
host.
+        let trimmed = url.path().trim_start_matches('/').to_string();
+        let (bucket, key) = match trimmed.split_once('/') {
+            Some((b, k)) => (b.to_string(), k.to_string()),
+            None => (trimmed, String::new()),
+        };
+        if bucket.is_empty() {
+            return Err(ExecutionError::GeneralError(format!(
+                "{original}:// URL is missing bucket name: {url}"
+            )));
+        }
+        url = Url::parse(&format!("s3://{bucket}/{key}")).map_err(|e| {
+            ExecutionError::GeneralError(format!("Could not normalize 
{original}:// URL: {e}"))
+        })?;
+    }
+    Ok(url)
+}
+
+/// String-returning normalizer for the Iceberg path, where paths are handed 
to iceberg-rust as
+/// owned strings on `FileScanTask` / `FileScanTaskDeleteFile`.
+///
+/// Unlike the Url-returning [`normalize_object_store_url`], this MUST return 
every non-`blob`
+/// input byte-for-byte. iceberg-rust keys its positional/equality delete maps 
by the delete
+/// file's recorded `file_path` column (raw, as Iceberg Java wrote it) and 
looks them up by the
+/// `data_file_path` Comet supplies here. Any rewrite Comet applies to 
`data_file_path` that the
+/// delete file's stored path did not get -- `s3a://` -> `s3://`, or `url`'s 
`file:/x` ->
+/// `file:///x` re-serialization -- desyncs the two and silently drops the 
deletes (observed as a
+/// merge-on-read table returning rows that should have been deleted, and 
rewrites failing to mark
+/// data files deleted).
+///
+/// So we only rewrite the s3-compliant alias schemes that genuinely need an 
`s3://` shape to
+/// reach an object store ([`S3_COMPLIANT_ALIAS_SCHEMES`], currently just 
`blob`). `file://`,
+/// `s3://`, `s3a://`, and schemeless bare paths (a local Hadoop-catalog 
warehouse) all pass
+/// through unchanged; iceberg-storage-opendal's `storage_factory_for` routes 
a `blob:/...` form
+/// correctly only after this rewrite and everything else via its own scheme 
detection.
+pub(crate) fn normalize_object_store_url_string(path: &str) -> Result<String, 
ExecutionError> {
+    let Ok(url) = Url::parse(path) else {
+        // Not a URL: a schemeless bare path from a local Hadoop-catalog 
warehouse (e.g.
+        // `/tmp/warehouse/db/t/metadata/v1.metadata.json` from 
`metadataFileLocation()`). Hand it
+        // to iceberg-storage-opendal's LocalFs backend unchanged, as the 
raw-passthrough these
+        // call sites used before blob normalization did.
+        return Ok(path.to_string());
+    };
+    if !S3_COMPLIANT_ALIAS_SCHEMES.contains(&url.scheme()) {
+        // file://, s3://, s3a://, ... -> return the ORIGINAL string 
byte-for-byte. Re-serializing
+        // through `url` (e.g. `String::from(url)`) rewrites the single-slash 
`file:/x` to the
+        // three-slash `file:///x`, which no longer matches the delete file's 
stored path.
+        return Ok(path.to_string());
+    }
+    // A recognized s3-compliant alias (blob) -> canonical `s3://bucket/key`, 
promoting the bucket
+    // out of the path for the single/three-slash forms. `String::from(Url)` 
moves the Url's
+    // serialization buffer out rather than re-serializing through `Display`.
+    Ok(String::from(rewrite_alias_to_s3(url)?))

Review Comment:
   **[P2] Preserve raw Iceberg object keys during alias normalization**
   
   A valid raw location such as `blob://bucket/warehouse/café/part.parquet` 
becomes `s3://bucket/warehouse/caf%C3%A9/part.parquet` here. Iceberg's OpenDAL 
adapter strips the prefix without decoding the key, and the S3 backend encodes 
it again. A local HTTP capture showed `caf%25C3%25A9` in the request instead of 
`caf%C3%A9`, so the scan targets a different object even without deletes. 
Preserve the raw key while changing the scheme/authority; the unchanged s3a 
path passed the same probe.



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -1095,12 +1112,14 @@ object CometScanRule extends Logging {
         allParquet = false
       }
 
-      // Filesystem scheme check for data file
+      // Filesystem scheme check for data file. Delegated to the native gate
+      // (`isNativelyReadableScheme` -> 
`NativeBase.isObjectStoreSchemeSupported`) so the iceberg
+      // and Parquet-scan paths share a single source of truth.
       try {
         val filePath = pathMethod.invoke(dataFile).toString
         val uri = new URI(filePath)
         val scheme = uri.getScheme
-        if (scheme != null && !supportedSchemes.contains(scheme)) {
+        if (scheme != null && !isIcebergReadableScheme(uri)) {

Review Comment:
   **[P2] Avoid caching raw Iceberg path failures as unsupported schemes**
   
   Iceberg formats a newline partition value as the literal directory `p=%0A`, 
which its native reader can read. Passing that raw path to `object_store` 
decodes `%0A` into a control character and returns false. If this is the first 
`file` check, `schemeSupportCache` stores false for the whole scheme, forcing 
subsequent ordinary Iceberg and Parquet scans to fall back until JVM restart. 
The actual reader returned `[10,20,30]` from this path; JNI accepted an 
ordinary file URL, but the Scala gate rejected it after the first lookup. Use a 
scheme-only probe rather than caching individual file-parse failures.



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