peter-toth commented on code in PR #60197:
URL: https://github.com/apache/spark/pull/60197#discussion_r4501298586


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/ExistingRDD.scala:
##########
@@ -117,7 +116,9 @@ case class LogicalRDD(
     session :: originStats :: originConstraints :: Boolean.box(fromCheckpoint) 
:: Nil
 
   override def newInstance(): LogicalRDD.this.type = {
-    val rewrite = Utils.toMap(output, output.map(_.newInstance()))
+    // Keyed by exprId, because the attributes in the partitioning, the 
ordering, the stats and
+    // the constraints can differ from `output` in qualifier or nullability.

Review Comment:
   **Finding 3.** `newInstance` can only remap what refers to `output`. But 
`fromDataset` (lines 210-211) copies `executedPlan.outputPartitioning` and 
`outputOrdering` without mapping them onto `output` at all, so their exprIds 
can differ from `output` too. `RemoveRedundantAliases` drops a same-name root 
alias, so the executed plan outputs the source column. With AQE off when 
checkpointing:
   ```scala
   val orig = spark.range(16)
   val cp = orig.repartition($"id").select($"id".as("id")).localCheckpoint()
   // LogicalRDD output [id#236], outputPartitioning hashpartitioning(id#232, 
5): orig's column
   broadcast(orig).join(cp, orig("id") % 2 === cp("id") % 
2).groupBy(orig("id")).count()
   ```
   This returns 72 rows of partial counts instead of 16 rows with count 8, on 
the base and at this head. It's issue 1 without a self-join. 
`rewriteStatsAndConstraints` already maps the optimized output onto the 
analyzed output for stats and constraints. Doing the same here for the executed 
plan's output fixes it:
   ```scala
       val toOutput = AttributeMap(executedPlan.output.zip(logicalPlan.output))
       def remap[E <: Expression](e: E): E =
         e.transform { case a: Attribute => toOutput.getOrElse(a, a) 
}.asInstanceOf[E]
       val partitioning = 
firstLeafPartitioning(executedPlan.outputPartitioning) match {
         case p: Expression => remap(p).asInstanceOf[Partitioning]
         case p => p
       }
       // then pass `partitioning` and `executedPlan.outputOrdering.map(remap)`
   ```
   With it, the query returns 16 rows with count 8, and `DatasetSuite` still 
passes.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/ExistingRDD.scala:
##########
@@ -117,7 +116,9 @@ case class LogicalRDD(
     session :: originStats :: originConstraints :: Boolean.box(fromCheckpoint) 
:: Nil
 
   override def newInstance(): LogicalRDD.this.type = {
-    val rewrite = Utils.toMap(output, output.map(_.newInstance()))
+    // Keyed by exprId, because the attributes in the partitioning, the 
ordering, the stats and
+    // the constraints can differ from `output` in qualifier or nullability.
+    val rewrite = AttributeMap(output.zip(output.map(_.newInstance())))

Review Comment:
   **Finding 1.** With `rewrite` keyed by exprId, `output.map(rewrite)` at line 
140 maps output columns that share an exprId onto one new attribute: the last 
column's new instance. If they differ in name or qualifier, the renewed copy 
renames a column. Case-insensitive resolution, the default, produces such 
columns:
   ```scala
   val cp = spark.range(3).select($"id", $"ID").localCheckpoint()  // output 
[id#0, ID#0]
   cp.join(cp).columns
   // base:      id, ID, id, ID
   // this head: id, ID, ID, ID
   ```
   Qualifiers behave the same way. Checkpointing `df.as("s").select(df("id"), 
$"s.id")` gives the output `[t.id, s.id]` on one exprId. So `$"t.id"` over 
`cp.join(cp)` resolves silently at this head, where the base raises 
`AMBIGUOUS_REFERENCE`.
   
   Keeping each attribute and swapping only its exprId, as SPARK-54718 did for 
`CTERelationRef.newInstance` (`cteOperators.scala:247`), fixes it:
   ```scala
         output.map(a => a.withExprId(rewrite(a).exprId)),
   ```
   With it, `cp.join(cp).columns` is `id, ID, id, ID` again, and the new test 
still passes. A `columns` check in the test would pin it.
   



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