Skip to content

[improvement](variant) Assert Variant sparse byte order in debug builds - #70416

Open
eldenmoon wants to merge 2 commits into
apache:masterfrom
eldenmoon:branch-cir22583-master
Open

eldenmoon wants to merge 2 commits into
apache:masterfrom
eldenmoon:branch-cir22583-master

Conversation

@eldenmoon

@eldenmoon eldenmoon commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Background

Variant V2 persists each row's sparse leaves as a dotted-path map, and every reader merges or
binary-searches those keys by raw path bytes. The writer sorts the sparse plan (#70001), and
vertical compaction merges the source sparse row with the materialized subcolumns, so byte order is
a producer obligation all the way to the compacted segment. Nothing checked it, so a regression
would only surface much later as a query failure (Duplicate Variant object key).

What problem does this PR solve?

Issue Number: None

Problem Summary:

Without a check, a writer or compaction merge that breaks the order produces segments whose reads
fail, far from the cause. This change asserts the invariant where it is relied on and keeps the
assertion out of release builds.

Details

  • Debug-only checks: DCHECK(is_variant_sparse_row_sorted(...)) in
    • StorageMapRowCursor::bind() (V2 row cursor used by every hierarchical read),
    • the exact sparse-path extractor,
    • SparseColumnMergeIterator::_merge_to for both the source row and the merged destination row,
      because the merge interleaves sparse cells with materialized subcolumn paths.
  • Why not repair: query-time and compaction-time tolerance is deliberately not added on this
    branch. Segments must be byte-ordered; a violation is a producer bug.
  • Performance: every check is a DCHECK, compiled out in release; queries and compaction keep
    the same scan, sort, and allocation behavior as before.
  • Tests: VariantAssemblerTest.DebugCheckRejectsUnsortedSparseRows (death test) feeds a
    two-cell unsorted row and expects the debug check to fire;
    SparseColumnMergeIteratorTest.KeepsByteOrderWhenMergingMaterializedSubcolumn merges a
    byte-sorted source row with a materialized a.d column and asserts the merged row is
    a-b, a.c, a.d. The import side is already covered by
    VariantShredderTest.SparseRowsUseByteOrderWithoutChangingStatisticsSelection.

Testing

  • ./run-be-ut.sh --run --filter='VariantAssemblerTest.*:BinaryColumnExtractIteratorV2Test.*:VariantShredderTest.*'
    on dev-x (ASAN): 17 tests passed.
  • ./run-be-ut.sh --run --filter='VariantColumnWriterReaderTest.*:IndexStorageVariantCompactionReadTest.*:SparseColumnMergeIteratorTest.*'
    on dev-x (ASAN): 75 passed, 4 pre-existing skips.
  • ./run-be-ut.sh --run --filter='SparseColumnMergeIteratorTest.*:VariantAssemblerTest.*'
    on dev-x (ASAN): 2 passed (merge interleaving + death test).
  • Negative check: temporarily removed the writer-side std::ranges::sort from VariantShredder
    ([fix](variant) Write Variant V2 sparse keys in byte order #70001). VariantShredderTest.SparseRowsUseByteOrderWithoutChangingStatisticsSelection failed
    its byte-order expectation and the new check aborted in StorageMapRowCursor::bind
    (Check failed: is_variant_sparse_row_sorted), so the assertion does catch a producer that stops
    sorting. The sort was restored afterwards.
  • build-support/check-build-hygiene.sh passed.

Release note

None.

Check List (For Author)

  • Behavior changed: No (assertions only, active in debug builds)
  • Does this need documentation: No
  • Pre-submission status: Ready

AI Disclosure

  • Tool: DeepSeek Harness
  • Generated: code, tests, commit message
  • Human review: pending

@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review setup stopped before context preparation ran; inspect the first failed or cancelled setup step.
Workflow run: https://github.com/apache/doris/actions/runs/42746524413

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

eldenmoon added a commit to eldenmoon/incubator-doris that referenced this pull request Nov 10, 2026
### Background

Variant V2 persists each row's sparse leaves as a dotted-path map. Readers binary-search that map by raw path
bytes and merge it in path order, so apache#70416 added a byte-ordered fallback for rows written before the
writer-side sort (apache#70001). The fallback validates a batch once but currently drops the binary-search fast path
for every row of the batch, and the hierarchical assembler can still index paths with unchecked map offsets.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#70416, apache#70001
PR Type: Bugfix

Problem Summary:

Mixed batches of conforming and legacy rows pay a linear path scan for every row, and corrupt sparse offsets can
reach path reads instead of failing with a corruption status. This commit records non-conforming rows per batch
and validates offsets in `start_row()`, with unit tests for mixed batches, later runs, missing paths, and bad
offsets. Users see no behavior change for healthy data.

### Details

- **Frequency:** Mixed batches exist only while legacy rows and newly written rows share a segment; corrupt
  offsets come from a damaged segment.
- **Root cause:** the batch-level sortedness flag was consulted per row, and `start_row()` trusted offsets that
  `bind()` already knew could be invalid.
- **Why existing tests missed it:** the fallback tests covered single-row batches only, and no test built
  non-monotonic offsets for the assembler.
- **Fix:** keep a bitmap of non-conforming rows; return `Status::Corruption` for invalid offsets.
- **Other paths and branches:** none; the V1 reader sorts merged sparse paths itself.

### Testing

- `./run-be-ut.sh --run --filter='VariantAssemblerTest.*:BinaryColumnExtractIteratorV2Test.*'` on dev-x (ASAN):
  21/21 tests passed.
- New cases: mixed sorted/legacy/empty batch, three-run legacy row, subtree split across runs, missing path,
  exact path in a later run, and non-monotonic offsets.
- `build-support/check-build-hygiene.sh` passed.

### Release note

None.

### Check List (For Author)

- Behavior changed: No
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: DeepSeek Harness
- Generated: code, tests, commit message
- Human review: pending
@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

eldenmoon added a commit to eldenmoon/incubator-doris that referenced this pull request Nov 10, 2026
…tion

### Background

Variant V2 readers binary-search sparse keys by raw path bytes. apache#70416 added a byte-ordered fallback for
segments written before the writer-side sort (apache#70001). Vertical variant compaction does not re-shred the
variant column: it copies each raw sparse bucket map into the new segment through
`SparseColumnMergeIterator`.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#70416, apache#70001
PR Type: Bugfix

Problem Summary:

Compacting a table that contains legacy out-of-order sparse rows kept the legacy order in the new segments, so
the read fallback would be required forever and a compacted replica would never converge to canonical order.
The merge now reads a non-conforming source row through a byte-ordered view, which canonicalizes compaction
output and matches the writer invariant. Conforming rows keep the zero-copy merge.

### Details

- **Frequency:** Vertical variant compaction (`enable_vertical_compact_variant_subcolumns`, default on) of
  segments written before apache#70001, or of rows assembled from several sorted runs.
- **Root cause:** `SparseColumnMergeIterator::_merge_to` copied source sparse cells in stored order; it is the
  only compaction path that does not go through `VariantShredder`, whose sparse plan is sorted.
- **Why existing tests missed it:** compaction tests write through the fixed shredder, so their source sparse
  maps are already byte-sorted; no test fed an unsorted raw map into the merge iterator.
- **Fix:** validate each source row and merge a non-conforming row through
  `build_variant_sparse_row_sorted_view`.
- **Other paths and branches:** non-vertical compaction and doc compaction shred through the sorted writer;
  queries keep the apache#70416 fallback for segments that have not been compacted yet.

### Testing

- `./run-be-ut.sh --run --filter='VariantAssemblerTest.*:BinaryColumnExtractIteratorV2Test.*:SparseColumnMergeIteratorTest.*'`
  on dev-x (ASAN): 22/22 passed.
- `./run-be-ut.sh --run --filter='VariantColumnWriterReaderTest.*:VariantShredderTest.*:VariantPathBuilderTest.*:IndexStorageVariantCompactionReadTest.*'`:
  102 tests, 98 passed and 4 pre-existing skips.
- `SparseColumnMergeIteratorTest.SortsLegacySparseRowsWhenCompacting` feeds a sorted row, a three-run legacy
  row, and an empty row through the merge; the legacy output is byte-sorted and keeps every cell.
- `build-support/check-build-hygiene.sh` passed.

### Release note

Fix vertical compaction of legacy VARIANT rows so compacted segments store sparse keys in byte order.

### Check List (For Author)

- Behavior changed: Yes (see Details) — compacted legacy rows get canonical key order.
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: DeepSeek Harness
- Generated: code, tests, commit message
- Human review: pending
@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review setup stopped before context preparation ran; inspect the first failed or cancelled setup step.
Workflow run: https://github.com/apache/doris/actions/runs/42759347828

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon
eldenmoon force-pushed the branch-cir22583-master branch from 65ce6ff to f3ce8c9 Compare November 10, 2026 09:49
@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon eldenmoon changed the title [fix](variant) Read legacy sparse rows with out-of-order keys [fix](variant) Canonicalize legacy sparse rows during vertical compaction Nov 10, 2026
@eldenmoon eldenmoon changed the title [fix](variant) Canonicalize legacy sparse rows during vertical compaction [improvement](variant) Assert Variant sparse byte order in debug builds Nov 10, 2026
@eldenmoon
eldenmoon force-pushed the branch-cir22583-master branch from f3ce8c9 to 5a15948 Compare November 10, 2026 10:23
@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review setup stopped before context preparation ran; inspect the first failed or cancelled setup step.
Workflow run: https://github.com/apache/doris/actions/runs/42803854926

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon
eldenmoon force-pushed the branch-cir22583-master branch 2 times, most recently from a0ad529 to 2779a69 Compare November 10, 2026 12:41
@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review setup stopped before context preparation ran; inspect the first failed or cancelled setup step.
Workflow run: https://github.com/apache/doris/actions/runs/42820588081

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

### Background

Variant V2 persists each row's sparse leaves as a dotted-path map, and every reader merges or
binary-searches those keys by raw path bytes. The writer sorts the sparse plan (apache#70001), and
vertical compaction merges the source sparse row with the materialized subcolumns, so byte order is
a producer obligation all the way to the compacted segment. Nothing checked it, so a regression
would only surface much later as a query failure (`Duplicate Variant object key`).

### What problem does this PR solve?

Issue Number: None

Problem Summary:

Without a check, a writer or compaction merge that breaks the order produces segments whose reads
fail, far from the cause. This change asserts the invariant where it is relied on and keeps the
assertion out of release builds.

### Details

- **Debug-only checks:** `DCHECK(is_variant_sparse_row_sorted(...))` in
  - `StorageMapRowCursor::bind()` (V2 row cursor used by every hierarchical read),
  - the exact sparse-path extractor,
  - `SparseColumnMergeIterator::_merge_to` for both the source row and the merged destination row,
    because the merge interleaves sparse cells with materialized subcolumn paths.
- **Why not repair:** query-time and compaction-time tolerance is deliberately not added on this
  branch. Segments must be byte-ordered; a violation is a producer bug.
- **Performance:** every check is a `DCHECK`, compiled out in release; queries and compaction keep
  the same scan, sort, and allocation behavior as before.
- **Tests:** `VariantAssemblerTest.DebugCheckRejectsUnsortedSparseRows` (death test) feeds a
  two-cell unsorted row and expects the debug check to fire;
  `SparseColumnMergeIteratorTest.KeepsByteOrderWhenMergingMaterializedSubcolumn` merges a
  byte-sorted source row with a materialized `a.d` column and asserts the merged row is
  `a-b, a.c, a.d`. The import side is already covered by
  `VariantShredderTest.SparseRowsUseByteOrderWithoutChangingStatisticsSelection`.

### Testing

- `./run-be-ut.sh --run --filter='VariantAssemblerTest.*:BinaryColumnExtractIteratorV2Test.*:VariantShredderTest.*'`
  on dev-x (ASAN): 17 tests passed.
- `./run-be-ut.sh --run --filter='VariantColumnWriterReaderTest.*:IndexStorageVariantCompactionReadTest.*:SparseColumnMergeIteratorTest.*'`
  on dev-x (ASAN): 75 passed, 4 pre-existing skips.
- `./run-be-ut.sh --run --filter='SparseColumnMergeIteratorTest.*:VariantAssemblerTest.*'`
  on dev-x (ASAN): 2 passed (merge interleaving + death test).
- Negative check: temporarily removed the writer-side `std::ranges::sort` from `VariantShredder`
  (apache#70001). `VariantShredderTest.SparseRowsUseByteOrderWithoutChangingStatisticsSelection` failed
  its byte-order expectation and the new check aborted in `StorageMapRowCursor::bind`
  (`Check failed: is_variant_sparse_row_sorted`), so the assertion does catch a producer that stops
  sorting. The sort was restored afterwards.
- `build-support/check-build-hygiene.sh` passed.

### Release note

None.

### Check List (For Author)

- Behavior changed: No (assertions only, active in debug builds)
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: DeepSeek Harness
- Generated: code, tests, commit message
- Human review: pending
@eldenmoon
eldenmoon force-pushed the branch-cir22583-master branch from 2779a69 to 7df2a08 Compare November 12, 2026 02:11
@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of all six changed files at head 7df2a08. One verified P2 finding: the new compaction destination assertion can compare keys across independent rows when a subsequent sub-batch appends to a nonempty destination, aborting a debug/ASAN BE on valid data. No P0/P1 findings or existing blocking inline comments were identified.

Critical checkpoint conclusions:

  • Goal and tests: shared debug assertions implement the byte-order check; the death test covers descending sparse keys. The new merge test checks an initially empty one-row destination and misses the repeated-append case in the inline finding.
  • Scope and reuse: the six-file change is focused, with one allocation-free helper shared by exact extraction, hierarchical assembly, and compaction.
  • Concurrency/locks: no new thread, shared mutation, lock, or lock-order change.
  • Lifetimes/static initialization: checks borrow existing columns; no new production ownership, COW mutation, or global initialization dependency.
  • Configuration: no configuration additions or dynamic-change requirements.
  • Compatibility: no persisted layout, encoding, or removed symbol changes. SPARSE and DOC producers sort the same raw bytes used by StringRef; bucket reconstruction preserves order. Debug rejection of already unsorted persisted maps matches the stated invariant.
  • Thrift/protobuf: no definition changes; field-ID compatibility review is not applicable.
  • Parallel paths: reviewed exact/hierarchical reads, SPARSE/DOC, single/multiple buckets, sparse/no-sparse merge, and sequential/rowid calls. The destination-append issue applies to compaction chunking.
  • Conditional checks: empty/singleton rows and equal keys remain valid; padded -1 offsets are intentional. The new destination range assumption is the verified defect.
  • Test coverage: inspected both new unit tests and relevant existing producer, assembler, bucket, and boundary tests. Repeated appends need coverage. No builds, tests, benchmarks, or build-hygiene checks were executed under this static-only contract.
  • Expected results: no golden files changed; the death expectation and a-b,a.c,a.d merge expectation match the constructed input.
  • Observability: the failing DCHECK identifies the invariant; no new logging or metrics are needed for this scope.
  • Transactions/persistence: no EditLog, version visibility, rowset lifecycle, failover, or delete-bitmap update changes. Existing bitmap filtering supplies a valid trigger for the finding.
  • Writes/atomicity/crashes: the diff adds no encoding or commit-protocol changes; the verified assertion can terminate valid compaction in debug/ASAN builds.
  • FE-BE variables: none added or changed.
  • Performance/memory: helper checks allocate nothing and run only with DCHECK enabled. Release uses -O3 -DNDEBUG; disabled expressions introduce no helper scan, and the empty outer loop is optimizable away. No release regression established by source inspection.
  • Other module checks: no hub-header/PCH, extern-template, unity source inclusion, glibc/system-library, or physical segment-ID changes. Existing nullability and COW handling are unchanged.

User focus: no additional focus was supplied. All candidates and risk items have explicit dispositions; no unresolved static coverage gaps remain. Review converged after two rounds. Runtime behavior and the author's reported test runs were not independently executed.

Review converged after 2 round(s).

// are byte-ordered. Assert the merged row keeps that invariant for downstream readers;
// debug-only, so release compaction pays nothing.
DCHECK(is_variant_sparse_row_sorted(dst_sparse_column_paths,
dst_sparse_column_offsets[ssize_t(row) - 1],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Limit the destination assertion to the row just appended

row restarts at zero for each source batch, but _merge_to appends to dst. SegmentIterator::_read_columns_by_index can call this iterator repeatedly on the same destination for 256-row chunks of discontinuous row IDs, for example after delete-bitmap filtering. On the second call, row == 0 makes this start offset dst_sparse_column_offsets[-1] == 0, so valid rows containing z followed by a new row containing a are checked as one sequence and abort a debug/ASAN BE. Capture the destination key count before appending each row (or use its actual last offset) and check only that row's range; add coverage that appends a second batch to a nonempty destination.

### Background

The sparse merge iterator test checks that merging a materialized subcolumn preserves sparse path byte order. Doris CI checks C++ formatting with clang-format 16.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#70416 (formatting follow-up)
PR Type: Build/CI/Chore

Problem Summary:

Clang Formatter rejected the wrapping of the SubstreamIterator construction in the new test. Apply clang-format 16.0.3 to fix that single formatting hunk.

### Details

- **Affected:** be/test/storage/segment/binary_column_extract_iterator_test.cpp.
- **Developer and release impact:** Whitespace-only changes; test expressions and runtime behavior are unchanged.
- **License and third-party:** None.
- **Rollback:** Revert this formatting-only commit.

### Testing

- Reproduced the CI formatting diff locally with clang-format 16.0.3 and build-support/check-format.sh on the six PR-modified C++ files.
- Ran build-support/clang-format.sh, then repeated check-format.sh on those six files; passed with exit code 0.
- Verified that only the reported test file changed, its non-whitespace content is identical, and the diff has no whitespace errors.

### Release note

None.

### Check List (For Author)

- Behavior changed: No
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: ChatGPT (GPT-6 Astra Pro), clang-format 16.0.3
- Generated: Formatting fix and commit message
- Human review: Pending
@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of all six changed files at head 46c236b. No additional inline findings. The existing P2 destination-append assertion issue still applies and should be fixed: a subsequent batch can check keys across independent destination rows and abort a debug/ASAN BE on valid data. It was independently confirmed and suppressed as a duplicate. No existing P0/P1 inline findings were identified.

Critical checkpoint conclusions:

  • Goal and evidence: the shared checks detect descending sparse keys at read/merge boundaries. The new death test reaches the intended assertion; the positive merge test checks byte order. The known destination-range defect prevents the intended invariant check from being correct for all valid batch sequences.
  • Scope and reuse: the change is focused on one allocation-free helper, its reader/compaction assertions, and two unit tests.
  • Concurrency and locks: the checks read existing batch columns and add no thread, shared mutation, lock, lock ordering, or work under a new lock.
  • Lifecycle and static initialization: no new production ownership, COW mutation, resources, cycles, or global initializers. The new test's input columns, options, stats and path set outlive their uses.
  • Configuration: no added configuration or dynamic-update behavior.
  • Compatibility: no persisted layout, encoding, removed symbol, or wire-protocol change. Current SPARSE and DOC producers sort the full raw path bytes, matching StringRef and existing consumers; bucket combination sorts the same way. Deliberately unsorted legacy producer data already violates the consumer ordering contract.
  • Thrift/protobuf IDs: no gensrc changes; cross-branch field-ID comparison is not applicable.
  • Parallel paths: inspected exact/hierarchical reads, SPARSE/DOC storage, single/multiple buckets, sparse/no-sparse merge and sequential/rowid calls. Source caches reset between batches; destination columns can remain populated, which confirms the existing P2.
  • Conditional checks: empty/singleton rows and equal adjacent keys satisfy nondecreasing order. Padded offset[-1] access is valid. Exact extraction validates offsets before the new predicate; the destination check's batch-local range is the known defect.
  • Test coverage: inspected the new negative and interleaving tests plus relevant existing producer, legacy assembler and boundary coverage. The new positive test uses one row into an empty destination and misses repeated appends, as the existing thread describes. No builds, tests, benchmarks or hygiene checks were executed under this static-only contract.
  • Expected test results: no result/golden files changed. The death diagnostic and expected a-b,a.c,a.d merge order match the constructed inputs.
  • Observability: the DCHECK diagnostic identifies the violated expression; no additional logging or metrics are necessary for this narrow assertion change.
  • Transactions and persistence: no changes to EditLog/replay, commit/version visibility, rowset lifecycle, delete-bitmap updates or failover handling.
  • Writes, atomicity and crash behavior: existing append/encoding protocols are retained. The existing P2 can terminate valid compaction in debug/ASAN builds; no separate write atomicity or cleanup defect was found.
  • FE-BE variables: none added or changed.
  • Performance and memory: the helper allocates nothing and does a linear scan only when its DCHECK expression is enabled. Disabled expressions introduce no helper scan; the assembler's otherwise empty loop has no independent side effects. No optimized binary or performance measurement was inspected.
  • Other applicable module checks: no new hub-header/PCH dependencies, explicit template instantiations, source .cpp test inclusions, glibc API dependencies, physical segment-ID assumptions or nullable/COW handling changes were introduced. No distinct additional issue was substantiated.

User focus: no additional focus was supplied. Review converged after 1 round: all three reviewers returned NO_NEW_VALUABLE_FINDINGS, all six changed paths were covered, and all candidates and risk items were verified, deduplicated or dismissed with evidence. No unresolved static coverage gaps remain. Compile/runtime behavior and the author's reported test runs were not independently executed.

Review converged after 1 round(s).

@eldenmoon

Copy link
Copy Markdown
Member Author

run p0

@eldenmoon

Copy link
Copy Markdown
Member Author

run vault_p0

@eldenmoon

Copy link
Copy Markdown
Member Author

run external

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant