What is the problem the feature request solves?
QueryPlanSerde honours spark.comet.expression.<Name>.enabled=false as forced fallback, and there are 293 expressions registered to it:
grep -oE "classOf\[[A-Za-z0-9_]+\] ->" spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala | sort -u | wc -l
That gives a free invariant, with no product change needed: for any expression Comet rates compatible, a query's outcome — its rows, or the error it raises — must be identical whether the expression is evaluated natively or forced back to Spark.
Four distinct tests exercise that lever today, each pinning one hand-picked expression: disable expression using dynamic config (Add) in CometExpressionSuite, plus the ToPrettyString, WidthBucket and StringDecode suites. All four do check the answer, via checkSparkAnswerAndOperator on the native leg and checkSparkAnswerAndFallbackReason on the forced leg. What is missing is a mechanical sweep across the registry, and any systematic check that the two legs agree on which error they raise.
The evidence, and its limits
To test whether an invariance sweep of this shape has teeth, I reapplied the pre-fix condition from #5218 — dropping the inputOrdinals.size == 1 || rootChildrenAreLeaves(expr) narrowing from CometBatchKernelCodegen.canShortCircuitNulls, so the null short-circuit again applies to a whole tree — and ran a set of detectors against that one probe on one build:
| Detector |
Result |
CometFuzzMathSuite |
30/30 pass — miss |
CometCodegenFuzzSuite |
28/28 pass — miss |
CometCodegenHOFSuite |
5/5 pass — miss |
org.apache.comet.rules.CometExecRuleSuite |
29/29 pass — miss |
CometCodegenSuite |
1 failure — the (#5218) regression guard |
CometCodegenSourceSuite |
2 failures — both (#5218) guards |
| proposed suite |
fails: AddMonths default=VALUE(2024-02-01) forced=THREW(CAST_INVALID_INPUT) |
Two honest caveats on that table, stated up front. The three suites that catch the probe are #5218's own regression tests, shipped with its fix — their failure shows the probe is genuinely behaviour-changing, and nothing more. And my corpus contains AddMonths because #5218 named it, so the last row is a validation replay, not an independent rediscovery: CometCodegenSuite's guard already covers that same query end-to-end. What the table does support is narrower — four generic mechanical detectors miss this class, including a fuzzer aimed squarely at codegen, while a fallback-invariance comparison surfaces it with a named witness. The comparator is not specific to add_months; it would flag this shape for any expression in the corpus.
The oracle also differs from the existing guard's. CometCodegenSuite's test compares Comet against Spark directly. This suite compares Comet-native against Comet-with-that-expression-forced-to-Spark, which is the config lever no current test sweeps.
Same build with the probe reverted: 24 pass / 0 fail / 1 skipped-vacuous — the single delta is the witness above.
Describe the solution
A deterministic scalatest suite, no product code, over the existing CometFuzzTestBase fixture. Per expression it runs one fixed query twice — default config, then .enabled=false — and:
- gates every comparison on evidence the flip actually moved execution. The default leg must show Comet operators and no Spark
ProjectExec; the forced leg must show a Spark ProjectExec. A plan that shows neither proves nothing and is reported SKIPPED-VACUOUS, never counted as a pass.
- compares three-valued outcomes (
Rows vs Threw(errorClass)), so a value on one leg against an exception on the other is a failure with a witness rather than a harness error. Differing error classes are also a failure.
- treats a divergence on an incompatible-rated expression as
EXCUSED — logged, never a pass.
- canonicalises NaN,
-0.0 and NULL to distinct tokens, and row order by sorting.
25 expressions × the base suite's 3 shuffle/C2R variants = 75 comparisons in ~10 s, so it is per-PR viable. Compiles and passes under spark-3.4, spark-3.5 and spark-4.1.
Current scope is deliberately 25 of 293 expressions. Widening is mechanical, and the bind gate is what keeps the coverage claim honest as it grows — without it a sweep like this silently overstates itself, which happened three times while this one was being built.
Related work I checked first
Additional context
This came out of a systematic, AI-assisted audit of Comet's verification machinery, human-verified at each step and mutation-tested with positive controls. Given #4085's note that LLM-assisted audits have outperformed fuzzing here, the suite is built to that lesson: fast, deterministic, named witnesses. Happy to share the full evidence trail, including the negative results — the audit also produced two candidate findings about the config lever that dissolved on verification, and those are not being filed.
PR attached.
What is the problem the feature request solves?
QueryPlanSerdehonoursspark.comet.expression.<Name>.enabled=falseas forced fallback, and there are 293 expressions registered to it:That gives a free invariant, with no product change needed: for any expression Comet rates compatible, a query's outcome — its rows, or the error it raises — must be identical whether the expression is evaluated natively or forced back to Spark.
Four distinct tests exercise that lever today, each pinning one hand-picked expression:
disable expression using dynamic config(Add) inCometExpressionSuite, plus theToPrettyString,WidthBucketandStringDecodesuites. All four do check the answer, viacheckSparkAnswerAndOperatoron the native leg andcheckSparkAnswerAndFallbackReasonon the forced leg. What is missing is a mechanical sweep across the registry, and any systematic check that the two legs agree on which error they raise.The evidence, and its limits
To test whether an invariance sweep of this shape has teeth, I reapplied the pre-fix condition from #5218 — dropping the
inputOrdinals.size == 1 || rootChildrenAreLeaves(expr)narrowing fromCometBatchKernelCodegen.canShortCircuitNulls, so the null short-circuit again applies to a whole tree — and ran a set of detectors against that one probe on one build:CometFuzzMathSuiteCometCodegenFuzzSuiteCometCodegenHOFSuiteorg.apache.comet.rules.CometExecRuleSuiteCometCodegenSuite(#5218)regression guardCometCodegenSourceSuite(#5218)guardsAddMonths default=VALUE(2024-02-01) forced=THREW(CAST_INVALID_INPUT)Two honest caveats on that table, stated up front. The three suites that catch the probe are #5218's own regression tests, shipped with its fix — their failure shows the probe is genuinely behaviour-changing, and nothing more. And my corpus contains
AddMonthsbecause #5218 named it, so the last row is a validation replay, not an independent rediscovery:CometCodegenSuite's guard already covers that same query end-to-end. What the table does support is narrower — four generic mechanical detectors miss this class, including a fuzzer aimed squarely at codegen, while a fallback-invariance comparison surfaces it with a named witness. The comparator is not specific toadd_months; it would flag this shape for any expression in the corpus.The oracle also differs from the existing guard's.
CometCodegenSuite's test compares Comet against Spark directly. This suite compares Comet-native against Comet-with-that-expression-forced-to-Spark, which is the config lever no current test sweeps.Same build with the probe reverted: 24 pass / 0 fail / 1 skipped-vacuous — the single delta is the witness above.
Describe the solution
A deterministic scalatest suite, no product code, over the existing
CometFuzzTestBasefixture. Per expression it runs one fixed query twice — default config, then.enabled=false— and:ProjectExec; the forced leg must show a SparkProjectExec. A plan that shows neither proves nothing and is reportedSKIPPED-VACUOUS, never counted as a pass.RowsvsThrew(errorClass)), so a value on one leg against an exception on the other is a failure with a witness rather than a harness error. Differing error classes are also a failure.EXCUSED— logged, never a pass.-0.0and NULL to distinct tokens, and row order by sorting.25 expressions × the base suite's 3 shuffle/C2R variants = 75 comparisons in ~10 s, so it is per-PR viable. Compiles and passes under
spark-3.4,spark-3.5andspark-4.1.Current scope is deliberately 25 of 293 expressions. Widening is mechanical, and the bind gate is what keeps the coverage claim honest as it grows — without it a sweep like this silently overstates itself, which happened three times while this one was being built.
Related work I checked first
StringTranslateis incompatible-rated, so by default it never runs natively and there is no native leg to compare against. Whether an expression is reachable natively at all is a dispatcher question, and the bind gate makes that visible instead of silently counting it as verified.ProjectExec. That work is default-off today, so there is no conflict now — but the bind gate is the part that would need updating alongside it, and it would report SKIPPED-VACUOUS rather than passing silently if the shape changed underneath it.Additional context
This came out of a systematic, AI-assisted audit of Comet's verification machinery, human-verified at each step and mutation-tested with positive controls. Given #4085's note that LLM-assisted audits have outperformed fuzzing here, the suite is built to that lesson: fast, deterministic, named witnesses. Happy to share the full evidence trail, including the negative results — the audit also produced two candidate findings about the config lever that dissolved on verification, and those are not being filed.
PR attached.