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


##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -1018,30 +1060,48 @@ case class CometScanTypeChecker() extends 
DataTypeSupport with CometTypeShim {
 
 object CometScanRule extends Logging {
 
-  // Per-scheme memo of `NativeBase.isObjectStoreSchemeSupported`. The answer 
depends only on the
-  // URL scheme, so we cache by scheme and never re-cross the JNI boundary for 
a repeated scheme.
+  // Memo of `NativeBase.isObjectStoreSchemeSupported`, keyed by (scheme, 
has-authority) because
+  // object_store's parser keys on that pair (host-based stores need a host, 
file/memory need
+  // none). Caching by scheme alone lets an authorityless URL poison the whole 
scheme's answer.
   private val schemeSupportCache =
-    new ConcurrentHashMap[String, JBoolean]()
+    new ConcurrentHashMap[(String, Boolean), JBoolean]()
+
+  /**
+   * Opt-in S3-compliant alias schemes from `fs.comet.s3Compliant.schemes` 
(comma-separated,
+   * trimmed, lowercased; empty/missing => none). The native gate no longer 
claims these, so the
+   * opt-in is decided here where the Hadoop config is available.
+   */
+  private[rules] def resolveS3CompliantSchemes(hadoopConf: Configuration): 
Set[String] =
+    NativeConfig.parseSchemeSet(hadoopConf.get(COMET_S3_COMPLIANT_SCHEMES_KEY))
 
   /**
-   * True when Comet's native object_store layer recognizes this URI's scheme 
(so the scan is
-   * natively readable). Delegates to the native layer -- the source of truth 
-- instead of a
-   * hardcoded scheme list. On any failure to consult native (e.g. the library 
isn't loaded on
-   * this JVM, or predates this method) we assume the scheme IS supported: the 
scheme gate is an
-   * early-fallback optimization, and a build without a working native library 
can't run Comet's
-   * native scan anyway, so declining here would only over-restrict.
+   * True when Comet's native Parquet scan can read this URI's scheme: 
object_store recognizes it
+   * (asked of the native layer, the source of truth) OR it is an opt-in 
S3-compliant alias. If
+   * native can't be consulted (library not loaded), assume supported -- the 
gate is only an
+   * early-fallback optimization and such a build can't run the native scan 
anyway.
    */
-  private[rules] def isNativelyReadableScheme(uri: URI): Boolean = {
+  private[rules] def isNativelyReadableScheme(
+      uri: URI,
+      s3CompliantSchemes: Set[String]): Boolean = {
     val scheme = uri.getScheme
     if (scheme == null) return true
+    val lower = scheme.toLowerCase(Locale.ROOT)
+    if (s3CompliantSchemes.contains(lower)) return true

Review Comment:
   **[P1] Preserve bucket identity when admitting multi-bucket alias scans**
   
   With `fs.comet.s3Compliant.schemes=blob`, this now admits a Parquet read 
spanning two buckets, but native planning still selects the first file's object 
store for the entire `FilePartition` and strips the authority from every file's 
object key.
   
   I reproduced this against MinIO with two distinct, equal-size Parquet files 
at the same key, asserted one native scan and one partition containing both 
input URLs:
   
   ```text
   blob://comet-review-first/review-multibucket/same-key.parquet  -> 111
   blob://comet-review-second/review-multibucket/same-key.parquet -> 222
   Spark: [111, 222]
   Comet: [111, 111]
   ```
   
   Restoring the previous alias-admission behavior on this head makes the same 
query fall back and return `[111, 222]`. The single-store limitation already 
exists for plain S3, but this PR newly exposes alias reads that previously fell 
back. Preserve resolved storage/bucket identity when grouping files, or decline 
mixed-store alias scans.



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -490,7 +498,9 @@ case class CometScanRule(session: SparkSession)
             val hadoopS3Options = 
NativeConfig.extractObjectStoreOptions(hadoopConf, effectiveUri)
 
             val hadoopDerivedProperties =
-              
CometIcebergNativeScan.hadoopToIcebergS3Properties(hadoopS3Options)
+              CometIcebergNativeScan.hadoopToIcebergS3Properties(
+                hadoopS3Options,
+                NativeConfig.bucketForUri(effectiveUri, s3CompliantSchemes))

Review Comment:
   **[P2] Do not apply metadata-bucket settings to every Iceberg file**
   
   `effectiveUri` selects the metadata bucket, so this promotes that bucket's 
overrides into the global properties used by one native `FileIO` for all 
data/delete files. Iceberg supports a separate data location through 
`write.data.path`.
   
   For a `HadoopFileIO` table with metadata under `s3://metadata/...`, data 
under `s3://data/...`, working global `fs.s3a.*` data-store settings, and 
`fs.s3a.bucket.metadata.*` overrides, the base translation sends a data request 
to the data endpoint with the data credentials. This revision instead sends the 
same request to the metadata endpoint with metadata credentials. An actual HTTP 
probe through the pinned FileIO returned success before the change and 404 
afterward.
   
   This trigger applies when FileIO properties do not supply overriding `s3.*` 
settings, and also affects ordinary S3 scans with aliases disabled. Evidence is 
compiled exact base/head configuration helpers plus FileIO requests, not a full 
Spark query. Resolve settings for the actual data/delete locations, or fall 
back when the scan cannot share one configuration.



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -1018,30 +1060,48 @@ case class CometScanTypeChecker() extends 
DataTypeSupport with CometTypeShim {
 
 object CometScanRule extends Logging {
 
-  // Per-scheme memo of `NativeBase.isObjectStoreSchemeSupported`. The answer 
depends only on the
-  // URL scheme, so we cache by scheme and never re-cross the JNI boundary for 
a repeated scheme.
+  // Memo of `NativeBase.isObjectStoreSchemeSupported`, keyed by (scheme, 
has-authority) because
+  // object_store's parser keys on that pair (host-based stores need a host, 
file/memory need
+  // none). Caching by scheme alone lets an authorityless URL poison the whole 
scheme's answer.
   private val schemeSupportCache =
-    new ConcurrentHashMap[String, JBoolean]()
+    new ConcurrentHashMap[(String, Boolean), JBoolean]()
+
+  /**
+   * Opt-in S3-compliant alias schemes from `fs.comet.s3Compliant.schemes` 
(comma-separated,
+   * trimmed, lowercased; empty/missing => none). The native gate no longer 
claims these, so the
+   * opt-in is decided here where the Hadoop config is available.
+   */
+  private[rules] def resolveS3CompliantSchemes(hadoopConf: Configuration): 
Set[String] =
+    NativeConfig.parseSchemeSet(hadoopConf.get(COMET_S3_COMPLIANT_SCHEMES_KEY))
 
   /**
-   * True when Comet's native object_store layer recognizes this URI's scheme 
(so the scan is
-   * natively readable). Delegates to the native layer -- the source of truth 
-- instead of a
-   * hardcoded scheme list. On any failure to consult native (e.g. the library 
isn't loaded on
-   * this JVM, or predates this method) we assume the scheme IS supported: the 
scheme gate is an
-   * early-fallback optimization, and a build without a working native library 
can't run Comet's
-   * native scan anyway, so declining here would only over-restrict.
+   * True when Comet's native Parquet scan can read this URI's scheme: 
object_store recognizes it
+   * (asked of the native layer, the source of truth) OR it is an opt-in 
S3-compliant alias. If
+   * native can't be consulted (library not loaded), assume supported -- the 
gate is only an
+   * early-fallback optimization and such a build can't run the native scan 
anyway.
    */
-  private[rules] def isNativelyReadableScheme(uri: URI): Boolean = {
+  private[rules] def isNativelyReadableScheme(
+      uri: URI,
+      s3CompliantSchemes: Set[String]): Boolean = {
     val scheme = uri.getScheme
     if (scheme == null) return true
+    val lower = scheme.toLowerCase(Locale.ROOT)
+    if (s3CompliantSchemes.contains(lower)) return true
+    val hasAuthority = uri.getRawAuthority != null
     schemeSupportCache
       .computeIfAbsent(
-        scheme.toLowerCase(Locale.ROOT),
-        _ =>
-          try 
JBoolean.valueOf(NativeBase.isObjectStoreSchemeSupported(uri.toString))
+        (lower, hasAuthority),
+        _ => {
+          // Probe a scheme(+fixed dummy host) URL, never the caller's 
authority/path: object_store
+          // keys on (scheme, host-presence) so we cache/probe by that pair. A 
fixed host stops an
+          // authorityless URL poisoning the authority-bearing form; dropping 
the real path avoids a
+          // spurious `false` from chars object_store rejects in a `Path` 
(e.g. Iceberg's `p=%0A`).
+          val probe = if (hasAuthority) s"$lower://comet-probe-host/" else 
s"$lower:///"
+          try JBoolean.valueOf(NativeBase.isObjectStoreSchemeSupported(probe))

Review Comment:
   **[P2] Preserve actual-path validation before selecting native Parquet**
   
   The dummy probe loses a path-specific rejection that native execution still 
enforces. A local Parquet directory whose name contains an actual newline is 
readable by Spark; its URI contains `%0A`. The exact native probe returns 
`false` for that URI and `true` for `file:///`.
   
   In a full Spark reproduction, Spark read `[0, 1, 2]`, while this head 
selected `CometNativeScanExec` and failed with `Generic URL error` / 
`Encountered illegal character sequence` when opening the real path. Restoring 
the previous admission decisions on this head, in a fresh test JVM, restored 
successful Spark fallback.
   
   This is an actual newline in a Parquet directory name, distinct from 
Iceberg's literal `p=%0A` path that motivated the earlier cache fix. Keep 
scheme-capability caching path-independent, but validate the actual Parquet 
path separately before claiming native execution.



##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -1054,13 +1114,52 @@ 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 Comet's native Iceberg scan can actually open, mirroring the 
match arms in
+   * 
`native/core/src/execution/operators/iceberg_scan.rs::storage_factory_for`. 
Deliberately NOT
+   * delegated to `isNativelyReadableScheme`: object_store recognizes schemes 
(http/https, azure,
+   * memory) that iceberg-rust's OpenDAL storage factory cannot build, and 
admitting them here
+   * turns a clean JVM fallback into a native runtime "Unsupported storage 
scheme" error. Add here
+   * what you add to `storage_factory_for` (currently Aliyun `oss` and GCS 
`gs`). S3-compliant
+   * aliases like `blob` are opt-in via `fs.comet.s3Compliant.schemes` (see
+   * `isIcebergReadableScheme`), not hardcoded, since the native planner opens 
them via S3.
+   */
+  private val icebergReadableSchemes: Set[String] =
+    Set("file", "s3", "s3a", "gs", "oss")
+
+  /** Effective Iceberg-readable schemes: built-in allowlist plus opt-in 
S3-compliant aliases. */
+  private[rules] def icebergSupportedSchemes(s3CompliantSchemes: Set[String]): 
Set[String] =
+    icebergReadableSchemes ++ s3CompliantSchemes
+
+  /** "Supported schemes: ..." suffix shared by the Iceberg scheme-fallback 
messages. */
+  private[rules] def icebergSupportedSchemesMessage(s3CompliantSchemes: 
Set[String]): String = {
+    val schemes = 
icebergSupportedSchemes(s3CompliantSchemes).toSeq.sorted.mkString(", ")
+    s"Supported schemes: $schemes"
+  }
+
+  /**
+   * Scheme gate for the Iceberg scan path: admit schemes iceberg-rust's 
storage factory can build
+   * (`icebergReadableSchemes`) or opt-in S3-compliant aliases 
(`fs.comet.s3Compliant.schemes`),
+   * plus schemeless local paths (which route to its LocalFs backend).
+   */
+  private[rules] def isIcebergReadableScheme(
+      uri: URI,
+      s3CompliantSchemes: Set[String]): Boolean = {
+    val scheme = uri.getScheme
+    if (scheme == null) return true
+    val lower = scheme.toLowerCase(Locale.ROOT)
+    icebergReadableSchemes.contains(lower) || 
s3CompliantSchemes.contains(lower)

Review Comment:
   **[P2] Reject authorityless Iceberg alias file paths**
   
   The scheme-only gate admits an actual data/delete location such as 
`blob:///bucket/key.parquet`, but Iceberg preserves that raw string and the 
selected S3 backend requires a URL host. Promoting the bucket while extracting 
Scala configuration does not change the transport path.
   
   Using the pinned Iceberg/OpenDAL FileIO and a local HTTP success control, 
`new_input(path).metadata()` succeeds for `blob://bucket/key.parquet` and 
returns `DataInvalid: ... missing bucket` for `blob:///bucket/key.parquet`. 
This is a FileIO component reproduction; an authorityless metadata location 
alone is not sufficient, since it selects the factory without necessarily being 
opened.
   
   Parquet explicitly promotes the authorityless form, so its support cannot be 
assumed for the raw Iceberg route. Decline these data/delete paths before 
native admission, or adapt only their transport location while preserving the 
original logical identity used for delete matching.



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