Skip to content

[refactor](be) Read stored values through StorageLayout - #69609

Merged
csun5285 merged 7 commits into
apache:masterfrom
csun5285:storage-layout-read-side
Oct 27, 2026
Merged

csun5285 merged 7 commits into
apache:masterfrom
csun5285:storage-layout-read-side

Conversation

@csun5285

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

Release note

None

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
  • Behavior changed:

    • No.
    • Yes.
  • 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

csun5285 and others added 4 commits October 25, 2026 03:56
… the storage types from core

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68608, apache#68664, apache#69070

Problem Summary: Since apache#69070 the write path converts every value through
StorageLayout<FieldType>, but the read direction still lives elsewhere, in
two copies. The plain and bitshuffle page decoders push the stored bytes
through IColumn::insert_many_fix_len_data, and ColumnVector and ColumnDecimal
unpack the V1 DATE, DATETIME and DECIMAL layouts inside that method. The zone
map turns the stored bounds of a page back into Fields through
Field::create_field_from_olap_value, which reads the storage type of every
PrimitiveType from PrimitiveTypeTraits::StorageFieldType. So core still knows
how the storage engine lays out three of its types, and the layout table is
not the one place that says how a type is stored.

This change gives the fixed-width layouts the read direction and removes the
other two copies:

- `StorageLayout<FT>::to_primitive` is the inverse of `to_storage` for one
  value, and `storage_to_column(values, n, dst)` appends n stored values,
  which need not be aligned, to the column of the PrimitiveType, or to a
  Nullable over it whose new rows are marked not NULL. The bit-cast and
  floating-point layouts copy once; the V1 layouts repack value by value.
  The plain and bitshuffle decoders read through it.
- `IColumn::insert_many_fix_len_data` and its overrides, with
  `ColumnVector::insert_date_column` / `insert_datetime_column`, are deleted;
  core no longer includes uint24.h or decimal12.h through primitive_type.h.
- The zone map writer keeps its running bounds as the stored values of the
  column's FieldType (`CppTypeTraits<FT>::CppType`) and materialises them
  with `to_primitive`. `Field::create_field_from_olap_value`,
  `PrimitiveTypeTraits::StorageFieldType` and the unused
  `WindowFunnelState::NativeType` go away.

Pages decode to the same values as before. Two details of the zone map
change in form only:

- Its DATE bound used to be rebuilt with the range-checked
  `from_olap_date`, which leaves the zero date when the stored bits do not
  form a valid date; the page decoders always used the unchecked
  `set_olap_date`, which `to_primitive` now uses for both. Every date the
  writer stores comes from `to_olap_date` of a checked value, so no stored
  bound changes; only a corrupted cell would now read back as its own
  fields instead of the zero date.
- Its DATETIME bounds compare as int64 instead of uint64; the packed digits
  never set the sign bit.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - New: StorageLayoutTest round trips `to_primitive` for every layout kind
      and reads unaligned V1 cells, bit-cast cells and cells into a Nullable
      column through `storage_to_column`; PlainPageTest and BitShufflePageTest
      decode a V1 DATE page by batch and by row id into ColumnDate and a
      Nullable over it.
    - Adapted: the tests that built Fields from stored bytes use
      `field_of_cell` (be/test/storage/storage_layout_test_util.h); the tests
      of the deleted insert helpers are removed.
    - Run on this revision (ASAN build): 518 tests from 23 suites, all pass: StorageLayoutTest (22),
      PlainPageTest (8), BitShufflePageTest (14), ColumnZoneMapTest (25),
      PageFormatTest (2), SegmentFlusherFormatTest (7) and
      SegmentFlusherTransformFormatTest (4) against their unchanged goldens,
      ColumnReaderTest (67), EncodingInfoTest (8), ColumnVectorTest (42),
      ColumnDecimalTest (38), ColumnArrayTest (55), ColumnNullableTest (18),
      DateBloomFilterTest (2), OlapTypeTest (43), TestRowCursor (19),
      InvertedIndexReaderTest (53), KeyCoderDateTimeTest (1), KeyCoderTest (8),
      RowKeyEncoderTest (9), IndexBuilderTest (42), PartitionTransformersTest
      (27), StorageFieldTypeTest (4).
    - clang-format 16, build hygiene, clang-tidy on the changed lines.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68608

Problem Summary: Review follow-up on the read direction of StorageLayout.
values_column had one caller, repack_to_column, and its name did not say what
it did: mark the rows a page decodes into a Nullable destination not NULL and
hand back the nested column. repack_to_column now does that itself before it
repacks into the nested column, next to the only code that needs it (the
bit-cast and floating-point layouts reach ColumnNullable through
insert_many_raw_data, which marks the rows itself). The comment above
TypedZoneMapIndexWriter::ValType, which master never had, is dropped.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - Run on this revision (ASAN build): the 518 tests from the 23 suites run for
      the read direction, all pass, StorageLayoutTest (22) with
      StorageToColumnIntoNullableMarksTheRowsNotNull, PlainPageTest (8) and
      BitShufflePageTest (14) with the V1 DATE pages read by row id into a
      Nullable column, and SegmentFlusherFormatTest (7) /
      SegmentFlusherTransformFormatTest (4) against their unchanged goldens.
    - clang-format 16, build hygiene, clang-tidy on storage_layout.cpp.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68608

Problem Summary: CppTypeTraits<FieldType>::CppType is the C++ type a segment
stores for a FieldType, and PrimitiveTypeTraits<PrimitiveType>::CppType is the
compute-layer type of a PrimitiveType. The same member name on both makes the
code that converts between the two layers (StorageLayout, the zone map, the key
coder) hard to read: every CppType has to be traced back to the traits it came
from.

The storage-layer traits become StorageTypeTraits<FieldType>, with the members
StorageType and UnsignedStorageType (the unsigned integer of the same width,
which the key coder and the BKD encodings use to reorder the bits). The
static_assert of the primary template and the comments that name the traits
follow. Aliases that only restate a member keep their own names:
BaseFieldTypeTraits / NumericFieldTypeTraits / TypeTraits::CppType,
the local CppType / UnsignedCppType of the key coders, and
StorageLayout::StorageValue. No type changes.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - Run on this revision (ASAN build; the full UT build compiles every
      user of the traits): 673 tests from 31 suites, all pass: the 23 suites
      run for the read direction (518 tests) and the suites of the test files
      the rename touches, BloomFilterIndexReaderWriterTest (31),
      PageBuilderColumnTest (16), KeyCoderTraitsTest (8), BkdDifferentialTest
      (21), BkdNativeOrderTest (32), BkdNativeOrderControl (1),
      BkdPropertyTest (24) and SniiBkdAdapterTypesTest (22).
    - clang-format 16, build hygiene. clang-tidy on the 11 changed .cpp files
      reports nothing on a changed line except the existing cognitive
      complexity warning of test_ordering in key_coder_test.cpp, whose
      signature line the rename touches; its body is unchanged.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…changes

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68608

Problem Summary: Review follow-up. clang-tidy reports errors on lines this
branch changed, in test files the earlier tidy pass did not cover:

- The decimal12_t brace initializers in storage_layout_test and
  row_cursor_test now name their fields
  (modernize-use-designated-initializers).
- The V1 DATE page tests of plain_page_test and bitshuffle_page_test build
  their page in a fixture helper, and decoding by batch and reading by row id
  into a Nullable column are now separate tests. Each test checks one path,
  and each body stays under the cognitive-complexity threshold.
- key_coder_test's test_ordering, whose body is unchanged from master and
  whose signature line the rename touched, gets the NOLINTNEXTLINE that other
  long test bodies in be/test use.

repack_to_column now tests for a Nullable destination with is_column_nullable,
as the column reader does. IColumn::is_nullable() is also true for a
ColumnConst over a Nullable, which the cast that follows would reject. A page
decoder is only ever handed a column the reader created, so nothing changes at
run time.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - Run on this revision (ASAN build): 675 tests from 31 suites, the set run
      for the rename, all pass; PlainPageTest (9) and BitShufflePageTest (15)
      now hold the split V1 DATE tests.
    - clang-format 16, build hygiene, clang-tidy on the six files this
      commit touches: no finding on a line the branch changes.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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?

@csun5285

Copy link
Copy Markdown
Contributor 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.

Reviewed all 42 changed paths at head 5287505, including the removed column test source, related reader/writer call chains, and parallel encoding paths. No substantiated new finding or existing blocking inline finding remains. No additional user focus was supplied.

The refactor routes fixed-width page decoding and zone-map bound conversion through StorageLayout while removing storage-specific column/Field APIs and duplicate type aliases. The concrete flow reviewed was: writer storage cells -> plain/bitshuffle page decoder -> canonical mutable primitive or nullable column; raw zone-map bounds -> StorageLayout::to_primitive -> Field -> serialized bounds.

Critical checkpoint conclusions:

  1. Goal and evidence: conversion ownership is centralized, with tests for inverse values, unaligned V1 bytes, append offsets, nullable flags, stored NaN payloads, and DATE batch/row-ID decoding. Tests were inspected, not executed.
  2. Scope and clarity: changes stay within conversion/trait migration and replacement tests; no unrelated production change identified.
  3. Concurrency and locks: no new threads, shared mutable state, lock acquisitions or lock-order changes. Destination columns retain existing mutable ownership.
  4. Lifecycle and initialization: conversions synchronously copy into owned column buffers. Test page owners outlive decoder views. No new global initializer or reference cycle.
  5. Configuration: no added or changed configuration.
  6. Compatibility: storage trait underlying types, cell widths, encodings and persisted formats remain unchanged. All removed in-tree API consumers were migrated; explicit layout template declarations and definitions match.
  7. Thrift/protobuf: no changes; cross-release field-ID checks are not applicable.
  8. Parallel paths: plain/bitshuffle sequential and row-ID reads and dictionary INT decoding are covered. FOR, RLE and binary decoding retain their separate paths; registered fixed-width types have layouts.
  9. Special conditions: the nullable branch documents the page invariant. FileColumnIterator separates NULL runs, and canonical file-type columns establish the new concrete V1 casts; no reachable ColumnConst or nullable-shape regression found.
  10. Test coverage: focused unit coverage includes negative decimals, unaligned input, nullable appends and DATE decoder integration. No end-to-end, build or test execution was performed under the static-only contract.
  11. Expected results: no regression output files changed; inspected C++ expectations match their stored values and row IDs.
  12. Observability: existing Status/exception paths remain sufficient for this refactor; no new distributed operation or independent operational state requires metrics/logs.
  13. Transactions and persistence: no EditLog, transaction visibility, rowset-version, delete-bitmap or failover protocol changes.
  14. Data writes and atomicity: write conversions retain existing representations; read appends and allocation/type failures remain local to the established mutable-column flow. No new durable state or crash-recovery dependency.
  15. FE/BE variables: no transmitted variables or send paths added.
  16. Performance and memory: raw layouts retain bulk copying; V1 repacking reserves once and appends per value using existing tracked POD arrays. No new owning buffer, per-row COW detachment or redundant scan identified.
  17. Additional risks: checked legacy DATE/DATETIME/DECIMAL conversion, signed DATETIME bounds, numeric/timestamp wrappers, zone-map serialization, removed-symbol references, transitive includes and CMake discovery. No supported-input regression was established.

The three initial risk areas have evidence-based dispositions: page conversion/null-map and destination invariants are preserved; trait/API/template migration is complete by static inspection; supported zone-map bounds retain their values. Arbitrary corrupt nonzero DATE cells can produce different intermediate Fields because the shared inverse uses unchecked date field setting; no supported introduced trigger was found, and equivalence for arbitrary corrupt cells is not claimed. DATE serde's existing invalid-parse behavior substitutes FIRST_DAY.

Coverage limits: static review only, with no compilation, runtime tests or release ELF audit. No unresolved concrete candidate or uncovered changed-file scope remains. Review converged after two rounds, including an independent check of the frozen payload.

Review converged after 2 round(s).

@csun5285

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17580	5187	5073	5073
q2	2140	303	258	258
q3	10259	1390	816	816
q4	4680	471	345	345
q5	7436	809	524	524
q6	189	183	153	153
q7	740	806	583	583
q8	9261	1302	1113	1113
q9	5568	4196	4209	4196
q10	6821	1304	1013	1013
q11	424	247	229	229
q12	630	407	277	277
q13	18112	3614	3019	3019
q14	268	273	248	248
q15	q16	717	717	651	651
q17	1216	763	580	580
q18	7022	6194	6113	6113
q19	1100	965	591	591
q20	384	337	235	235
q21	4523	2137	2286	2137
q22	360	305	245	245
Total cold run time: 99430 ms
Total hot run time: 28399 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5621	5591	5597	5591
q2	679	599	546	546
q3	5178	5371	4930	4930
q4	2001	2193	1586	1586
q5	5261	5128	5511	5128
q6	261	206	158	158
q7	2310	2099	1898	1898
q8	3087	2698	2731	2698
q9	8082	7983	7933	7933
q10	3791	3688	3346	3346
q11	632	454	414	414
q12	709	724	549	549
q13	3501	3872	3228	3228
q14	317	330	324	324
q15	q16	690	740	658	658
q17	8399	7570	7020	7020
q18	13078	12149	12886	12149
q19	942	825	817	817
q20	2140	2159	1942	1942
q21	5672	4714	4928	4714
q22	526	462	417	417
Total cold run time: 72877 ms
Total hot run time: 66046 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150935 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 528750585f828cbf34971b5e99e22abd6989b00c, data reload: false

query5	4307	588	433	433
query6	433	216	189	189
query7	4807	448	222	222
query8	314	170	145	145
query9	8773	3125	3124	3124
query10	493	312	258	258
query11	6027	3868	3614	3614
query12	139	96	86	86
query13	1253	430	327	327
query14	6186	3964	3707	3707
query14_1	3422	3496	3409	3409
query15	208	204	177	177
query16	951	455	436	436
query17	832	644	523	523
query18	2407	423	306	306
query19	197	161	127	127
query20	90	84	83	83
query21	203	128	116	116
query22	13001	13104	12945	12945
query23	13474	12782	12340	12340
query23_1	12589	12430	12528	12430
query24	7360	817	453	453
query24_1	467	468	471	468
query25	513	398	346	346
query26	1267	255	136	136
query27	2764	435	277	277
query28	4642	1932	1932	1932
query29	1522	590	438	438
query30	302	223	178	178
query31	999	924	814	814
query32	139	91	92	91
query33	518	299	235	235
query34	946	853	490	490
query35	718	743	656	656
query36	819	813	726	726
query37	130	102	88	88
query38	1786	1755	1634	1634
query39	790	799	744	744
query39_1	736	743	751	743
query40	222	124	97	97
query41	88	88	71	71
query42	90	87	85	85
query43	351	361	312	312
query44	1316	683	697	683
query45	186	176	169	169
query46	824	956	563	563
query47	3020	2985	2807	2807
query48	300	312	228	228
query49	578	404	303	303
query50	681	280	217	217
query51	10176	10152	10045	10045
query52	84	84	80	80
query53	190	210	156	156
query54	244	223	198	198
query55	74	75	72	72
query56	249	240	219	219
query57	1674	1592	1585	1585
query58	294	280	272	272
query59	2143	2197	1977	1977
query60	310	267	242	242
query61	171	173	167	167
query62	392	343	297	297
query63	191	162	168	162
query64	2854	1055	836	836
query65	3343	3280	3277	3277
query66	1741	416	300	300
query67	20222	20228	20184	20184
query68	3178	1002	609	609
query69	416	290	240	240
query70	846	778	774	774
query71	324	224	213	213
query72	2676	2555	2286	2286
query73	516	527	291	291
query74	4663	4544	4340	4340
query75	2324	2311	1928	1928
query76	2322	1023	590	590
query77	343	381	268	268
query78	9828	9675	9155	9155
query79	994	858	515	515
query80	1255	427	342	342
query81	592	328	278	278
query82	599	131	112	112
query83	314	198	181	181
query84	316	127	106	106
query85	869	427	370	370
query86	401	236	234	234
query87	2004	1951	1809	1809
query88	3656	2687	2674	2674
query89	330	283	245	245
query90	1922	171	170	170
query91	165	156	131	131
query92	105	88	88	88
query93	983	1005	571	571
query94	646	315	276	276
query95	589	334	311	311
query96	674	490	236	236
query97	2423	2463	2322	2322
query98	173	150	152	150
query99	767	765	647	647
Total cold run time: 235520 ms
Total hot run time: 150935 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.53 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 528750585f828cbf34971b5e99e22abd6989b00c, 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.11	0.10
query5	0.24	0.24	0.24
query6	1.17	0.69	0.68
query7	0.04	0.01	0.01
query8	0.05	0.04	0.04
query9	0.40	0.35	0.36
query10	0.63	0.58	0.58
query11	0.20	0.15	0.14
query12	0.18	0.15	0.15
query13	0.50	0.53	0.51
query14	1.03	1.02	1.01
query15	0.64	0.65	0.61
query16	0.35	0.35	0.37
query17	1.15	1.08	1.13
query18	0.22	0.19	0.18
query19	2.05	2.08	2.03
query20	0.02	0.02	0.01
query21	15.48	0.19	0.13
query22	4.88	0.05	0.06
query23	16.28	0.32	0.12
query24	3.20	0.58	0.39
query25	0.13	0.07	0.05
query26	1.12	0.21	0.16
query27	0.06	0.03	0.03
query28	3.36	0.84	0.37
query29	12.54	4.19	3.30
query30	0.30	0.13	0.10
query31	2.77	0.57	0.32
query32	3.23	0.60	0.50
query33	3.22	3.23	3.27
query34	15.46	4.02	3.41
query35	3.36	3.35	3.37
query36	0.56	0.44	0.44
query37	0.09	0.07	0.06
query38	0.06	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.15	0.15
query41	0.09	0.03	0.03
query42	0.04	0.03	0.04
query43	0.04	0.03	0.03
Total cold run time: 97.32 s
Total hot run time: 24.53 s

csun5285 and others added 3 commits October 26, 2026 11:05
…writer does

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68608

Problem Summary: Review follow-up. The read direction of StorageLayout handled a Nullable
destination: repack_to_column, which writes the typed column of DATE, DATETIME and DECIMALV2
directly, unwrapped a Nullable itself, while the other layouts left it to ColumnNullable's
insert_many_raw_data. The write direction never sees a Nullable: ScalarColumnWriter::append
peels it off with peel_nullable, writes the NULL runs itself and hands column_to_storage the
nested column.

FileColumnIterator now does the same when it reads. next_batch and read_by_rowids peel a
Nullable destination once: the iterator appends the NULL runs to its null map and nested
column, and the page decoders append the values to the nested column. A dictionary page
decoder may replace the column it appends to (a ColumnDictI32 becomes a ColumnString when a
page falls back to plain encoding), so the nested column goes back into the Nullable when the
read ends, on an early return too. Neither the page decoders nor StorageLayout see a Nullable
any more, and repack_to_column loses its Nullable branch.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - PageFormatTest reads every column back by batch and now also by row id, two rows of
      every three so that the reads start and stop inside NULL runs; the DICT cases are read
      into a Nullable over ColumnDictI32 as well, which the plain pages at the end of each
      column make the decoder replace.
    - The V1 DATE decoder tests read by row id into a ColumnDate; the StorageLayout test of a
      Nullable destination is removed.
    - Run on this revision (ASAN build): the 31 suites of the earlier runs, 674
      tests, all pass; and 76 more suites that read through a column reader
      (SegmentIterator, compaction, rowset, variant, inverted index, page
      decoders): 1015 tests pass, 3 skip themselves, none fail.
      TestDeltaWriterClusterKey and SegmentCacheTest stop in their first test once
      its read-back checks have passed: drop_tablet reaches Rowset::clear_cache,
      which uses a FileCacheFactory that no earlier test of a filtered run installs
      (enable_file_cache defaults to true). TestDeltaWriter, which drops tablets the
      same way, was not run.
    - clang-format 16, build hygiene, clang-tidy on the changed files: no finding on a line
      the branch changes.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… column writer does"

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68608

Problem Summary: Review follow-up. This reverts commit 184f186, which peeled a Nullable
destination off once per read through the PeeledNullable helper class in column_reader.cpp. The
next commit redoes the change at the page decoder call sites instead, without the class.

### Release note

None

### Check List (For Author)

- Test: No need to test (the tree returns to 5287505, which was tested; the next commit
  carries the replacement and its tests)
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lable

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68608

Problem Summary: Review follow-up, redoing the reverted 184f186 without its PeeledNullable
class. The read direction of StorageLayout handled a Nullable destination: repack_to_column,
which writes the typed column of DATE, DATETIME and DECIMALV2 directly, unwrapped a Nullable
itself, while the other layouts left it to ColumnNullable's insert_many_raw_data. The write
direction never sees a Nullable: ScalarColumnWriter::append peels it off with peel_nullable
and hands column_to_storage the nested column.

FileColumnIterator::next_batch and read_by_rowids now pass each page decoder call through
decode_values (column_reader.cpp). For a Nullable dst the decoder appends to the nested
column, and the null map grows by the rows the nested column gained, as ColumnNullable's
insert overrides did. A dictionary page decoder may replace the column it appends to (a
ColumnDictI32 becomes a ColumnString when a page falls back to plain encoding); the
replacement goes back into the Nullable right after the call. Between the calls dst is a
whole Nullable again, so the NULL runs are appended as before and the loops change only at
the four decoder calls. Neither the page decoders nor StorageLayout see a Nullable any more:
repack_to_column loses its Nullable branch, and the dictionary page decoder's comment no
longer says that a ColumnNullable converts its nested column.

Lending the nested column costs, per decoder call into a Nullable, an is_column_nullable
call and one reference count increment and decrement; 184f186 paid that once per read,
at the price of the class and of appending the NULL runs to the lent column.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - PageFormatTest reads every column back by batch and now also by row id, two rows of
      every three so that the reads start and stop inside NULL runs; the DICT cases are read
      into a Nullable over ColumnDictI32 as well, which the plain pages at the end of each
      column make the decoder replace. Under gdb, ColumnNullable::change_nested_column is
      reached from all four decoder calls, 16 times, once for each ColumnDictI32 the decoder
      converts, and the test passes.
    - The V1 DATE decoder tests read by row id into a ColumnDate; the StorageLayout test of a
      Nullable destination is removed.
    - Run on this revision (ASAN build): 674 tests from the 31 suites of the earlier runs,
      all pass; and the reader suites of the earlier run (SegmentIterator, compaction,
      rowset, variant, inverted index, page decoders): the same 1015 tests in 75 suites
      pass, 3 skip themselves. VectorSearchTest.InnerProductRangeSearchBasic failed once
      (249 results against 251, tolerance 1; it compares an approximate HNSW range search
      over vectors seeded from std::random_device with an exact one and reads no column)
      and passed in 5 reruns. SegmentCacheTest and TestDeltaWriterClusterKey stop in their
      first test once its read-back checks have passed: Rowset::clear_cache uses a
      FileCacheFactory that no earlier test of a filtered run installs (enable_file_cache
      defaults to true). TestDeltaWriter was not run.
    - clang-format 16, build hygiene, clang-tidy on the changed files: no finding on a line
      this change touches; the cognitive complexity of next_batch and read_by_rowids stays
      at the base's 97 and 148.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@csun5285

Copy link
Copy Markdown
Contributor Author

/review

@csun5285

Copy link
Copy Markdown
Contributor Author

run buildall

@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.

Reviewed all 45 changed paths at a6682b0. No new actionable findings and no existing inline blockers to retain. No additional user focus was supplied.

The reviewed flow is: execution column -> ScalarColumnWriter nullable peeling -> stored cells/pages -> FileColumnIterator NULL/value runs -> page decoder -> StorageLayout execution values -> nullable map/replacement restoration -> scanner predicates/output. Index writers consume the same cells; zone maps convert their bounds back to Fields through the same layout.

Critical checkpoint conclusions:

  • Goal: storage-specific read conversion is centralized in StorageLayout and removed from compute column APIs. Direct conversion and integrated page-read tests support the intended refactor; they were inspected, not run.
  • Scope and clarity: the production changes and corresponding test migrations stay focused on this representation boundary.
  • Concurrency and locks: no new threads, shared mutable conversion state, locks, lock order, or lock-duration change. Existing iterator and destination ownership remains in use.
  • Lifecycle and initialization: no new static initialization dependency. Nullable nested borrowing ends within decode_values; dictionary fallback retains existing rows and installs the replacement column before returning.
  • Configuration: no items added or changed.
  • Compatibility: all 34 renamed storage traits retain their concrete storage/unsigned types. Page writers and encodings retain their format; DATE, DATETIME and DECIMAL reads use the former conversion routines. No exchange serialization or mixed-version protocol change was found. Internal removed APIs have no remaining live callers in repository searches.
  • Thrift/protobuf: no definitions changed.
  • Parallel paths: traced batch and rowid reads, plain/bitshuffle, bool RLE, binary plain V1/V2/V3, dictionary fallback, prefix, scalar complex-type children, offset peeking and IndexedColumnIterator. FOR value reads remain pre-existing NotSupported behavior.
  • Conditions and errors: nullable detection is structural; replacement detection handles a real dictionary-to-string transition. NULL runs stay outside value decoding, value rows receive matching false null flags, and decoder Status errors propagate.
  • Test coverage: new checks cover unaligned V1 cells, append to a nonempty column, negative decimal values, NaN payloads, DATE batch/rowid decoding, and selected rowids/NULL maps/dictionary fallback across pages. Offset-only and null-map-only paths were code-traced. Compilation, runtime error injection and end-to-end SQL behavior remain unexecuted validation limits.
  • Expected results: no .out or golden bytes changed. Inspected expected dates, decimal signs, NaN bits and selected row comparisons agree with the representations. Existing page-format golden comparisons remain in place.
  • Observability: this refactor adds no operational path requiring new logs or metrics; existing error propagation remains available.
  • Transactions and persistence: no transaction, rowset-version, delete-bitmap, EditLog, publish or failover protocol changes. Persisted cell representations are retained.
  • Writes and crash behavior: writers consume the same cell types and use existing encoders; no new commit ordering, persistent state or crash cleanup mechanism is introduced.
  • FE/BE variables: none added or passed differently.
  • Performance and memory: compatible layouts retain bulk copies; legacy layouts reserve once and convert linearly into tracked column storage. Nullable borrowing does not invoke per-run COW cloning. No new owning conversion buffer is added.
  • Other checks: layout extern declarations match explicit instantiations; migrated test discovery and direct scalar-type includes were checked. No new source .cpp test inclusion or libc/toolchain dependency was found. Binary ABI compatibility was not measured.

The review converged after 2 rounds: both complete-review agents and the independent risk agent reported NO_NEW_VALUABLE_FINDINGS in the final round; every candidate and R1/R2/R3 risk was dismissed with evidence, and no static coverage question remains. This is static inspection only: no builds, tests, benchmarks, formatting/static-analysis gates or ELF audits were run, and no source files were modified.

Review converged after 2 round(s).

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17608	5223	5091	5091
q2	2115	355	267	267
q3	10204	1383	864	864
q4	4666	467	352	352
q5	7436	811	543	543
q6	183	189	152	152
q7	793	797	590	590
q8	9258	1331	1135	1135
q9	5511	4204	4198	4198
q10	6830	1291	1024	1024
q11	423	246	228	228
q12	637	399	283	283
q13	18128	3596	3058	3058
q14	275	266	253	253
q15	q16	740	706	660	660
q17	1226	761	588	588
q18	6976	6204	6122	6122
q19	1123	958	593	593
q20	374	336	224	224
q21	5471	2487	2418	2418
q22	388	336	267	267
Total cold run time: 100365 ms
Total hot run time: 28910 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5983	5913	6158	5913
q2	783	599	591	591
q3	5684	5832	5270	5270
q4	2041	2153	1589	1589
q5	5825	5865	5701	5701
q6	236	182	144	144
q7	2317	2006	1820	1820
q8	3049	2657	2692	2657
q9	8030	7649	7630	7630
q10	3628	3581	3121	3121
q11	585	407	385	385
q12	666	691	514	514
q13	3282	3612	3074	3074
q14	287	302	265	265
q15	q16	663	679	642	642
q17	7761	7024	6938	6938
q18	12963	12207	12874	12207
q19	893	794	793	793
q20	2155	2191	1922	1922
q21	5585	4588	4673	4588
q22	542	458	405	405
Total cold run time: 72958 ms
Total hot run time: 66169 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150851 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 a6682b0cf8cdd6fd9e038953ffba7f0e4275df77, data reload: false

query5	4326	578	451	451
query6	419	206	190	190
query7	4812	442	228	228
query8	311	161	148	148
query9	8809	3200	3238	3200
query10	479	307	258	258
query11	6016	3891	3551	3551
query12	147	91	90	90
query13	1246	442	345	345
query14	6104	3916	3624	3624
query14_1	3379	3468	3417	3417
query15	208	210	176	176
query16	939	411	377	377
query17	826	632	524	524
query18	2411	420	302	302
query19	187	160	130	130
query20	83	86	80	80
query21	204	131	106	106
query22	13015	12919	12764	12764
query23	13358	12940	12362	12362
query23_1	12473	12554	12452	12452
query24	7347	830	472	472
query24_1	472	475	465	465
query25	520	424	399	399
query26	1257	250	138	138
query27	2776	433	271	271
query28	4624	2059	2041	2041
query29	1563	585	430	430
query30	290	227	179	179
query31	994	914	817	817
query32	136	89	98	89
query33	515	296	236	236
query34	921	845	501	501
query35	728	748	647	647
query36	848	811	703	703
query37	125	98	83	83
query38	1770	1740	1666	1666
query39	783	790	739	739
query39_1	732	718	737	718
query40	216	113	96	96
query41	69	63	64	63
query42	86	80	81	80
query43	346	361	308	308
query44	1302	695	703	695
query45	183	174	170	170
query46	819	978	563	563
query47	2763	2961	2630	2630
query48	294	308	223	223
query49	568	397	296	296
query50	673	280	215	215
query51	10179	10286	10455	10286
query52	79	82	72	72
query53	182	209	156	156
query54	234	199	202	199
query55	78	68	72	68
query56	244	230	225	225
query57	1654	1591	1549	1549
query58	291	274	272	272
query59	2214	2233	1984	1984
query60	296	254	241	241
query61	176	167	170	167
query62	391	331	287	287
query63	188	160	164	160
query64	2886	1066	868	868
query65	3354	3289	3272	3272
query66	1837	475	300	300
query67	20491	20086	20047	20047
query68	3236	1031	619	619
query69	392	284	251	251
query70	870	810	742	742
query71	296	230	204	204
query72	2831	2652	2316	2316
query73	531	525	302	302
query74	4655	4484	4305	4305
query75	2355	2287	1926	1926
query76	2401	1004	586	586
query77	358	374	271	271
query78	9714	9745	9255	9255
query79	991	900	531	531
query80	524	445	336	336
query81	527	319	267	267
query82	709	133	119	119
query83	301	217	198	198
query84	309	123	105	105
query85	871	433	368	368
query86	329	238	229	229
query87	1993	1949	1797	1797
query88	3766	2759	2734	2734
query89	337	293	243	243
query90	1855	166	168	166
query91	162	152	128	128
query92	100	79	92	79
query93	993	1003	584	584
query94	466	324	267	267
query95	591	391	322	322
query96	676	490	246	246
query97	2442	2451	2300	2300
query98	158	148	149	148
query99	775	770	647	647
Total cold run time: 234513 ms
Total hot run time: 150851 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.10	0.06	0.05
query3	0.27	0.09	0.09
query4	1.62	0.10	0.10
query5	0.23	0.24	0.23
query6	1.16	0.68	0.69
query7	0.03	0.01	0.00
query8	0.05	0.04	0.04
query9	0.41	0.36	0.36
query10	0.58	0.61	0.56
query11	0.20	0.14	0.14
query12	0.19	0.15	0.14
query13	0.49	0.48	0.49
query14	1.03	1.04	1.00
query15	0.61	0.60	0.60
query16	0.34	0.35	0.34
query17	1.13	1.15	1.08
query18	0.22	0.20	0.20
query19	2.04	2.06	2.01
query20	0.02	0.01	0.01
query21	15.50	0.19	0.13
query22	4.93	0.06	0.05
query23	16.29	0.32	0.12
query24	3.13	0.51	0.44
query25	0.11	0.06	0.04
query26	1.17	0.23	0.17
query27	0.04	0.04	0.05
query28	3.48	0.78	0.39
query29	12.47	4.17	3.32
query30	0.28	0.13	0.10
query31	2.76	0.54	0.32
query32	3.21	0.60	0.49
query33	3.16	3.14	3.27
query34	15.41	4.00	3.39
query35	3.35	3.32	3.34
query36	0.59	0.46	0.45
query37	0.09	0.06	0.06
query38	0.05	0.04	0.04
query39	0.03	0.02	0.02
query40	0.18	0.15	0.14
query41	0.08	0.03	0.02
query42	0.04	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 97.12 s
Total hot run time: 24.36 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (34/34) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.35% (36072/46634)
Line Coverage 62.59% (407702/651383)
Region Coverage 59.05% (344602/583534)
Branch Coverage 59.93% (159360/265929)

@eldenmoon eldenmoon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@csun5285
csun5285 merged commit 3d2d9aa into apache:master Oct 27, 2026
37 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by anyone and no changes requested.

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

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

Labels

approved Indicates a PR has been approved by one committer. reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants