Skip to content

Math expression audit follow-ups (from #4486) #4500

Closed

Description

@andygrove

Tracking issue for follow-up work surfaced by the math expression audit in #4486. Each item below is either a support-level / serde correctness fix that the audit deliberately deferred, or a coverage gap the audit documented but did not implement.

High priority

1. Lift convert-time withInfo fallbacks in arithmetic.scala into getSupportLevel

CometAdd, CometSubtract, CometMultiply, CometDivide, CometIntegralDivide, CometRemainder, CometUnaryMinus, and CometRound (spark/src/main/scala/org/apache/comet/serde/arithmetic.scala:94-271, 300-321, 355) currently bail out from convert with withInfo(...) + return None for cases that are knowable from the expression alone (unsupported datatype, EvalMode.TRY on Remainder, negative-scale Decimal on Round, Float / Double on Round, etc.).

The audit-comet-expression skill requires expression-shape restrictions to be declared as Unsupported(Some(reason)) / Incompatible(Some(reason)) branches in getSupportLevel so that:

  • EXPLAIN surfaces the reason at planning time
  • the auto-generated compatibility doc picks them up
  • the dispatcher can route around them

This was deferred from #4486 because arithmetic.scala is load-bearing and the fix is mechanical but wide.

2. UnaryPositive has no serde on Spark 3.4 / 3.5

Projections containing +col silently disable Comet for the enclosing projection on Spark 3.4 / 3.5 because there is no CometUnaryPositive registration. On Spark 4.0+ UnaryPositive is RuntimeReplaceable and the optimizer removes it before serde, so the gap is transparent there. Wire a passthrough serde for the 3.4 / 3.5 shim path so the projection stays native.

3. hex / unhex Spark 4.x collation propagation

Spark 4.x widens Hex / Unhex input types to StringTypeWithCollation and preserves collation in dataType. CometHex / CometUnhex pass expr.dataType to the native SparkHex / spark_unhex UDFs, which always return Utf8, so collation does not propagate. Per the audit skill, the support level should be Incompatible(Some(reason)) for non-default collations rather than the current Compatible. Cross-reference #2190 / #4496.

Medium priority (robustness / coverage)

4. greatest / least do not gate input types

Registered as bare CometScalarFunction(\"greatest\") / CometScalarFunction(\"least\") in QueryPlanSerde.scala. Spark accepts interval inputs (and a few Spark-specific orderings) that the native UDF may not handle; there is no explicit fallback path, so a runtime error from the native UDF is the failure mode. Add an input-type guard that falls back to Spark for the unsupported cases.

5. ceil(expr, scale) / floor(expr, scale) (RoundCeil / RoundFloor) not wired

The two-argument forms always fall back to Spark. Wire them via the same path as Round.

6. BRound (HALF_EVEN rounding) not wired

Listed as [ ] bround in the support doc. Banker's rounding is independent of the BigDecimal-via-toString issue that blocks Float / Double Round, so the integer / decimal paths could be supported.

7. signum rejects interval inputs

Spark Signum accepts DayTimeIntervalType / YearMonthIntervalType via inputTypes; Comet only handles DoubleType. Either add an explicit fallback or extend the native path.


Surfaced by the audit-comet-expression skill run in #4486.

Activity

  1. added this to the 0.17.0 milestone on May 28, 2026
  2. added
    priority:mediumFunctional bugs, performance regressions, broken features
    and removed on Jun 1, 2026
  3. modified the milestones: 0.17.0, 1.0.0 on Jun 25, 2026
  4. self-assigned this
    on Jun 29, 2026
  5. andygrove commented on Jun 29, 2026

    @andygrove
    MemberAuthor

    Item #3 (hex/unhex collation) — investigated, no change needed.

    Hex output is restricted to the fixed ASCII alphabet 0-9A-F, so its values are collation-invariant: identical hex strings compare equal and group identically under every collation, and the small uppercase alphabet is not reordered by realistic locale collations. Unhex returns BinaryType, which carries no collation at all.

    The only way collation could matter is a collation-aware operation consuming the hex result (e.g. WHERE hex(x) = 'ab', ORDER BY hex(x)). Comet already declines to evaluate collation-aware string comparisons/sorts natively (QueryPlanSerde.scala:1051), so those fall back to Spark and produce correct results. The hex projection still runs natively.

    Net: Comet cannot produce a collation-related wrong answer for hex/unhex today, so they correctly stay Compatible. The original audit flag was a blanket-policy concern, not a defect. (Contrast concat/reverse, which are gated for the same theoretical reason and are arguably over-gated.)

  6. andygrove commented on Jun 29, 2026

    @andygrove
    MemberAuthor

    Item #2 (UnaryPositive serde on Spark 3.4 / 3.5) — investigated, no change needed.

    The premise that +col reaches serde on Spark 3.4 / 3.5 does not hold. The default optimizer batch includes RemoveDispensableExpressions, which unconditionally rewrites UnaryPositive(child) => child (sql/catalyst/.../optimizer/expressions.scala), so UnaryPositive is stripped before Comet ever sees the plan. This mirrors the Spark 4.0+ path where it is RuntimeReplaceable and removed by the optimizer.

    Verified empirically: SELECT +_1, +_2, +_3, +_4 FROM tbl runs fully natively on Spark 3.5 with no code change, even with spark.comet.exec.scalaUDF.codegen.enabled=false. The enclosing projection stays native because the + is already gone by serde time.

    Consequently the CometUnaryPositive codegen-dispatch registration added in #4538 is effectively dead code in normal queries: it is only reachable if a user disables RemoveDispensableExpressions via spark.sql.optimizer.excludedRules, and even then only when the codegen dispatcher is enabled. No correctness gap exists, so no serde change is required.

  7. andygrove commented on Jun 29, 2026

    @andygrove
    MemberAuthor

    Item #4 (greatest / least input-type gating) — spun out to #4756.

    Investigation found no live defect: on Spark 3.5 and 4.0, every Spark-accepted input type either runs natively and matches Spark (numerics incl. NaN, decimal, string, date, timestamp, binary, arrays, structs, null-only) or falls back gracefully (intervals, collated strings). The issue's proposed scalar-only gating would regress the array / struct / decimal cases that work today.

    Deferred to #4756 to revisit once the in-flight native interval-support PRs land (intervals currently fall back via non-serializable types; that changes once they serialize), and to add regression tests for the verified behavior.

  8. andygrove commented on Jun 29, 2026

    @andygrove
    MemberAuthor

    Items #5 (ceil/floor with scale = RoundCeil/RoundFloor) and #6 (BRound) — investigated together; no correctness defect, but native support is blocked on adding rounding modes to spark_round.

    Verified current behavior on Spark 3.5 (parquet-backed columns so the projection can run natively):

    Expression Native today Matches Spark
    ceil(x, scale) / floor(x, scale) (RoundCeil/RoundFloor) no — always falls back to Spark yes
    bround(x, scale) (BRound, HALF_EVEN) yes, via CometCodegenDispatch (codegen flag on, the default) yes
    bround(...) with spark.comet.exec.scalaUDF.codegen.enabled=false no — falls back yes

    Neither is a correctness defect: RoundCeil/RoundFloor fall back to Spark, and BRound is handled correctly by the codegen dispatcher (which runs Spark's own doGenCode in a native kernel).

    Blocker for true-native support. The native spark_round (native/spark-expr/src/math_funcs/round.rs) implements HALF_UP only (the integer/decimal macros round half-away-from-zero; floats delegate to DataFusion's RoundFunc, also HALF_UP). So "wire via the same path as Round" is not viable as written — that path would silently apply HALF_UP and produce wrong bround/ceil/floor results. Native support requires adding HALF_EVEN / CEILING / FLOOR modes to spark_round for the integer and decimal paths. Float/Double would stay Unsupported, same BigDecimal-via-toString reason that already blocks Round on float/double (as #6 notes).

    Nuance for #6. BRound via codegen dispatch currently covers all numeric types incl. float/double. A pure-native int/decimal impl would gate float/double to fallback unless codegen dispatch is retained for those, i.e. a partial coverage regression for the codegen-on case.

    Unified approach when this is picked up. One native change serves both items: add a RoundingMode parameter (HALF_UP / HALF_EVEN / CEILING / FLOOR) to spark_round for int+decimal, thread it through proto, then add CometRoundCeil / CometRoundFloor serdes and optionally switch CometBRound to native, all gating float/double + negative-scale decimal as Unsupported to mirror CometRound.

    Net: #5 is a clean coverage win (zero native accel today); #6 is marginal (already correct via codegen dispatch). Left as-is for now per investigation; no code change.

  9. andygrove commented on Jun 29, 2026

    @andygrove
    MemberAuthor

    Item #7 (signum rejects interval inputs) — investigated; no live defect, folded into #4756.

    Probed on Spark 3.5: signum(double) and signum(int) run natively and match Spark; day-time / year-month interval inputs fall back gracefully (intervals do not serialize), so there is no runtime error today.

    Like greatest / least (item #4), this only becomes relevant once the in-flight native interval-support PRs land, at which point signum(interval) will serialize and reach the native UDF (which currently handles only double). Added to #4756 as a deferred item: once intervals serialize, either extend the native signum path to take the sign of the interval's underlying integer representation (months / micros) or add an explicit getSupportLevel gate.

  10. andygrove commented on Jun 29, 2026

    @andygrove
    MemberAuthor

    Closing summary

    All seven follow-up items have been investigated. None turned out to be a true correctness defect; closing the tracker accordingly. Each item was probed empirically (Comet vs Spark, native-vs-fallback) before reaching a conclusion.

    Item Outcome
    1. Lift arithmetic.scala convert-time fallbacks into getSupportLevel Already done by #4674 (and #4615 for try_mod). The one residual, CometUnaryMinus, is addressed in #4759.
    2. UnaryPositive serde on Spark 3.4 / 3.5 Non-issue: RemoveDispensableExpressions removes UnaryPositive before serde on all supported versions, so +col projections already stay native.
    3. hex / unhex collation Non-issue: hex output is collation-invariant (0-9A-F); unhex returns BinaryType. Any collation-aware downstream op already falls back.
    4. greatest / least input gating No live defect (intervals/collated strings fall back gracefully; arrays/structs/NaN run natively and match Spark). Scalar-only gating would regress working cases. Deferred to #4756.
    5. ceil / floor with scale (RoundCeil/RoundFloor) Not a defect (falls back to Spark). True-native support needs CEILING/FLOOR modes added to spark_round (currently HALF_UP only).
    6. BRound (HALF_EVEN) Not a defect (correct via codegen dispatcher). True-native support needs HALF_EVEN in spark_round.
    7. signum interval inputs No live defect (intervals fall back gracefully). Deferred to #4756, to be handled when native interval support lands.

    Follow-ups carried forward

    Closing as there are no true correctness issues to track here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions