peter-toth commented on code in PR #55518:
URL: https://github.com/apache/spark/pull/55518#discussion_r4071667376
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RewriteRowLevelCommand.scala:
##########
@@ -109,6 +123,43 @@ trait RewriteRowLevelCommand extends Rule[LogicalPlan] {
relation)
}
+ /**
+ * Resolves the connector-declared required data attributes for
column-update writes against
+ * the given relation. Non-existent columns are rejected with an
`AnalysisException`.
+ */
+ protected def resolveRequiredDataAttrs(
+ relation: DataSourceV2Relation,
+ operation: SupportsColumnUpdates): Seq[AttributeReference] = {
+ val refs = operation.requiredDataAttributes
+ if (refs.isEmpty) {
+ throw
QueryCompilationErrors.emptyRequiredDataAttributesError(operation.getClass.getName)
+ }
+ val nested = refs.filter(_.fieldNames.length !=
1).map(_.describe()).toImmutableArraySeq
+ if (nested.nonEmpty) {
+ throw QueryCompilationErrors.nestedRequiredDataAttributeError(
+ operation.getClass.getName, nested)
+ }
+ val normalizedNames = refs.map { ref =>
+ val name = ref.fieldNames.head
+ if (conf.caseSensitiveAnalysis) name else name.toLowerCase(Locale.ROOT)
+ }
+ val duplicates = normalizedNames.groupBy(identity).collect {
+ case (_, occurrences) if occurrences.length > 1 => occurrences.head
+ }.toSeq
Review Comment:
**Finding 32.** `occurrences.head` is the normalized name, not the declared
one, so with `spark.sql.caseSensitive` off a declaration of `["PK", "Pk"]`
reports `[pk]` — a spelling the connector never wrote. The nested-attribute
check nine lines up reports `_.describe()`, so the two messages disagree about
which spelling the connector author gets to see.
Keeping the first declared spelling:
```suggestion
val duplicates = refs.zip(normalizedNames).groupBy(_._2).collect {
case (_, occurrences) if occurrences.length > 1 =>
occurrences.head._1.describe()
}.toSeq
```
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RewriteUpdateTable.scala:
##########
@@ -228,4 +382,169 @@ 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, validating that every assigned column is covered. Returns `Nil`
otherwise.
+ */
+ private def resolveConnectorDataAttrs(
+ relation: DataSourceV2Relation,
+ operation: RowLevelOperation,
+ assignments: Seq[Assignment]): Seq[AttributeReference] = operation match
{
+ case scu: SupportsColumnUpdates =>
+ val attrs = resolveRequiredDataAttrs(relation, scu)
+ validateUpdatedColumnsSubset(scu, assignments, attrs)
+ attrs
+ case _ => Nil
+ }
+
+ /**
+ * Computes the narrow set of data columns that must be present in the scan
for a column-update
+ * write: connector-declared attrs (`requiredDataAttributes()`), unioned
with any table columns
+ * referenced by non-identity assignment RHS expressions, the operation
condition, and the
+ * table's CHECK constraints.
+ */
+ 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 checkConstraintAttrs = resolveCheckConstraintAttrs(relation)
+ dedupAttrs(connectorDataAttrs ++ extraRefs ++ checkConstraintAttrs)
+ }
+
+ /**
+ * Returns the relation attributes referenced by the table's CHECK
constraints, so that
+ * narrowing does not drop a column `ResolveTableConstraints` needs later to
re-validate the
+ * rewritten write query. Falls back to the full relation output whenever a
constraint's
+ * condition cannot be resolved to a `Predicate` (see `Check#predicate()`).
+ */
+ private def resolveCheckConstraintAttrs(
+ relation: DataSourceV2Relation): Seq[AttributeReference] = {
+ val checks =
Option(relation.table.constraints).getOrElse(Array.empty[Constraint]).collect {
+ case c: Check => c
+ }
+ if (checks.isEmpty) {
+ Nil
+ } else if (checks.exists(_.predicate() == null)) {
+ relation.output
+ } else {
+ val refs = checks.flatMap(_.predicate().references()).toSeq
+ V2ExpressionUtils.resolveRefs[AttributeReference](refs, relation)
Review Comment:
**Finding 31.** `Check#predicate().references()` returns references at full
depth, so a constraint on a nested field gives a two-part `FieldReference`.
`V2ExpressionUtils.resolveRef` puts that through `LogicalPlan.resolve`, which
returns an `Alias(GetStructField(...))` for a nested path, and its
`asInstanceOf[T]` is erased — so the `Alias` rides along inside a
`Seq[AttributeReference]` and blows up at the first frame that needs the real
type.
Measured on `9932f88`, delta MoR (`column-update`) and CoW
(`column-update-cow`), table `pk INT NOT NULL, id INT, dep STRING, s STRUCT<c1:
INT, c2: INT>` partitioned by `dep`:
```sql
ALTER TABLE t ADD CONSTRAINT positive_c1 CHECK (s.c1 > 0);
UPDATE t SET id = -1 WHERE pk = 1;
```
```
java.lang.ClassCastException: class
org.apache.spark.sql.catalyst.expressions.Alias cannot be cast to
class org.apache.spark.sql.catalyst.expressions.AttributeReference
at RewriteRowLevelCommand.dedupAttrs(RewriteRowLevelCommand.scala:107)
at RewriteUpdateTable$.computeNarrowReadAttrs(RewriteUpdateTable.scala:454)
```
Control: the same table, constraint and statement on the wide
`supports-deltas` connector passes, so this is specific to narrowing. A `CHECK`
on a *top-level* undeclared column is fine on all three of delta MoR, CoW and
the split path — I ran that too.
Narrowing is at root-column granularity anyway, so truncating each reference
to its root column is enough:
```suggestion
val refs = checks.flatMap(_.predicate().references()).toSeq
.map(ref => FieldReference(Seq(ref.fieldNames.head)))
V2ExpressionUtils.resolveRefs[AttributeReference](refs, relation)
```
Needs `import org.apache.spark.sql.connector.expressions.FieldReference`.
With that applied both probes pass and the two suites stay at 155. Worth a
nested-field-constraint test on both narrow paths.
--
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]