Joorgem opened a new pull request, #57769:
URL: https://github.com/apache/spark/pull/57769

   ### What changes were proposed in this pull request?
   
   `DataSource.checkAndGlobPathIfNecessary` runs its lambda once per 
glob-looking path, but the `enableGlobbing == false` branch returned 
`qualifiedPaths` — the whole input list:
   
   ```scala
   ThreadUtils.parmap(globPaths, "globPath", numThreads) { globPath =>
     val fs = globPath.getFileSystem(hadoopConf)
     val globResult = if (enableGlobbing) {
       SparkHadoopUtil.get.globPath(fs, globPath)
     } else {
       qualifiedPaths          // <-- the whole list, once per glob-looking path
     }
   ```
   
   The branch now returns `Seq(globPath)`, the path it was handed.
   
   ### Why are the changes needed?
   
   **The query returns duplicate rows.** With `G` paths that look like globs 
and `n` that do not, the result held `G*(G+n)+n` entries instead of `G+n`. 
Those duplicates become the file index's `rootPaths`, and for an unpartitioned 
relation `PartitioningAwareFileIndex.allFiles()` does `rootPaths.flatMap`, so 
the same `FileStatus` is returned several times and `FileScanRDD` reads the 
same file repeatedly.
   
   Three single-row files, one of them named `weird_[x].csv`:
   
   ```
   spark.read.options(**{"__globPaths__": "false"}).csv(paths).count()
   # 5      <-- three files, five rows
   #   x2  ('0', 'row_from_plain_a.csv')     DUPLICATED
   #   x2  ('1', 'row_from_plain_b.csv')     DUPLICATED
   #   x1  ('2', 'row_from_weird_[x].csv')
   ```
   
   "Looks like a glob" is a bare character scan with no notion of escaping 
(`SparkHadoopUtil.isGlobPath` tests for any of `{}[]*?\`), so the branch is 
reached by ordinary data. Filenames containing brackets are legal on HDFS and 
S3 — and SPARK-32810 and SPARK-32815 exist precisely because the project 
decided such names must work.
   
   **The highest-impact caller is the Structured Streaming file source**, which 
sets `GLOB_PATHS_KEY -> "false"` unconditionally and hands the whole 
microbatch's file list to a `DataSource`. So a microbatch containing one file 
whose *name* holds a metacharacter emits every other file in that batch more 
than once. `MLUtils.parseLibSVMFile` and the `TextInput*.infer` 
schema-inference paths pass multiple paths with globbing disabled too.
   
   The diff is one expression, so it is worth being explicit that the *finding* 
is silent data duplication rather than a style change.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes, and only in the direction of correctness: affected reads return the 
right number of rows instead of a larger one, and do less I/O.
   
   No behaviour changes for the case the option was introduced to serve. With a 
single path, `qualifiedPaths` and `Seq(globPath)` are the same value, so every 
scenario SPARK-32810 fixed and tested behaves identically. `globResult` stays 
non-empty, so the `checkEmptyGlobPath` check cannot begin raising 
`PATH_NOT_FOUND` where it did not before.
   
   ### How was this patch tested?
   
   Two tests added to `DataSourceSuite`, and **verified to fail on unmodified 
`master` before the fix was applied** — the fork CI run of the test alone 
reported `Tests: succeeded 21345, failed 2`, the two failures being exactly 
these, with the duplication visible in the messages:
   
   ```
   - SPARK-58518: checkAndGlobPathIfNecessary must not duplicate paths when 
globbing is disabled *** FAILED ***
     List(mockFs://mockFs/somepath1, mockFs://mockFs/somepath2, 
mockFs://mockFs/globpath1*,
          mockFs://mockFs/somepath1, mockFs://mockFs/somepath2)
   
   - SPARK-58518: a filename with a glob metacharacter must not duplicate rows 
when globbing is disabled *** FAILED ***
     Array("row_a", "row_a", "row_b", "row_b", "row_c") did not equal 
Array("row_a", "row_b", "row_c")
   ```
   
   With the fix, the full matrix is green.
   
   **Both tests assert on ordered sequences rather than sets, and that is 
deliberate.** Every existing assertion in this suite compares 
`resultPaths.toSet` against a `Set` — and a `Set` is exactly what erases 
duplication. Together with the fact that all six existing 
`checkAndGlobPathIfNecessary` tests pass `enableGlobbing = true`, that is why 
this went unnoticed: the branch was never exercised, and the suite's assertion 
style could not have caught it if it had been.
   
   The end-to-end test uses `text` rather than CSV, since the defect is in 
`DataSource` and is format-agnostic. Verified by hand in `text`, `json`, `csv` 
and `parquet`, all returning five rows for three files.
   
   The predicted count holds exactly, which is what distinguishes a diagnosis 
from an observation:
   
   | G | n | files | predicted `G*(G+n)+n` | observed |
   |---|---|---|---|---|
   | 1 | 0 | 1 | 1 | 1 |
   | 1 | 2 | 3 | 5 | 5 |
   | 2 | 0 | 2 | 4 | 4 |
   | 2 | 2 | 4 | 10 | 10 |
   | 3 | 1 | 4 | 13 | 13 |
   
   The `G=1, n=0` row is also the answer to why this survived since 2020: every 
test SPARK-32810 added reads a single path, and that is the one case that comes 
back correct.
   
   **Scope, stated honestly:** the correctness impact needs an unpartitioned 
relation. When partition columns are discovered, `allFiles()` takes a branch 
returning a `Map`'s values, which absorbs the duplicates — measured on the same 
data, 15 rows without `basePath` against 9 with it. On partitioned relations 
this remains wasted listing work rather than wrong results.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Opus 5)
   
   ### Related
   
   - SPARK-32810 (#29659) introduced this branch; SPARK-32815 covers LibSVM 
with such filenames. Neither reports this defect.
   - SPARK-28266 also produced duplicate rows, but from a Hive serde `path` 
property repeating the table LOCATION, and was fixed in #33328 — nowhere near 
this method. It does establish that duplicated input paths are treated as a 
correctness bug.
   - No JIRA matches this method plus duplication, and no PR mentions 
`enableGlobbing` apart from #29659.
   


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