Uh oh!
There was an error while loading. Please reload this page.
[fix](zonemap) Treat reversed zone map bounds as invalid - #67431
Conversation
hello-stephen
commented
Sep 2, 2026
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
csun5285
commented
Sep 2, 2026
/review |
There was a problem hiding this comment.
Review status: complete after two convergence rounds on head 625ecd3. I found two distinct issues, both left inline.
Critical checkpoints:
- Goal and proof: detecting reversed FLOAT/DOUBLE sentinel bounds is the right mechanism, and the new unit cases cover current writer output plus direct FLOAT/DOUBLE and nullable deserialization. The implementation does not yet safely meet the full compatibility goal because of the two inline cases.
- Scope and lifecycle: the change is focused. The per-writer page/reset/segment lifecycle, all finite/non-finite/null page orderings, and the merge-before-page-invalidation ordering were traced and are conservative. There is no shared-state concurrency, lock-order, static-initialization, ownership, or configuration concern.
- Parallel consumers: current-head classic predicates, expression zonemap filtering, delete predicates, predicate elimination, and pushed-down MIN/MAX all fence
pass_allbefore consuming invalid bounds. Released 4.1 readers do not have the segment statistics fence, which creates the mixed-version issue noted inline. - Persistence and compatibility: no FE/BE variable, EditLog, transaction, or storage-format schema field is added, but the new segment-level
pass_allwriter state is an incompatible use of the existing protobuf during rolling upgrades. - Tests and results: the added BE unit tests cover current metadata and several historical reversed encodings, but not the old-reader transition or flag-overridden
.16gbounds. Per the review-runner instruction, I did not build or execute tests; the live Clang Formatter check is passing. - Performance and observability: the legacy ignored-bound case can unnecessarily disable pruning for an otherwise sound historical map. No new metric or log is needed for the local validation logic.
User focus: no additional focus was supplied, so the full PR was reviewed.
Verdict: request changes for the rolling-upgrade correctness issue and the historical pruning regression.
Uh oh!
There was an error while loading. Please reload this page.
| if (_segment_zone_map.has_not_null && | ||
| is_reversed(_segment_zone_map.min_value, _segment_zone_map.max_value, | ||
| _data_type->get_storage_field_type())) { | ||
| _segment_zone_map.pass_all = true; |
There was a problem hiding this comment.
[P1] Keep segment metadata readable by older BEs
This starts writing segment-level pass_all=true, which makes to_proto() clear both bounds. Released 4.1 readers do not have the segment_zone_maps_can_answer_agg() fence added in #67341: they still select VStatisticsIterator for pushed-down MIN/MAX, skip parsing the cleared strings because pass_all is set, and then insert both bounds. For a non-nullable FLOAT/DOUBLE all-NaN (or single-infinity) segment, one side remains a default TYPE_NULL Field, so an older BE reading a segment produced by an upgraded BE can consume uninitialized storage and return a wrong aggregate during rolling upgrade. This is reachable through shared cloud segments and replica snapshot/clone. Please keep the legacy segment encoding until every supported reader fences pass_all, or version-gate this on-disk transition. The reader-side reversal check can still repair existing metadata, and page-level pass_all is already established.
NaN and infinity are recorded in the has_nan/has_positive_inf/ has_negative_inf flags and never move min/max, so a page holding nothing else leaves both at the values add_values() starts from: min = DBL_MAX and max = -DBL_MAX. Those are written out as if they were data, and the flags stand in for at most one side, so reading them back reports a DBL_MAX that is nowhere in the page -- SELECT MIN(v) answers with it while SELECT COUNT(*) WHERE v = that value answers 0. On data written before 4.0 there is no flag at all and both bounds are used, which prunes the page away: 25 rows of NaN return 0 for WHERE v > 5. The bounds round-trip exactly, so neither the flags nor the parse failure apache#67341 keys on can tell such a zone map from a sound one. Comparing against the values the writer starts from does not work either: 4.0 and 4.1 wrote them with digits10 + 1 digits, so a FLOAT page recorded 3.402823e+38, which no longer equals FLT_MAX but is still reversed. Mark the zone map pass_all when its bounds come back reversed, which only FLOAT and DOUBLE can do -- any value of any other type moves both bounds. Reversed is exactly the state of a page that held no finite value, so the test walks all 127 non-empty subsets of {NaN, +inf, -inf, max, lowest, 1.5, 20.5} through the writer and checks that each zone map either reports itself unusable or covers every value the page holds, and that the seven subsets without a finite value are the only unusable ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPTGDEAgu5sQVYYvfvyTJz
625ecd3 to
b39c77fComparecsun5285
commented
Sep 4, 2026
/review |
csun5285
commented
Sep 4, 2026
run buildall |
There was a problem hiding this comment.
Review status: complete after one convergence round on head b39c77f. Both normal full-review agents and the separate risk-focused agent returned NO_NEW_VALUABLE_FINDINGS, and the main final sweep found no distinct inline issue.
Critical checkpoints:
- Goal and proof: the change detects parseable reversed FLOAT/DOUBLE sentinel bounds during
ZoneMap::from_protoand conservatively marks the mappass_all. The 127 non-empty special/finite subsets for each floating type, current encodings, legacy truncated FLOAT bounds, flag combinations, sound controls, and nullable/non-nullable deserialization cover the intended mechanism and fail on the base behavior. - Scope and clarity: the production change is focused on one typed comparison helper and the deserialization order; the larger test addition is a bounded exhaustive property check.
- Concurrency: no shared mutable state, threads, locks, atomics, or lock ordering are introduced. Deserialization operates on call-local state.
- Lifecycle and ownership: no new resource lifetime, static initialization dependency, ownership cycle, or allocation-tracking concern exists. Test writers are closed and fixture teardown removes their files.
- Configuration: no configuration item is added or changed. The test only reads the existing zone-map row threshold.
- Compatibility: current head changes only reader interpretation and does not persist segment-level
pass_allor alter the protobuf/storage format. The writer-side hunk behind discussion r3911576164 is absent, so that new older-reader rolling-upgrade regression is resolved. - Parallel consumers: classic segment/page predicates, delete predicates, expression zone-map filtering, predicate elimination, and pushed-down non-COUNT aggregates all fence
pass_all. MIN/MAX falls back beforeVStatisticsIterator; COUNT does not read bounds; nullness-only evaluation uses valid null flags. - Test coverage and results: the added BE unit cases are deterministic, bounded, and exercise page-to-segment construction plus direct historical deserialization. No result file is involved. Per the review-only runner instruction, I did not build or run tests locally. Live Clang Formatter and repository checks passed. The COMPILE job failed before
build.shbecause the build container timed out connecting to gitee.com while installing nvm, so it produced no compiler diagnostic; BE UT remained pending at the final audit. - Observability: no new log or metric is needed for this local conservative validation path.
- Transactions, persistence, and writes: no transaction, EditLog, visibility, delete-bitmap, or data-write protocol is modified; only existing zone-map metadata is interpreted more safely.
- FE/BE propagation: no new cross-process variable, thrift field, or execution-version behavior is introduced.
- Performance: repaired maps fall back to data reads as intended. Parsing a flag-overridden legacy bound can also disable an otherwise usable map, but that exact pruning loss is already covered by discussion r3911576155 and the author explicitly accepted invalidation; no distinct correctness or availability consequence was found.
- Other risks: no memory-safety, error-propagation, nullability-type, special-value ordering, or parallel-path defect remains after the missed-issue rechecks.
Existing review context: r3911576155 remains the duplicate fence for the acknowledged conservative pruning degradation; r3911576164 is resolved by removal of writer-side segment pass_all and was not repeated.
User focus: no additional focus was supplied, so the full PR was reviewed.
Verdict: approval opinion from the code-review standpoint; no distinct blocking issue remains on the reviewed head.
hello-stephen
commented
Sep 4, 2026
TPC-H: Total hot run time: 16886 ms |
hello-stephen
commented
Sep 4, 2026
TPC-DS: Total hot run time: 81774 ms |
hello-stephen
commented
Sep 4, 2026
ClickBench: Total hot run time: 14.67 s |
hello-stephen
commented
Sep 4, 2026
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
PR approved by anyone and no changes requested. |
PR approved by at least one committer and no changes requested. |
NaN and infinity are recorded in the has_nan/has_positive_inf/ has_negative_inf flags and never move min/max, so a page holding nothing else leaves both at the values add_values() starts from: min = DBL_MAX and max = -DBL_MAX. Those are written out as if they were data, and the flags stand in for at most one side, so reading them back reports a DBL_MAX that is nowhere in the page -- SELECT MIN(v) answers with it while SELECT COUNT(*) WHERE v = that value answers 0. On data written before 4.0 there is no flag at all and both bounds are used, which prunes the page away: 25 rows of NaN return 0 for WHERE v > 5. The bounds round-trip exactly, so neither the flags nor the parse failure #67341 keys on can tell such a zone map from a sound one. Mark it pass_all when the bounds come back reversed, which only FLOAT and DOUBLE can do -- any value of any other type moves both. Do the same in flush() and finish() so newly written pages and segments record that they have no bounds instead of leaving it to the reader.
NaN and infinity are recorded in the has_nan/has_positive_inf/ has_negative_inf flags and never move min/max, so a page holding nothing else leaves both at the values add_values() starts from: min = DBL_MAX and max = -DBL_MAX. Those are written out as if they were data, and the flags stand in for at most one side, so reading them back reports a DBL_MAX that is nowhere in the page -- SELECT MIN(v) answers with it while SELECT COUNT(*) WHERE v = that value answers 0. On data written before 4.0 there is no flag at all and both bounds are used, which prunes the page away: 25 rows of NaN return 0 for WHERE v > 5.
The bounds round-trip exactly, so neither the flags nor the parse failure #67341 keys on can tell such a zone map from a sound one. Mark it pass_all when the bounds come back reversed, which only FLOAT and DOUBLE can do -- any value of any other type moves both. Do the same in flush() and finish() so newly written pages and segments record that they have no bounds instead of leaving it to the reader.
What problem does this PR solve?
Issue Number: close #xxx
Related PR: #xxx
Problem Summary:
Release note
None
Check List (For Author)
Test
Behavior changed:
Does this need documentation?
Check List (For Reviewer who merge this PR)