expr: render an unbounded OutOfDomain instead of panicking - #37974
Conversation
|
We previously had #37049, which had to be reverted! I'm trying to be careful not to repeat this, while still fixing the correctness issues |
|
I'm not happy that this is causing known perf. regressions, drafting for now. Edit: Done |
041b411 to
72b1607
Compare
|
I'm not sure if I would consider the AND/OR thing a bug, because we (and Postgres) doesn't really have defined evaluation order for AND/OR. Note that Postgres does promise error guarding for certain constructs (e.g. (Btw. in the long run, we might be able to have a defined evaluation order for AND/OR that we can actually uphold without performance hits when we implement |
|
Thanks Gabor, I'll try to get the fuzzers to ignore this (again). Edit: A small source of panics remained, so left this PR open. |
`Display for EvalError` panicked on `OutOfDomain(None, None, _)`, a state no constructor produces: all four `EvalError::OutOfDomain` call sites set at least one bound. It is reachable by decoding a corrupted or forged `ProtoEvalError`, because `from_proto` accepts any pair of present `ProtoDomainLimit`s, and `DomainLimit::None` is a present field with an unbounded value. That path is live in clusterd. `DataflowErrorSer::Display` decodes error bytes read straight out of a persist shard and delegates to `EvalError::fmt`, and the index peek path renders the error-trace key. So one such error in a shard panics the dataflow, and re-panics on every retry. Render the degenerate domain instead. Rejecting it in `from_proto` would not help: `DataflowErrorSer::deserialize` expects the conversion to succeed, so the same bytes would panic a few frames earlier on the same path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MapFilterProject::optimize` may legally change what a row yields once lazy `And`/`Or` evaluation absorbs an operand error, in two ways. Reduce's error propagation folds the absorbed error into the plan (the open bug CLU-137), and memoization hoists a fallible subexpression shared under the `And`/`Or` into a mapped column that `SafeMfpPlan::evaluate_inner` runs eagerly, surfacing the error. The latter is not a bug: AND/OR evaluation order is undefined, in Materialize as in Postgres (STG-54), so the surfaced error is a legal outcome of the same plan. It is what failed release qualification 1330, 1332, 1334 and 1335, and after review #37974 no longer changes the product to prevent it, so the oracle has to tolerate it or release qualification keeps tripping on it. The coarse expression-level skip this target used drops the entire input whenever any `And`/`Or` has a statically fallible operand. That is 2.2% of MFPs, but they are exactly the AND/OR-over-fallible shapes where optimize's memoization is busiest, so the value and filter arms went blind where a real miscompile is most likely to sit. Port `mir_scalar_reduce`'s row-precise `absorbs_and_or_operand_error` instead and skip exactly the rows whose lazy evaluation absorbs an operand error. Temporal mode keeps the coarse skip: its reference side substitutes a literal for `mz_now()` and re-runs `optimize` per probed time, so a per-row answer is not well-defined there. Verified in both directions against the unfixed product. A hand-crafted input generating `((#2 * #2) <= (#2 * #2)) IS NULL OR #4` with an overflowing `#2` crashes the pre-existing oracle with the release qualification assertion and passes the row-precise one. A twin input hiding the shared fallible part in an untaken `If` branch still crashes the row-precise oracle when `eager_children`'s `If` guard is deliberately removed, so the tolerance does not extend to genuine eager-evaluation bugs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Display for EvalErrorpanicked on an unboundedOutOfDomain.OutOfDomain(None, None, _)is a state no constructor produces, since all fourcall sites set at least one bound. It is reachable by decoding a corrupted or
forged
ProtoEvalError, becausefrom_protoaccepts any pair of presentProtoDomainLimits andDomainLimit::Noneis a present field with an unboundedvalue.
That path is live in clusterd.
DataflowErrorSer::Displaydecodes error bytesread straight out of a persist shard and delegates to
EvalError::fmt, and theindex peek path renders the error-trace key, so one such error in a shard panics
the dataflow and re-panics on every retry. Rejecting the state in
from_protowould not help:
DataflowErrorSer::deserializeexpects the conversion tosucceed, so the same bytes would panic a few frames earlier on the same path.
Render the degenerate domain instead. Covered by a rendering test, and the
sharpened
eval_error_proto_roundtriporacle in #37979 crashes against theunfixed
Display.