Persist pushdown hardening - #38110
Open
def- wants to merge 11 commits into
Open
Conversation
The Time, Timestamp, TimestampTz, Interval, and Uuid arms of col_values matched any FixedSize stats. from_bytes validates length only, and PackedNaiveDateTime, PackedInterval, and Uuid are all 16 bytes, so wrong-kind bytes decoded silently into garbage bounds. Not reachable today because a column's type cannot change under a reused name, but one schema evolution feature away from wrong bounds on old parts. Mismatched kinds now fall through to the catch-all arm and degrade to no stats. Also soften the Timestamp arm's two hard expects on the roundtrip through CheckedTimestamp to match the TimestampTz arm: malformed bytes should degrade, not panic the replica. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
The legacy Atomic arm of col_values used four hard expects on ProtoDatum decode: malformed V0 stats bytes panicked the replica at stats read time. It also never validated the decoded datum against the column type, and the V0 encoding carries no type tag, so a wrong-typed bound produced a range that excludes every value of the column's actual type, i.e. wrong results from corrupt legacy stats. Decode failures and type mismatches now degrade to no stats via soft errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
may_match_mfp treated an absent err column in the part stats as "no errors", while the storage read path's filter_result treats it as "may error". The err column is on the default force-keep list for stats trimming, but that list is configurable, so a trimmed part would have let the peek path (StatsCursor uses may_match_mfp) skip a part whose error rows must surface regardless of any filter. Align with filter_result and keep the part. The regression test builds a part with an error row, strips the err column's stats, and applies a filter no Ok row matches: the old code skipped the part. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
The shard_source filter and the stats accessors in fetch called LazyPartStats::decode, which panics on undecodable bytes, on stats that other processes wrote. Stats from a newer version can use a proto variant an older reader does not know, so a reader inside the upgrade window panicked in the pushdown filter instead of falling back to fetching the part, the fail-open behavior every other missing-stats case on this path already has. Decode failures now degrade to "no stats". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
ok_count had a hard expect on the err column's stats shape: corrupt or version-skewed durable stats panicked the replica at pushdown time. An unknown err count already fails open (callers keep the part), so degrade to that instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
apply_demand filters metadata by raw ColumnIndex but types by position, which only agree on dense descs. Assert that, so a future dropped-column desc fails loudly instead of attaching statistics and filter specs to the wrong columns. Document the two other invariants the residual-risk review found load-bearing but unwritten: name-keyed part stats, and diffs_sum substitution requiring registered batch bounds to match the blob. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
The pushdown audit fired only when the MFP produced output on an Ok row. Error rows from an audited part were emitted with no check, so a part discarded because its err stats undercount, the one violation class the err-count guard exists for, passed the audit silently. Mirror the audit into the error arm. No deterministic test exists for this: the audit only fires on an actual violation, which requires a live stats bug to construct through the runtime operator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
Corpus: add -NaN and -0.0 floats, jsonb maps and lists, truncation-edge strings, and a leap-second time to interesting_datums. Containment: check that stats-derived specs contain every datum for every scalar type, over datum pairs and full sets, after trim and trim_to_budget at every budget. End to end: wide-schema and multi-part audit proptests over real part stats with mz_now bounds and error rows, plus zero-column ReplaceWith and schema-drift cases, and regression tests for both PER-53 variants in pushdown.slt. Interpreter: grow the equivalence proptest vocabulary to 18 scalar types and ~150 declared-monotone functions, validating their monotonicity claims continuously. PROPTEST_CASES now overrides the built-in case counts for long runs, and a new coverage-guided pushdown_soundness cargo-fuzz target explores raw bit patterns beyond the corpus with the same soundness oracle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
range_lower maps empty and unbounded-lower ranges to NULL, which the interpreter's endpoint box cannot represent. The claim is sound only because those inputs form a downward-closed prefix of the range ordering, so valued endpoints imply no NULL-yielding interior. Record that argument at the declaration and pin the SQL results, verified empirically (range columns also collect no statistics today, so pushdown cannot prune on them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
err_count subtracted the ok count from the part length unchecked: err stats whose none count exceeds the part length (corrupt or version-skewed durable state, the same class the previous hardening covers) underflowed and panicked the replica. Report the err count as unknown instead, which callers treat as may-error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
Drop the duplicated value_between doc summary line, and document that the fuzz runner's interrupted classification treats a kernel OOM SIGKILL as interrupted too, relying on libFuzzer's rss limit to catch memory blowups as artifact-producing OOMs first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR
This was referenced Aug 7, 2026
def-
marked this pull request as ready for review
August 7, 2026 11:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Filter pushdown lets Materialize skip persist parts whose statistics prove no row can match a query's filter. A wrong skip is a silently wrong result, and a stats shape the decoder does not expect is a replica panic. This PR hardens the pipeline at every layer, stats collection, stats-to-range conversion, the interpreter's monotonicity contracts, and the runtime audit, and adds the test coverage that found the bugs.
Description
apply_demandrelies on, and document the name-keyed-stats and diffs_sum invariants along with whyrange_lower's monotone claim survives its NULLs.mz_nowbounds and error rows, interpreter monotonicity vocabulary grown to ~150 declared-monotone functions, a coverage-guidedpushdown_soundnesscargo-fuzz target, and slt regressions for every SQL-reachable fix.--.Verification
Every fix has a deterministic test verified red before the fix, unit or slt, except the audit error-row arm, where a test would require a live stats bug (explained in its commit message). Soaks: 16k-case interpreter equivalence runs over the full vocabulary, 131k-case wide e2e proptest, and a 160M-execution coverage-guided fuzz run, all clean. An analytic per-function proof pass of every
is_monotonedeclaration backs the vocabulary.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q5GiWioZM6CWrwWgfBADDR