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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RewriteUpdateTable.scala:
##########
@@ -226,4 +389,98 @@ object RewriteUpdateTable extends RewriteRowLevelCommand {
     val expandOutput = generateExpandOutput(attrs, outputs)
     Expand(outputs, expandOutput, matchedRowsPlan)
   }
+
+  /**
+   * Variant of `buildDeletesAndInserts` for the `SupportsColumnUpdates` 
narrow-scan path.
+   * This variant realigns the assignments to one value per surviving rowAttr 
padding unassigned
+   * rowAttrs with identity, so the reinsert output arity matches the delete 
output arity in
+   * the resulting Expand.
+   */
+  private def buildNarrowDeletesAndInserts(
+      matchedRowsPlan: LogicalPlan,
+      assignments: Seq[Assignment],
+      rowIdAttrs: Seq[Attribute]): Expand = {
+
+    val (metadataAttrs, rowAttrs) = matchedRowsPlan.output.partition { attr =>
+      MetadataAttribute.isValid(attr.metadata)
+    }
+    val assignmentMap = AttributeMap(assignments.collect {
+      case a @ Assignment(key: Attribute, _) => key -> a
+    })
+    val reinsertAssignments = rowAttrs.map { attr =>
+      assignmentMap.get(attr) match {
+        case Some(a) => a
+        case None => Assignment(attr, attr)
+      }
+    }
+    val deleteOutput = deltaDeleteOutput(rowAttrs, rowIdAttrs, metadataAttrs)
+    val insertOutput = deltaReinsertOutput(reinsertAssignments, metadataAttrs)
+    val outputs = Seq(deleteOutput, insertOutput)
+    val operationTypeAttr = AttributeReference(OPERATION_COLUMN, IntegerType, 
nullable = false)()
+    val attrs = operationTypeAttr +: matchedRowsPlan.output
+    val expandOutput = generateExpandOutput(attrs, outputs)
+    Expand(outputs, expandOutput, matchedRowsPlan)
+  }
+
+  /**
+   * Resolves the connector's `requiredDataAttributes()` if the operation opts 
into column
+   * updates. Returns `Nil` otherwise.
+   */
+  private def resolveConnectorDataAttrs(
+      relation: DataSourceV2Relation,
+      operation: RowLevelOperation): Seq[AttributeReference] = operation match 
{
+    case scu: SupportsColumnUpdates => resolveRequiredDataAttrs(relation, scu)
+    case _ => Nil
+  }
+
+  /**
+   * Computes the narrow set of data columns that must be present in the scan 
for a column-update
+   * write: connector-declared attrs, unioned with any table columns 
referenced by non-identity
+   * assignment RHS expressions, the operation condition, and the table's 
partition expressions.
+   * Partition-column refs are always kept so downstream rules 
(V2ScanPartitioningAndOrdering,
+   * GroupBasedRowLevelOperationScanPlanning) can resolve the table's 
partitioning expressions
+   * against the scan output.
+   */
+  private def computeNarrowReadAttrs(
+      relation: DataSourceV2Relation,
+      connectorDataAttrs: Seq[AttributeReference],
+      assignments: Seq[Assignment],
+      cond: Expression): Seq[AttributeReference] = {
+    val relationSet = relation.outputSet
+    val nonIdentityRhsRefs = assignments.iterator
+      .filterNot(a => a.key.isInstanceOf[Attribute] &&
+        isIdentityAssignment(a.key.asInstanceOf[Attribute], a.value))
+      .flatMap(_.value.references.toSeq)
+      .toSeq
+    val extraRefs = (cond.references.toSeq ++ nonIdentityRhsRefs)
+      .collect { case a: AttributeReference => a }
+      .filter(relationSet.contains)
+    val partitionRefNames = relation.table.partitioning().toImmutableArraySeq

Review Comment:
   **Finding 9.** I hit the concrete failure this thread is about, so here it 
is with numbers, plus my read on the three options.
   
   A `SupportsDelta` + `SupportsColumnUpdates` connector declaring 
`requiredDataAttributes() = [pk, id]` on a table partitioned by `dep`, whose 
write requires `Distributions.clustered(dep)` and an ordering on `dep`:
   
   ```sql
   UPDATE t SET id = -1 WHERE pk = 1
   ```
   
   fails at `V2Writes` with
   
   ```
   AnalysisException: Unable to resolve dep given 
[__row_operation,id,pk,_partition,index,pk]
     at 
DistributionAndOrderingUtils$.prepareQuery(DistributionAndOrderingUtils.scala:47)
     at V2Writes$$anonfun$apply$1.applyOrElse(V2Writes.scala:126)
   ```
   
   The same connector on the CoW path resolves fine. The asymmetry is that 
`buildNarrowReplaceDataUpdateProjection` maps over all of `plan.output`, so the 
partition columns stay in the ReplaceData write query, while 
`buildColumnUpdateProjection` emits only `assignedValues ++ 
connectorPassThroughValues ++ metadata ++ rowIds`, so they are gone from the 
WriteDelta query. `computeNarrowReadAttrs`' comment says the partition refs are 
kept "so downstream rules ... can resolve the table's partitioning expressions 
against the scan output" -- that holds for the scan, but the delta write query 
then drops them again, which is exactly the hole you describe.
   
   Worth noting for whichever option you pick: `prepareQuery` resolves against 
`query.output`, and `updateRowProjection` selects the payload out of that query 
*by name*. So a column can sit in the write query without entering the write 
payload -- no `updateSchema()` change needed. That means option 1 is 
implementable by adding the scan-only attrs to the `Project` list in 
`buildColumnUpdateProjection` and leaving `updateRowAttrs = connectorDataAttrs` 
alone, which is what the CoW path effectively already does.
   
   On the options:
   
   - **Option 1 (`scanOnlyDataAttributes()`)** -- my preference. It is the only 
one that names the actual distinction (needed for planning, not for the write 
row), and it keeps `updateSchema()` meaning "the columns being written".
   - **Option 2 (fold into `requiredDataAttributes()`)** -- 
`requiredDataAttributes()` *is* the write-row schema today: `updateSchema()` is 
exactly that list, and `ReplaceData`/`WriteDelta.dataAttrsResolved` validates 
the projection against it position for position. Folding scan-only columns in 
would push them into the payload and make `updateSchema()` stop meaning that, 
which also silently re-introduces the reconstruct-or-lose problem for those 
columns.
   - **Option 3 (widen `requiredMetadataAttributes()`)** -- those are validated 
against `projectedMetadataAttrs` and delivered through the metadata row, so 
data columns would arrive in the metadata slot. That is a larger contract 
change than option 1 for a smaller gain.
   
   Whichever way it goes, the two paths should end up agreeing -- right now CoW 
carries the columns and delta does not, and nothing in the tests catches the 
difference because every test connector clusters on the `_partition` metadata 
column rather than a data column.
   
   Small aside in the same method: `pk` appears twice in that projection 
(`connectorPassThroughValues` and `rowIdValues` both emit it, hence the 
duplicate in the output list above). It is harmless -- `findColOrdinal` takes 
the first and both share an `exprId`, so resolution does not go ambiguous -- 
but the second one is dead and could be filtered out.
   



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