This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 7426431337 fix(workflow-core): resolve Windows drive paths as local
files (#8950)
7426431337 is described below
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"))