Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions spark/src/main/scala/org/apache/comet/ExtendedExplainInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,8 @@ object CometExplainInfo {
}

/**
* Union of a `Set`-valued tag over `exprs`, skipping nodes the serde never tags.
* Union of a coverage or info tag over `exprs`, skipping nodes the serde never tags for those
* purposes. This filter must not be used for `FALLBACK_REASONS`, which literals can carry.
*
* Catalyst copies a rewritten node's tags onto its replacement (`TreeNode.copyTagsFrom`, which
* copies whenever the replacement has no tags of its own). Rewriting a tagged expression into a
Expand All @@ -328,10 +329,10 @@ object CometExplainInfo {
}

/**
* Nodes that never carry a Comet tag of their own, so anything found on one arrived by the
* copying described in [[collectExprTagValues]]. `Literal` is the node that matters, being the
* only one with JVM-wide singletons (`Literal.TrueLiteral`, `Literal.FalseLiteral`); the other
* two are listed because nothing legitimate can live on them either.
* Nodes that never carry their own coverage or info tags, so those tags can only arrive by the
* copying described in [[collectExprTagValues]]. This set must match
* `QueryPlanSerde.isStructuralExpr` minus `Alias`; changing either set requires checking the
* other. This invariant does not apply to `FALLBACK_REASONS`.
*
* `Alias` is deliberately absent even though the serde does not tag one directly:
* `QueryPlanSerde.liftCoverageTags` lands names on whichever node the operator holds, and for a
Expand Down
15 changes: 8 additions & 7 deletions spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
Original file line number Diff line number Diff line change
Expand Up @@ -855,14 +855,10 @@ object QueryPlanSerde extends Logging with CometExprShim with CometTypeShim {
}

private def liftCoverageTags(from: Expression, to: Expression): Unit = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes this filter safe is that isStructuralExpr at line 1002 never lets the serde tag an Attribute, BoundReference, or Literal in the first place, so CometExplainInfo.isNeverTagged can only ever discard copied tags. That invariant is now load-bearing for this fix, but the two lists live in different files under different names and neither comment mentions the other. Could isNeverTagged be derived from isStructuralExpr minus Alias, or failing that, could each comment name the other and state the subset relationship? Otherwise someone adding a node type to just one of them either reopens this path or quietly deletes real coverage.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 95c744c. Added reciprocal comments on isStructuralExpr and isNeverTagged: the read filter is the write-side structural set minus Alias, because rewritten-child coverage is lifted onto the original alias. I also narrowed the wording to coverage/info tags and explicitly excluded FALLBACK_REASONS from that invariant; literals can legitimately carry those reasons.

val native = mutable.Set.empty[String]
val dispatched = mutable.Set.empty[String]
from.foreach { e =>
e.getTagValue(CometExplainInfo.NATIVE_EXPRS).foreach(native ++= _)
e.getTagValue(CometExplainInfo.CODEGEN_DISPATCH_EXPRS).foreach(dispatched ++= _)
val exprs = from.collect { case e: Expression => e }
Seq(CometExplainInfo.NATIVE_EXPRS, CometExplainInfo.CODEGEN_DISPATCH_EXPRS).foreach { tag =>
appendTagValues(to, tag, CometExplainInfo.collectExprTagValues(exprs, tag))
}
appendTagValues(to, CometExplainInfo.NATIVE_EXPRS, native.toSet)
appendTagValues(to, CometExplainInfo.CODEGEN_DISPATCH_EXPRS, dispatched.toSet)
}

/**
Expand Down Expand Up @@ -1017,6 +1013,11 @@ object QueryPlanSerde extends Logging with CometExprShim with CometTypeShim {
* Nodes that carry no computation of their own. They are excluded from the expression coverage
* stats in extended explain because they appear in nearly every expression tree and would swamp
* the names a user actually cares about.
*
* `CometExplainInfo.isNeverTagged` must be this set minus `Alias`: the read-side filter retains
* aliases because [[liftCoverageTags]] uses them to hold names from rewritten children. Keep
* both sets in sync. This coverage invariant does not exclude structural nodes from carrying
* `FALLBACK_REASONS`.
*/
private def isStructuralExpr(expr: Expression): Boolean = expr match {
case _: Attribute | _: BoundReference | _: Literal | _: Alias => true
Expand Down
34 changes: 33 additions & 1 deletion spark/src/test/scala/org/apache/comet/CometCodegenSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import org.apache.arrow.vector._
import org.apache.spark.{SparkConf, SparkEnv, TaskContext}
import org.apache.spark.sql.CometTestBase
import org.apache.spark.sql.api.java.UDF1
import org.apache.spark.sql.catalyst.expressions.{BoundReference, CreateArray, CreateMap, CreateNamedStruct, Expression, Literal, MapConcat}
import org.apache.spark.sql.catalyst.expressions.{Add, Alias, AttributeReference, BoundReference, Cast, CreateArray, CreateMap, CreateNamedStruct, Expression, Hypot, Literal, MapConcat}
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types._
Expand All @@ -34,6 +34,7 @@ import org.apache.spark.unsafe.types.UTF8String
import org.apache.comet.CometSparkSessionExtensions.isSpark41Plus
import org.apache.comet.codegen.CometBatchKernelCodegen
import org.apache.comet.codegen.CometBatchKernelCodegen.ArrowColumnSpec
import org.apache.comet.serde.QueryPlanSerde
import org.apache.comet.udf.codegen.CometScalaUDFCodegen
import org.apache.comet.vector.CometVector

Expand Down Expand Up @@ -268,6 +269,22 @@ class CometCodegenSuite
}
}

test("codegen dispatch coverage survives the decimal promotion rewrite") {
val decimal = AttributeReference("amount", DecimalType(10, 2), nullable = false)()
val dispatched = Hypot(Cast(Add(decimal, decimal), DoubleType), Literal(4.0d))
val projection = Alias(dispatched, "value")()

// Promotion rebuilds Hypot as well as the Alias above it. Unlike the original Add, the
// dispatched copy is not reachable from the original tree, so only the coverage lift can
// bring its name back to the projection owner.
val proto = QueryPlanSerde.exprToProto(projection, Seq(decimal)).get
assert(proto.hasJvmScalarUdf)
assert(proto.getJvmScalarUdf.getClassName === classOf[CometScalaUDFCodegen].getName)
assert(dispatched.getTagValue(CometExplainInfo.DISPATCHED_SELF).isEmpty)
assert(dispatched.getTagValue(CometExplainInfo.CODEGEN_DISPATCH_EXPRS).isEmpty)
assert(projection.getTagValue(CometExplainInfo.CODEGEN_DISPATCH_EXPRS).contains(Set("hypot")))
}

test("tags copied onto the shared TrueLiteral do not leak into unrelated plans") {
// Catalyst copies a rewritten node's tags onto its replacement, so a tagged expression that an
// earlier query rewrote into `Literal.TrueLiteral` brands that process-wide singleton for the
Expand All @@ -278,7 +295,20 @@ class CometCodegenSuite
val planted = Literal.TrueLiteral
planted.setTagValue(CometExplainInfo.EXTENSION_INFO, Set("PLANTED_INFO"))
planted.setTagValue(CometExplainInfo.NATIVE_EXPRS, Set("plantedexpr"))
planted.setTagValue(CometExplainInfo.CODEGEN_DISPATCH_EXPRS, Set("planteddispatch"))
try {
// Decimal promotion rebuilds this projection. Its coverage lift must not copy the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to split this out into its own test? It asserts something quite different from the rest of the test it is in, which is about the dynamic-pruning plan not reporting the planted name, and the test name and comment only describe that second half. A separate test named for the lift, something like "the coverage lift ignores stale tags on the shared TrueLiteral", would point straight at the mechanism when it fails, and it would sit next to "expression coverage stats survive the decimal promotion rewrite", which is the positive case for the same code path.

Related question. Is there a way to drive this through a real plan the way the neighbouring tests do, or does planting on the singleton force the direct exprToProto call? I ask because this is the only place in these suites that builds a Catalyst tree by hand, so if a plan-level version is workable it would be more consistent and would cover the roll-up and the explain rendering too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 95c744c. I kept the seeded-singleton probe with its plan-level check so the two phases share one setup and guaranteed cleanup. The direct call pins the exact Literal.TrueLiteral identity and the original Alias; normal SQL optimization can change that shape. The neighboring decimal test already covers plan-level coverage, and the second half of this test covers DPP roll-up and extended explain. There are also existing direct Catalyst probes in this suite for BoundReference and map/array expressions.

// singleton's stale tags onto the Alias, which is a legitimate coverage owner.
val decimal = AttributeReference("amount", DecimalType(10, 2), nullable = false)()
val projection = Alias(
CreateNamedStruct(Seq(Literal("flag"), planted, Literal("sum"), Add(decimal, decimal))),
"value")()
assert(QueryPlanSerde.exprToProto(projection, Seq(decimal)).isDefined)
val native = projection.getTagValue(CometExplainInfo.NATIVE_EXPRS).getOrElse(Set.empty)
assert(native.contains("checkoverflow"), s"expected lifted decimal coverage, got: $native")
assert(!native.contains("plantedexpr"))
assert(projection.getTagValue(CometExplainInfo.CODEGEN_DISPATCH_EXPRS).isEmpty)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change routes CODEGEN_DISPATCH_EXPRS through the structural filter for the first time, and both of the new assertions for it are negative. Would it be worth adding a positive case, where an expression inside a promoted decimal tree really is routed through the JVM codegen dispatcher, checking that its name still reaches the original owner? Without one, a future change to isNeverTagged could drop genuine dispatch coverage across a decimal rewrite and these tests would still pass.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 95c744c. Added codegen dispatch coverage survives the decimal promotion rewrite. It places Hypot above a cast of decimal addition, so promotion rebuilds both Hypot and its alias. The test checks the emitted JvmScalarUdf dispatcher class, verifies that the original Hypot has neither the dispatch marker nor its coverage tag, and requires the original alias to receive hypot. This passed locally on Spark 4.1.3. As a mutation check, removing only liftCoverageTags(newExpr, expr) made this new assertion fail with None did not contain Set("hypot").


withSQLConf(
CometConf.COMET_EXTENDED_EXPLAIN_FORMAT.key ->
CometConf.COMET_EXTENDED_EXPLAIN_FORMAT_VERBOSE,
Expand All @@ -301,13 +331,15 @@ class CometCodegenSuite

val info = new ExtendedExplainInfo()
assert(!info.getNativeExpressions(plan).contains("plantedexpr"))
assert(!info.getCodegenDispatchExpressions(plan).contains("planteddispatch"))
val explain = info.generateExtendedInfo(plan)
assert(!explain.contains("PLANTED_INFO"), s"tag leaked into:\n$explain")
}
}
} finally {
planted.unsetTagValue(CometExplainInfo.EXTENSION_INFO)
planted.unsetTagValue(CometExplainInfo.NATIVE_EXPRS)
planted.unsetTagValue(CometExplainInfo.CODEGEN_DISPATCH_EXPRS)
}
}

Expand Down
Loading