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