-
Notifications
You must be signed in to change notification settings - Fork 361
fix: ignore structural tags when lifting expression coverage #5471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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._ | ||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change routes
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in 95c744c. Added |
||
|
|
||
| withSQLConf( | ||
| CometConf.COMET_EXTENDED_EXPLAIN_FORMAT.key -> | ||
| CometConf.COMET_EXTENDED_EXPLAIN_FORMAT_VERBOSE, | ||
|
|
@@ -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) | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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
isStructuralExprat line 1002 never lets the serde tag anAttribute,BoundReference, orLiteralin the first place, soCometExplainInfo.isNeverTaggedcan 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. CouldisNeverTaggedbe derived fromisStructuralExprminusAlias, 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.There was a problem hiding this comment.
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
isStructuralExprandisNeverTagged: the read filter is the write-side structural set minusAlias, because rewritten-child coverage is lifted onto the original alias. I also narrowed the wording to coverage/info tags and explicitly excludedFALLBACK_REASONSfrom that invariant; literals can legitimately carry those reasons.