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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-8950-988c655597f30e89cd2887dc8c7f27499ad0fef8
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 74264313370d747ac748300f0bbbb780297d7e65
Author: Xinyuan Lin <[email protected]>
AuthorDate: Thu Oct 15 05:20:47 2026 +0000

    fix(workflow-core): resolve Windows drive paths as local files (#8950)
    
    ### What changes were proposed in this PR?
    
    `FileResolver.isFileResolved` no longer counts a one-letter
    `java.net.URI` scheme as "already a URI". A one-letter scheme is a
    Windows drive letter, so `C:/Users/me/a.txt` now falls through to the
    local resolver and becomes `file:///C:/Users/me/a.txt`.
    
    **Root cause.** `isFileResolved` returned true for any non-empty scheme,
    and `new URI("C:/Users/me/a.txt")` parses `C` as the scheme. `resolve`
    therefore returned the path unchanged, and `DocumentFactory` rejected it
    with `Unsupported URI scheme: C for creating the ReadonlyDocument`. File
    Scan hit this in the worker; the CSV / JSONL / Arrow scans hit it at
    compilation, during schema inference.
    
    ```
    Before:  "C:/data/a.txt" -> isFileResolved = true  -> URI "C:/data/a.txt"   
                      -> Unsupported URI scheme: C
    After:   "C:/data/a.txt" -> isFileResolved = false -> local resolver -> 
"file:///C:/data/a.txt" -> read
    ```
    
    Every scheme Texera produces or reads (`file`, `dataset`, `model`,
    `vfs`, `s3`, `https`, ...) has more than one letter, so those are still
    treated as resolved. The backslash form `C:\data\a.txt` was never
    affected: a backslash is illegal in a URI.
    
    ### Any related issues, documentation, discussions?
    
    Closes #8949
    
    ### How was this PR tested?
    
    New cases in `FileResolverSpec`:
    
    | Input | Pinned behavior |
    | --- | --- |
    | `C:/x/y.txt`, `c:/x/y.txt`, `C:\x\y.txt` | `isFileResolved` is false |
    | `s3://`, `dataset:///`, `file:///C:/...`, `file:/tmp/...`, `vfs:///`,
    `https://` | `isFileResolved` is true |
    | an existing temp file's absolute path, written with `/` | `resolve`
    returns that file's `file` URI |
    | `C:/x/y.txt` (does not exist) | `resolve` throws
    `FileNotFoundException` instead of returning `URI("C:/x/y.txt")` |
    
    Before the fix, the `C:/x/y.txt` cases (`isFileResolved` and `resolve`)
    failed on every OS. The temp-file case failed on Windows, where the path
    starts with a drive letter.
    
    ```
    sbt "WorkflowCore/testOnly *FileResolver*"   # 26 passed
    sbt scalafmtCheckAll "WorkflowCore/scalafixAll --check"
    ```
    
    Callers that go through `resolve` / `fileResolved()` also pass locally:
    `WorkflowCompiler/testOnly *LogicalPlanSpec *WorkflowCompilerSpec` (60
    passed), and `WorkflowOperator/testOnly *ScanSourceOpDescSpec
    *TestOperatorsSpec *FileScanSourceOpDescSpec *CSVScanSourceOpDescSpec
    *JSONLScanSourceOpDescSpec *FileScanOpExecSpec` (78 of 79 passed). The
    one failure is `FileScanSourceOpDescSpec`'s UTF-16 test. On Windows it
    cannot delete its temp file while the reader is still open, and it fails
    the same way without this change.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5.5)
---
 .../texera/amber/core/storage/FileResolver.scala   |  5 +++-
 .../texera/amber/storage/FileResolverSpec.scala    | 34 ++++++++++++++++++++--
 2 files changed, 36 insertions(+), 3 deletions(-)

diff --git 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
index f43db07198..5571cb3f4d 100644
--- 
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
+++ 
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
@@ -332,13 +332,16 @@ object FileResolver {
   /**
     * Checks if a given file path has a valid scheme.
     *
+    * A one-letter scheme is a Windows drive letter (`C:/data/a.csv`), not a 
URI scheme, so such a
+    * path is left to the local resolver.
+    *
     * @param filePath The file path to check.
     * @return `true` if the file path contains a valid scheme, `false` 
otherwise.
     */
   def isFileResolved(filePath: String): Boolean = {
     try {
       val uri = new URI(filePath)
-      uri.getScheme != null && uri.getScheme.nonEmpty
+      uri.getScheme != null && uri.getScheme.length > 1
     } catch {
       case _: Exception => false // Invalid URI format
     }
diff --git 
a/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
 
b/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
index 93b92c1ee2..7ad4fd1a8e 100644
--- 
a/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
+++ 
b/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
@@ -40,7 +40,7 @@ import org.apache.texera.dao.jooq.generated.tables.pojos.{
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
 
-import java.nio.file.Paths
+import java.nio.file.{Files, Paths}
 
 class FileResolverSpec
     extends AnyFlatSpec
@@ -175,6 +175,24 @@ class FileResolverSpec
     assert(localUri == Paths.get(localCsvFilePath).toUri)
   }
 
+  "FileResolver" should "resolve an absolute local path written with forward 
slashes" in {
+    // On Windows this is `C:/...`, whose drive letter `java.net.URI` parses 
as a scheme.
+    val localFile = Files.createTempFile("file-resolver-spec", ".txt")
+    try {
+      val absolutePath = localFile.toAbsolutePath.toString.replace('\\', '/')
+
+      assert(FileResolver.resolve(absolutePath) == localFile.toUri)
+    } finally {
+      Files.deleteIfExists(localFile)
+    }
+  }
+
+  "FileResolver" should "not pass a missing Windows path through as a URI" in {
+    assertThrows[FileNotFoundException] {
+      FileResolver.resolve("C:/x/y.txt")
+    }
+  }
+
   "FileResolver" should "resolve dataset file correctly" in {
     val datasetACsvUri = FileResolver.resolve(datasetACsvFilePath)
     val dataset1TxtUri = FileResolver.resolve(dataset1TxtFilePath)
@@ -235,11 +253,15 @@ class FileResolverSpec
     }
   }
 
-  "isFileResolved" should "return true when the path has a non-empty scheme" 
in {
+  "isFileResolved" should "return true when the path has a multi-letter 
scheme" in {
     assert(FileResolver.isFileResolved("s3://bucket/key"))
     assert(
       
FileResolver.isFileResolved(s"${FileResolver.DATASET_FILE_URI_SCHEME}:///repo/hash/file.csv")
     )
+    assert(FileResolver.isFileResolved("file:///C:/x/y.txt"))
+    assert(FileResolver.isFileResolved("file:/tmp/y.txt"))
+    assert(FileResolver.isFileResolved("vfs:///repo/hash/file.csv"))
+    assert(FileResolver.isFileResolved("https://example.com/y.txt";))
   }
 
   it should "return false when the path has no scheme" in {
@@ -247,6 +269,14 @@ class FileResolverSpec
     
assert(!FileResolver.isFileResolved("/[email protected]/test_dataset/v1/1.txt"))
   }
 
+  it should "return false for an absolute Windows path" in {
+    // `java.net.URI` parses the drive letter as a one-letter scheme
+    assert(!FileResolver.isFileResolved("C:/x/y.txt"))
+    assert(!FileResolver.isFileResolved("c:/x/y.txt"))
+    // a backslash is illegal in a URI, so this form never parsed as one
+    assert(!FileResolver.isFileResolved("C:\\x\\y.txt"))
+  }
+
   it should "return false for a malformed URI" in {
     // a space is illegal in a URI and makes the java.net.URI constructor throw
     assert(!FileResolver.isFileResolved("has a space"))

Reply via email to