peter-toth commented on code in PR #58340:
URL: https://github.com/apache/spark/pull/58340#discussion_r3881910283
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/FileTable.scala:
##########
@@ -111,7 +120,15 @@ abstract class FileTable(
override def properties: util.Map[String, String] =
options.asCaseSensitiveMap
- override def capabilities: java.util.Set[TableCapability] =
FileTable.CAPABILITIES
+ override def capabilities: java.util.Set[TableCapability] =
+ if (supportsScanMerging) FileTable.CAPABILITIES_WITH_SCAN_MERGING else
FileTable.CAPABILITIES
Review Comment:
**Finding 8.** The criterion holds only while the read is strict.
Your reply on @dongjoon-hyun's thread justifies the four overrides with "a
corrupt column chunk, or `datetimeRebaseModeInRead=EXCEPTION` meeting an
ancient value, makes any format throw on a column the narrower scan had
pruned". That is true only with `spark.sql.files.ignoreCorruptFiles=false`.
With it on, `FilePartitionReader.next` catches any `RuntimeException` or
`IOException` and returns `false`, dropping the rest of that file
(`FilePartitionReader.scala:74`, via
`DataSourceUtils.shouldIgnoreCorruptFileException`). So the merged scan returns
fewer rows than the narrow scan did. That is the "silent and unrecoverable
change to row membership" you gave as what separates CSV and JSON, reached by a
format that declares the capability.
Measured on this head, parquet, `spark.sql.files.ignoreCorruptFiles=true`.
`b` is written as a string and read as a long, so the reader throws only when
it actually reads `b`; a corrupt column chunk is the same code path, just
harder to build in a test.
```scala
spark.range(0, 10).selectExpr("id AS a", "cast(id AS string) AS
b").write.parquet(path)
spark.read.schema("a long, b
long").parquet(path).createOrReplaceTempView("t")
sql("SELECT (SELECT sum(a) FROM t), (SELECT count(b) FROM t)")
```
| path | default | `MergeSubplans` excluded |
|---|---|---|
| V2 | `[null, 0]` | `[45, 0]` |
| V1 | `[null, 0]` | `[45, 0]` |
`sum(a)` touches only healthy data and is correct at 45 today; after the
merge it is `null`. V1 is the same, so this is V1 parity rather than a V1/V2
split - but it is the parity you declined to copy for CSV and JSON, on the same
reasoning. ORC did not reproduce with this particular type mismatch; the
exposure is any per-column read failure, so it is not parquet-specific.
The fix has a precedent in the tree: `InMemoryRelation` refuses to treat a
file scan as repeatable under either flag (`InMemoryRelation.scala:378-398`,
and `FileScanRDD.hasStrictFileReads`).
```scala
override def capabilities: java.util.Set[TableCapability] =
if (supportsScanMerging && hasStrictFileReads) {
FileTable.CAPABILITIES_WITH_SCAN_MERGING
} else {
FileTable.CAPABILITIES
}
/**
* Whether a read of this table is strict. A best-effort read is not
reproducible: a failure on a
* column only the other scan projects is swallowed, so the merged scan
would not read a superset
* of either input's rows.
*/
private def hasStrictFileReads: Boolean = {
val fileSourceOptions = new
FileSourceOptions(options.asCaseSensitiveMap.asScala.toMap)
!fileSourceOptions.ignoreCorruptFiles &&
!fileSourceOptions.ignoreMissingFiles
}
```
`FileSourceOptions` is already imported here and it resolves the per-read
option over the session conf, so both spellings are covered. Only
`ignoreCorruptFiles` has the failure above - a missing file drops the same rows
whatever is projected - so including `ignoreMissingFiles` is just matching the
existing predicate; drop it if you prefer the narrower gate.
If you would rather keep the capability unconditional, then the
migration-guide entry has to come back for this shape. Unlike the errors the
criterion accepts, this one is a silent result change.
##########
connector/avro/src/main/scala/org/apache/spark/sql/v2/avro/AvroTable.scala:
##########
@@ -52,4 +52,9 @@ case class AvroTable(
override def supportsDataType(dataType: DataType): Boolean =
AvroUtils.supportsDataType(dataType)
override def formatName: String = "Avro"
+
+ // Every record is decoded against the full schema before the projection is
applied to the decoded
Review Comment:
**Finding 9.** The first sentence is not what `AvroPartitionReaderFactory`
does.
The deserializer is built from `readDataSchema`, i.e. the projection, not
the full schema (`AvroPartitionReaderFactory.scala:101-111`).
`positionalFieldMatching` is passed straight through, and
`AvroUtils.AvroSchemaHelper.getAvroField` then resolves a catalyst field by its
*position in that projected schema* (`AvroUtils.scala:463-469`). So the
projection is exactly what decides which Avro field feeds which column, and
widening it re-maps them.
Measured on this head. File fields `a`, `b`, `c` holding `id`, `100 * id`,
`10000 * id` for ids 0 to 4, read with `positionalFieldMatching=true`, query
`SELECT (SELECT sum(a) FROM t), (SELECT sum(c) FROM t)`:
| path | default | `MergeSubplans` excluded |
|---|---|---|
| V2 | `[10, 1000]` | `[10, 10]` |
| V1 | `[10, 1000]` | `[10, 10]` |
`sum(c)` is 100000. Both values are wrong, because Avro positional matching
against a pruned schema is already broken without this PR: the `c`-only scan
resolves position 0 and reads `a`. Merging cannot turn a correct answer into a
wrong one here either, since the union of two prefixes is the longer prefix. So
this is not a correctness regression, and I am not asking you to exclude Avro.
Two smaller asks. State the mechanism as it is - what makes merging safe for
Avro is that the format has no record-level parse verdict, not that decoding
ignores the projection - so a later reader does not build on a property the
reader does not have. And "No query result changes" in the description needs a
qualifier, or the pruning bug needs its own JIRA to point at.
@dongjoon-hyun named `positionalFieldMatching` as the Avro-specific risk on
the coverage thread above; the new `AvroV2Suite` test does not reach it.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/parquet/ParquetTable.scala:
##########
@@ -70,4 +70,8 @@ case class ParquetTable(
}
override def formatName: String = "Parquet"
+
+ // A row is decoded from the column chunks the scan asked for, so reading
more columns can only
+ // surface an error, never silently change which rows come back.
+ override def supportsScanMerging: Boolean = true
Review Comment:
**Finding 11.** This override widens the seam from `protected` to public.
`FileTable.supportsScanMerging` is `protected`, and an override with no
access modifier is public in Scala. So `ParquetTable`, `OrcTable`, `TextTable`
and `AvroTable` each expose it as public API while `CSVTable` and `JsonTable`
keep it protected. Either add the modifier to the four, or drop it from the
base if the seam is meant to be part of the format-author surface.
```suggestion
override protected def supportsScanMerging: Boolean = true
```
--
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]