Skip to content

[fix](variant) Write Variant V2 sparse keys in byte order - #70001

Merged
eldenmoon merged 1 commit into
apache:masterfrom
eldenmoon:branch-sparse-byte-order
Nov 3, 2026
Merged

eldenmoon merged 1 commit into
apache:masterfrom
eldenmoon:branch-sparse-byte-order

Conversation

@eldenmoon

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: N/A

Related PR: #69995

#69995 (variant_escape_dots_in_keys) sorts the sparse plan by bytes only for escaped columns; whichever PR merges second removes that escaped-only sort, since this PR sorts the sparse plan in all modes.

Problem Summary:

With variant_max_subcolumns_count=1, insert {"z":1,"a-b":1,"a":{"c":2}} followed by rows containing only z. Direct a-b and a.c projections return NULL/NULL even though the whole value retains both leaves. The trigger is a key component that is a prefix of a sibling component continuing with a byte below . (space, !, ", #, $, %, &, ', (, ), *, +, ,, -), with both paths in the same sparse bucket of one row. The writer orders paths by components (a.c before a-b), while the reader binary-searches full path bytes (a-b before a.c).

Sort the sparse plan by full path bytes after select_sparse_statistics, preserving the statistics budget tie-break, statistics publication and materialized order. This is a writer-only fix: the reader is unchanged, so it adds no read cost. Newly written rows return 1/2 for the repro projections.

Rows written before this fix keep their order and are not repaired; Variant V2 is not enabled by default in any release (branch-4.1/4.2 default enable_variant_v2=false), so no reader compatibility path is added.

Release note

Fix incorrect NULL results when querying newly written Variant V2 sparse keys such as a-b beside nested a.c.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Unit Test: 140 affected tests across 6 suites; 138 passed and 2 existing NestedGroup write-path tests skipped because the write path is unavailable in this build. SparseRowsUseByteOrderWithoutChangingStatisticsSelection failed with the HEAD writer and passed with the fix, including reads through the unchanged assembler.

  • Regression test: sparse_key_byte_order passed with -parallel 1 and no -genOut, comparing the unchanged generated output for all 96 rows (DUP/MOW, 1/3 shards, ordinary/doc, before/after full compaction). Full variant_p0 ran 183 suites: 181 passed and 2 known unrelated failures (object-storage InvalidAccessKeyId/code=403 and the old schema-change public=1 versus true baseline). Both failures were independently rerun with -parallel 1 and fail the same way on unmodified master (0976b2c).

  • Manual test: the new ASAN build writes the repro table and returns 1/2 for a-b/a.c; parent and whole-value reads also match.

  • Validation: ASAN BE build, clang-format 16, build hygiene and git diff --check passed. Clang-tidy passed for both changed C++ files with no diagnostics on added lines.

  • Behavior changed:

    • No.
    • Yes. New Variant V2 sparse rows use byte order so sparse projections return their stored values.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label (branch-4.1 and branch-4.2 have the same V2 sparse-plan ordering defect)

🤖 Generated with Claude Code

### What problem does this PR solve?

Issue Number: N/A

Related PR: apache#69995

apache#69995 (`variant_escape_dots_in_keys`) sorts the sparse plan by bytes only for escaped columns; whichever PR merges second removes that escaped-only sort, since this PR sorts the sparse plan in all modes.

Problem Summary: With variant_max_subcolumns_count=1, insert {"z":1,"a-b":1,"a":{"c":2}} followed by rows containing only z. Direct a-b and a.c projections return NULL/NULL even though the whole value retains both leaves. The trigger is a key component that is a prefix of a sibling component continuing with a byte below `.` (space, `!`, `"`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `-`), with both paths in the same sparse bucket of one row. The writer orders paths by components (a.c before a-b), while the reader binary-searches full path bytes (a-b before a.c).

Sort the sparse plan by full path bytes after select_sparse_statistics, preserving the statistics budget tie-break, statistics publication and materialized order. This is a writer-only fix: the reader is unchanged, so it adds no read cost. Newly written rows return 1/2 for the repro projections.

Rows written before this fix keep their order and are not repaired; Variant V2 is not enabled by default in any release (branch-4.1/4.2 default enable_variant_v2=false), so no reader compatibility path is added.

### Release note

Fix incorrect NULL results when querying newly written Variant V2 sparse keys such as a-b beside nested a.c.

### Check List (For Author)

- Test:
    - Unit Test: 140 affected tests across 6 suites; 138 passed and 2 existing NestedGroup write-path tests skipped because the write path is unavailable in this build. SparseRowsUseByteOrderWithoutChangingStatisticsSelection failed with the HEAD writer and passed with the fix, including reads through the unchanged assembler.
    - Regression test: sparse_key_byte_order passed with -parallel 1 and no -genOut, comparing the unchanged generated output for all 96 rows (DUP/MOW, 1/3 shards, ordinary/doc, before/after full compaction). Full variant_p0 ran 183 suites: 181 passed and 2 known unrelated failures (object-storage InvalidAccessKeyId/code=403 and the old schema-change public=1 versus true baseline). Both failures were independently rerun with -parallel 1 and fail the same way on unmodified master (0976b2c).
    - Manual test: the new ASAN build writes the repro table and returns 1/2 for a-b/a.c; parent and whole-value reads also match.
    - Validation: ASAN BE build, clang-format 16, build hygiene and git diff --check passed. Clang-tidy passed for both changed C++ files with no diagnostics on added lines.
- Behavior changed: Yes. Newly written Variant V2 sparse rows use byte order, restoring correct sparse projections.
- Does this need documentation: No. This restores existing query semantics.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28168 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 2f255a80b0aac96eb71f126979cc42c90fed035e, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17592	5206	5075	5075
q2	2158	300	241	241
q3	10268	1387	805	805
q4	4677	456	335	335
q5	7438	776	523	523
q6	179	182	151	151
q7	741	788	587	587
q8	9249	1301	1039	1039
q9	5493	4202	4165	4165
q10	6826	1279	1027	1027
q11	427	237	222	222
q12	640	398	272	272
q13	18084	3557	2992	2992
q14	275	274	242	242
q15	q16	712	720	668	668
q17	1185	745	578	578
q18	6866	6143	6094	6094
q19	1094	962	587	587
q20	382	329	227	227
q21	4709	2089	2259	2089
q22	339	286	249	249
Total cold run time: 99334 ms
Total hot run time: 28168 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5596	5589	5588	5588
q2	668	547	554	547
q3	5198	5339	4947	4947
q4	1958	2119	1542	1542
q5	5242	5137	5260	5137
q6	277	201	152	152
q7	2389	2094	1866	1866
q8	2997	2691	2687	2687
q9	8069	8036	8033	8033
q10	3829	3732	3205	3205
q11	609	420	422	420
q12	682	714	521	521
q13	3566	3890	3244	3244
q14	312	329	299	299
q15	q16	684	723	641	641
q17	8058	7023	6857	6857
q18	12997	12128	12899	12128
q19	870	774	773	773
q20	2127	2137	1903	1903
q21	5527	4573	4805	4573
q22	504	460	404	404
Total cold run time: 72159 ms
Total hot run time: 65467 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150793 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 2f255a80b0aac96eb71f126979cc42c90fed035e, data reload: false

query5	4306	588	430	430
query6	423	217	185	185
query7	4794	448	226	226
query8	337	167	155	155
query9	8782	3174	3181	3174
query10	474	296	262	262
query11	5989	3858	3567	3567
query12	143	89	85	85
query13	1253	436	328	328
query14	6099	3879	3626	3626
query14_1	3412	3398	3373	3373
query15	213	202	178	178
query16	946	379	405	379
query17	850	668	537	537
query18	2447	434	307	307
query19	197	165	137	137
query20	85	83	83	83
query21	214	127	105	105
query22	12870	13125	12766	12766
query23	13470	12737	12485	12485
query23_1	12693	12576	12481	12481
query24	7331	822	473	473
query24_1	488	483	480	480
query25	541	427	364	364
query26	1259	257	151	151
query27	2754	439	274	274
query28	4608	2033	2048	2033
query29	1570	606	456	456
query30	293	218	185	185
query31	1013	921	806	806
query32	152	93	92	92
query33	525	300	238	238
query34	901	871	514	514
query35	738	754	650	650
query36	812	792	714	714
query37	133	104	85	85
query38	1765	1734	1673	1673
query39	791	796	738	738
query39_1	729	717	740	717
query40	224	120	104	104
query41	71	69	68	68
query42	90	82	88	82
query43	348	370	308	308
query44	1321	718	712	712
query45	181	182	171	171
query46	833	976	582	582
query47	2996	2778	2684	2684
query48	348	306	219	219
query49	584	397	284	284
query50	703	295	219	219
query51	10486	10718	10395	10395
query52	74	85	67	67
query53	183	205	157	157
query54	230	187	197	187
query55	70	64	62	62
query56	214	221	201	201
query57	1655	1582	1580	1580
query58	281	244	252	244
query59	2164	2198	1959	1959
query60	276	242	219	219
query61	154	164	146	146
query62	392	334	299	299
query63	183	157	157	157
query64	2727	997	767	767
query65	3321	3269	3242	3242
query66	1790	415	298	298
query67	20377	20037	20040	20037
query68	2949	1015	630	630
query69	386	271	237	237
query70	873	776	764	764
query71	289	226	207	207
query72	2759	2546	2262	2262
query73	508	523	297	297
query74	4630	4520	4329	4329
query75	2339	2270	1916	1916
query76	2308	1008	605	605
query77	350	375	264	264
query78	9851	9800	9161	9161
query79	1003	848	533	533
query80	549	433	341	341
query81	535	320	264	264
query82	581	125	102	102
query83	310	192	185	185
query84	323	128	102	102
query85	799	428	361	361
query86	320	236	231	231
query87	1995	1964	1769	1769
query88	3787	2784	2765	2765
query89	330	286	242	242
query90	1920	171	166	166
query91	165	151	129	129
query92	99	86	80	80
query93	994	1004	577	577
query94	448	322	275	275
query95	572	396	324	324
query96	680	489	234	234
query97	2466	2475	2339	2339
query98	156	150	147	147
query99	769	765	655	655
Total cold run time: 234499 ms
Total hot run time: 150793 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.17 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 2f255a80b0aac96eb71f126979cc42c90fed035e, data reload: false

query1	0.00	0.00	0.00
query2	0.09	0.05	0.05
query3	0.27	0.09	0.09
query4	1.61	0.10	0.10
query5	0.23	0.25	0.24
query6	1.16	0.70	0.67
query7	0.04	0.01	0.01
query8	0.06	0.04	0.04
query9	0.41	0.34	0.33
query10	0.59	0.55	0.58
query11	0.19	0.14	0.15
query12	0.18	0.14	0.14
query13	0.49	0.49	0.48
query14	1.04	1.01	1.01
query15	0.63	0.60	0.60
query16	0.34	0.34	0.34
query17	1.12	1.16	1.10
query18	0.20	0.18	0.19
query19	2.10	2.03	1.97
query20	0.02	0.01	0.02
query21	15.51	0.20	0.14
query22	4.74	0.06	0.06
query23	16.27	0.33	0.12
query24	3.01	0.58	0.35
query25	0.11	0.07	0.06
query26	1.24	0.22	0.16
query27	0.05	0.05	0.04
query28	2.96	0.66	0.35
query29	12.47	4.19	3.23
query30	0.28	0.13	0.11
query31	2.77	0.56	0.31
query32	3.23	0.60	0.51
query33	3.12	3.18	3.24
query34	15.35	3.97	3.38
query35	3.35	3.35	3.33
query36	0.55	0.43	0.44
query37	0.09	0.06	0.07
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.15	0.14
query41	0.09	0.04	0.03
query42	0.04	0.03	0.03
query43	0.05	0.03	0.04
Total cold run time: 96.31 s
Total hot run time: 24.17 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.64% (35868/46195)
Line Coverage 62.98% (403320/640362)
Region Coverage 59.55% (341152/572906)
Branch Coverage 60.67% (157919/260297)

@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/41567512541

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

@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 at the exact PR head found one distinct P1 compatibility issue. The added raw-byte sort is the correct order for newly written sparse rows: the unchanged VariantAssembler lower-bound lookup, bucket combiner, and sparse merge paths all consume raw-byte-sorted keys, and statistics are selected before the reorder so the existing budget tie-break is retained. The added unit and regression cases were inspected across ordinary/DOC, duplicate/unique, and one/three-bucket configurations; no separate test or generated-output defect was found. Existing inline review threads are empty and the user focus file has no extra focus points.

The inline P1 explains that current master always uses Variant V2, so segments written by the pre-fix writer can retain component order while readers still binary-search raw bytes; this PR does not provide a reader fallback or rewrite guarantee for those persisted rows. This review was static only; no builds or tests were executed by this invocation.

Review did not converge after 3 rounds. This automated review cannot pass the review check.

The assigned raw-byte/statistics risk subagent could not return after repeated gpt-6-sol and fallback model-capacity failures. Main independently verified that scope and all candidate dispositions, but the contract's all-assigned-subagents convergence condition was not met.

@eldenmoon eldenmoon closed this Nov 3, 2026
@eldenmoon eldenmoon reopened this Nov 3, 2026
@eldenmoon
eldenmoon merged commit a35707d into apache:master Nov 3, 2026
68 of 69 checks passed
yiguolei pushed a commit that referenced this pull request Nov 6, 2026
@yiguolei yiguolei removed the dev/4.2.x label Nov 6, 2026
eldenmoon added a commit that referenced this pull request Nov 10, 2026
…#70189)

cherry-pick #70001

Test adaptation: branch-4.1 defaults `enable_variant_v2=false`, so the
regression case now temporarily enables it via `setFeConfigTemporary`
(suite marked `nonConcurrent`) and additionally asserts
`variant_type(v)`; expected outputs are otherwise unchanged.
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 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 added a commit to eldenmoon/incubator-doris that referenced this pull request Nov 10, 2026
…tion

### Background

Variant V2 persists each row's sparse leaves as a dotted-path map and every reader
binary-searches those keys by raw path bytes. The writer sorts the sparse plan (apache#70001), but
vertical compaction does not re-shred the variant column: it copies the raw sparse bucket maps
into the new segment through `SparseColumnMergeIterator`.

### What problem does this PR solve?

Issue Number: None

Problem Summary:

Compacting a table that still contains rows persisted before apache#70001 kept the legacy order, so the
compacted replica never converged to the byte-order invariant that readers assume. This change
makes compaction canonicalize such rows and adds debug-only assertions so a producer that violates
the invariant is caught by ASAN/UT builds without a release cost.

### 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.
- **Fix:** merge a non-conforming source row through `build_variant_sparse_row_sorted_view`, so
  compaction output is byte-ordered. Conforming rows keep the zero-copy merge.
- **Debug checks:** `DCHECK(is_variant_sparse_row_sorted(...))` in the V2 row cursor and in the
  exact sparse-path extractor. A producer regression fails the ASAN/UT build; release builds pay
  nothing for the checks.
- **Other paths:** non-vertical and doc compaction shred through the sorted writer.
- **Upgrade behavior:** query-time tolerance is intentionally not added. A legacy row keeps
  failing with `Duplicate Variant object key` until vertical compaction rewrites its segment.

### Testing

- `./run-be-ut.sh --run --filter='SparseColumnMergeIteratorTest.*:VariantAssemblerTest.*:BinaryColumnExtractIteratorV2Test.*'`
  on dev-x (ASAN): passed.
- The new case feeds a sorted row, a three-run legacy row, an empty row, a tail inversion, and a
  byte-first-stored-last row through the merge and asserts the output paths are byte-ordered.
- `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. Un-compacted legacy segments keep reporting `Duplicate Variant object key` until compaction
rewrites them.

### 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 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, and every reader merges or
binary-searches those keys by raw path bytes. The writer sorts the sparse plan (apache#70001) and vertical
compaction copies the cells verbatim, so byte order is a producer obligation. Nothing checked it, so
a producer regression would 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 that stops sorting 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 the V2 row cursor
  (`StorageMapRowCursor::bind`), in the exact sparse-path extractor, and in the compaction merge
  loop. They compile out in release, so query and compaction performance are unchanged: no extra
  scan and no allocation.
- **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.
- **Test:** `VariantAssemblerTest.DebugCheckRejectsUnsortedSparseRows` (death test) feeds a
  two-cell unsorted row and expects the debug check to fire.

### Testing

- `./run-be-ut.sh --run --filter='VariantAssemblerTest.*:BinaryColumnExtractIteratorV2Test.*:VariantShredderTest.*'`
  on dev-x (ASAN): 17 tests passed.
- Negative check: temporarily removed the writer-side `std::ranges::sort` from `VariantShredder`
  (apache#70001). `VariantShredderTest.SparseRowsUseByteOrderWithoutChangingStatisticsSelection` then
  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 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, 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.
- **Test:** `VariantAssemblerTest.DebugCheckRejectsUnsortedSparseRows` (death test) feeds a
  two-cell unsorted row and expects the debug check to fire.

### 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.
- 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 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, 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 added a commit to eldenmoon/incubator-doris that referenced this pull request Nov 12, 2026
### 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
yiguolei pushed a commit that referenced this pull request Nov 13, 2026
…ds (#70416)

### 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`
(#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants