Repository navigation
Conversation
…tNNonNulls AtLeastNNonNulls, and the null short-circuit of function arguments, refuse to skip an argument that holds a subexpression Spark shares between the operator's expressions. Spark evaluates a shared subexpression for every row, so skipping one could hide an error that Spark raises. The check applied to every shared subexpression, so a filter whose columns merely read the same struct field or take the same substring, such as na.drop over columns derived from one input, fell back to Spark, with a transition on each side. Only a shared subexpression that can raise needs to be kept. canSkipEvaluation now leaves out those made of expressions that cannot fail whatever the row holds: literals and columns, struct field reads, comparisons and boolean operators, string functions such as substr and upper, and casts and numeric arithmetic outside ANSI mode. The list fails closed, so anything it does not name, such as element_at, an ANSI cast, interval arithmetic or a decimal expression after promotion, still counts as able to raise.
927cba9 to
ce1feae
Compare
comphead
left a comment
There was a problem hiding this comment.
Thanks, the fail-closed list reads well and the tests pin both directions. One gap in the Cast arm.
| case _: Length | _: Upper | _: Lower | _: Substring | _: StartsWith | _: EndsWith | | ||
| _: Contains => | ||
| true | ||
| case c: Cast => evalMode(c) != CometEvalMode.ANSI |
There was a problem hiding this comment.
A non-ANSI cast can still raise. Cast from DATE to TIMESTAMP or TIMESTAMP_NTZ goes through daysToMicros and instantToMicros, which use Math.multiplyExact, so a date past about year 294247 (dates go up to 5881580) throws ArithmeticException in every eval mode. With such a cast shared between na.drop children, Spark raises on every row while native evaluation would skip it. The comment on keyCastCannotFail in CometCast.scala already calls out these date and timestamp conversions. Could this arm also exclude casts to or from date, timestamp and interval types, so the motivating string to int cast keeps the skip and the list stays closed for the conversions that overflow? I have not run it.
Which issue does this PR close?
N/A. Follow-up to #6180 (native
AtLeastNNonNulls).Rationale for this change
AtLeastNNonNulls(whatdf.na.drop(n)plans) falls back to Spark withAtLeastNNonNulls would skip a subexpression that Spark evaluates for every rowwhenever two of its children share a subexpression, even when that subexpression cannot fail. A common case isna.dropover columns derived from one input:The filter runs on Spark, with a transition on each side, although every expression in it is native.
The guard exists because Spark evaluates a shared subexpression for every row, so a shared expression that fails (an ANSI cast of a malformed string, say) raises even on a row where
AtLeastNNonNullshas already reached n. Native evaluation that skips the later children would not raise. If the shared subexpression cannot fail, skipping it shows nothing, so the guard has no reason to apply. The same check (canSkipEvaluation) also decides the null short-circuit of function arguments, so this applies to them too.What changes are included in this PR?
SharedSubexpressions.commonno longer contains a shared subexpression that cannot raise. A newcannotRaisecheck lists what cannot fail whatever the row holds: literals and column references, struct field reads, comparisons and boolean operators,coalesce,ifandCASE,substr,upper,lower,length,startswith,endswithandcontains, and casts and numeric arithmetic outside ANSI mode. Under ANSI mode, which is the default from Spark 4, a cast or arithmetic that can fail still counts as able to raise, so the filter above keeps falling back there.element_at(the index 0 raises in every mode), an ANSI cast or arithmetic, interval arithmetic (it raises on overflow in every mode), and a decimal expression after promotion (which wraps it inCheckOverflow).QueryPlanSerdeSuitetests that used a string-to-int cast as the shared expression now useelement_at(arr, 0), which raises in every mode, so they still pin the mechanism. The cast is no longer a case where the guard applies outside ANSI mode.How are these changes tested?
QueryPlanSerdeSuite: a sharedlengthand a shared struct field can be skipped. A sharedelement_at(arr, 0), a shared interval addition, and an expression that cannot raise but holds a raising one still block the skip, also when other shared subexpressions that cannot raise are present.CometExpressionSuite: twoAtLeastNNonNullsqueries that shareupper(s), and a struct field, with subexpression elimination on. Both now run natively and match Spark. The two existing tests, which share an ANSI cast that fails and an ANSI divide by zero, still fall back and still raise.CometSqlFileTestSuite(769 tests) and theAtLeastNNonNullstests ofCometExpressionSuite(8 tests) pass on Spark 3.5.QueryPlanSerdeSuiteand theAtLeastNNonNullstests also pass on Spark 3.4, and the main and test code compile on Spark 3.4, 4.0 and the default profile. I have not run the tests on 4.x.Filteron currentmainand as aCometFilterwith this change.Related
#7322 has a similar fail-closed check, private to
CometArrayFilter, for whether a lambda body can raise. If both land they can share one classifier; I kept them apart so neither PR waits on the other.