cloud-fan commented on code in PR #58409:
URL: https://github.com/apache/spark/pull/58409#discussion_r3913900478


##########
sql/core/src/main/scala/org/apache/spark/sql/avro/AvroDeserializer.scala:
##########
@@ -452,15 +462,44 @@ private[sql] class AvroDeserializer(
     }
   }
 
+  /**
+   * The position of each `projection` field in `dataSchema`, which is what a 
positional field match
+   * resolves against. Empty when there is no data schema to resolve against, 
or when field matching
+   * is by name and the positions are unused.
+   *
+   * This takes a data schema position for an Avro field position, which 
`recursiveFieldMaxDepth`
+   * can break: `SchemaConverters` drops a field it will not recurse into, so 
the data schema is a
+   * gapped view of the Avro schema and every field after the gap resolves one 
position early.
+   * Positional matching is already wrong for such a schema without this 
method, because the fields
+   * after the gap shift by one whatever the projection is.
+   */
+  private def positionsInDataSchema(projection: StructType): Array[Int] = 
dataSchema match {
+    case Some(schema) if positionalFieldMatch =>
+      projection.map(field => schema.fieldIndex(field.name)).toArray

Review Comment:
   **Non-blocking (P2):** [P2] Preserve ordinals when the data schema has 
duplicate names
   
   `schema.fieldIndex(field.name)` cannot distinguish duplicate field-name 
occurrences. Spark permits duplicate file-source schema names, so with 
`positionalFieldMatching=true` a two-field schema such as `(x, x)` maps both 
fields to one Avro ordinal and can silently duplicate a value, or fail when the 
physical types differ. Please carry each required field's source ordinal from 
the scan-pruning boundary into the deserializer and add V1 and V2 regression 
coverage with duplicate names and distinct values.
   
   **Recommended change:** Preserve occurrence-specific source ordinals at the 
scan-pruning boundary and pass them into Avro deserialization instead of 
reconstructing them from StructField names.
   
   **Why this works:** Capture the ordinal mapping while the pruned attributes 
still distinguish duplicate occurrences, thread it through the V1 and V2 reader 
construction paths, and let AvroSchemaHelper consume those ordinals directly.
   
   **Scope:** Avro V1/V2 scan-pruning and reader/deserializer plumbing, plus 
focused duplicate-name regression tests in the Avro module.
   
   **Compatibility:** Unique-name schemas and name-based matching retain their 
current behavior; supported duplicate-name positional reads are corrected to 
return distinct physical fields.
   
   **Risks:** Ordinal plumbing could diverge between the V1 and V2 construction 
paths. Capturing positions after occurrence identity has already been reduced 
to StructField names would preserve the bug.
   
   **Constraints:** Keep nested-record matching on explicit local positions. Do 
not change default name-based matching or broaden the separate 
recursiveFieldMaxDepth limitation.
   
   **Success:** V1 and V2 positional reads return distinct physical values for 
duplicate-name user-schema fields, with regression coverage proving the mapping 
and no change to existing unique-name behavior.



##########
sql/core/src/main/scala/org/apache/spark/sql/avro/AvroDeserializer.scala:
##########
@@ -45,6 +45,13 @@ import org.apache.spark.unsafe.types.UTF8String
 
 /**
  * A deserializer to deserialize data in avro format to data in catalyst 
format.
+ *
+ * @param dataSchema The schema `rootCatalystType` was projected from, for a 
read that prunes
+ *                   columns. A positional field match pairs a Catalyst field 
with the Avro field
+ *                   at the same position, and the position that means is the 
one in the full

Review Comment:
   **Nit (P3):** [P3] Fix the malformed Scaladoc sentence
   
   `the position that means is the one` is not grammatical and obscures which 
ordinal is being described. Please change it to something like `that position 
is the one in the full schema rather than in the projection`.



##########
connector/avro/src/test/scala/org/apache/spark/sql/avro/AvroSuite.scala:
##########
@@ -1737,6 +1737,142 @@ abstract class AvroSuite
     }
   }
 
+  test("SPARK-59108: positionalFieldMatching resolves fields against the full 
schema") {
+    withTempPath { dir =>
+      val path = dir.getCanonicalPath
+      spark.range(0, 5).selectExpr("id AS a", "id * 100 AS b", "id * 10000 AS 
c")
+        .write.format("avro").save(path)
+      // The names differ from the file's, so only the positions can pair the 
two schemas.
+      val renamedSchema = new StructType()
+        .add("x", LongType).add("y", LongType).add("z", LongType)
+      val df = spark.read.format("avro")
+        .option("positionalFieldMatching", true.toString)
+        .schema(renamedSchema)
+        .load(path)
+
+      val rows = (0 until 5).map(i => Row(i.toLong, i * 100L, i * 10000L))
+      checkAnswer(df, rows)
+      // A column keeps its own Avro field however few of them the query 
projects.
+      checkAnswer(df.select("z"), rows.map(r => Row(r.get(2))))
+      checkAnswer(df.select("y"), rows.map(r => Row(r.get(1))))
+      checkAnswer(df.select("x", "z"), rows.map(r => Row(r.get(0), r.get(2))))
+      checkAnswer(df.select("z", "x"), rows.map(r => Row(r.get(2), r.get(0))))
+      checkAnswer(df.select("y", "z"), rows.map(r => Row(r.get(1), r.get(2))))
+      checkAnswer(df.selectExpr("sum(z)"), Row(100000L))
+      // With pushdown on the filter runs inside the deserializer, with it off 
above the scan.

Review Comment:
   **Nit (P3):** [P3] Delimit the two pushdown conditions
   
   As written, `pushdown on the filter` initially parses as a single phrase. 
Please write: `With pushdown on, the filter runs inside the deserializer; with 
it off, the filter runs above the scan.`



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