real-mj-song opened a new pull request, #18797:
URL: https://github.com/apache/pinot/pull/18797

   **TL;DR:** The column-major build added in #18638 removed per-row 
`GenericRow` materialization, but it still consumes every value through the 
generic `Object` path — so each primitive is **boxed twice**, once on the stats 
pass and once on the index pass. This PR adds a typed, boxing-free fast path 
for single-value `INT`/`LONG`/`FLOAT`/`DOUBLE` columns: 
`ColumnReader.nextInt()` → `collect(int)` / `indexOfSV(int)` → 
`IndexCreator.addInt(value, dictId)`. Purely additive; nulls, multi-value, and 
every other type fall back to the unchanged Object path. **No SPI changes, no 
new public API.**
   
   Tracks #18629. Builds on #18638 (column-major build) and the `ColumnReader` 
/ `ColumnReaderFactory` SPI (#16727).
   
   ### Problem
   
   #18629 set out to build a segment from a columnar source without "the 
per-row `GenericRow` allocation **and** per-primitive boxing." #18638 delivered 
the column-major path and removed the `GenericRow` allocation, but it routes 
values through the generic `Object` API:
   
   ```
   columnReader.next()                         // Object — boxes the primitive
     -> ColumnarValueNormalizer.normalize(..)  // Object
     -> dictionaryCreator.indexOfSV(Object)    // boxed dictionary lookup
     -> creator.add(Object, dictId)            // unboxes again to write
   ```
   
   So the per-primitive boxing #18629 aimed to remove is still on the hot path 
— and twice over, since `buildColumnar()` reads every column once for stats and 
once for indexing. (This is the follow-up requested in the #18638 review.)
   
   ### What this PR does
   
   The `ColumnReader` SPI (#16727) already exposes typed accessors 
(`nextInt()`, `isInt()`, …) and the downstream sinks already have primitive 
overloads — `AbstractColumnStatisticsCollector.collect(int)`, 
`SegmentDictionaryCreator.indexOfSV(int)`, and `addInt(value, dictId)` on 
`ForwardIndexCreator` / `CombinedInvertedIndexCreator` / 
`DictionaryBasedInvertedIndexCreator`. They were simply never wired to the 
column-major build. This PR wires them:
   
   - **Stats pass** (`ColumnarSegmentPreIndexStatsContainer`) — when the reader 
can serve a single-value column as a primitive (`isInt()`/`isLong()`/…), read 
with `nextInt()`/… and feed `collect(int)`/… directly.
   - **Index pass** (`SegmentColumnarIndexCreator`) — same dispatch: 
`nextInt()` → `indexOfSV(int)` → `creator.addInt(value, dictId)` across the 
column's index creators (creators with no typed override keep the boxing 
default and stay correct).
   
   Key choices:
   
   - **Gated on the logical `FieldSpec` type** (`INT`/`LONG`/`FLOAT`/`DOUBLE`), 
so `BOOLEAN` (stored `INT`) and `TIMESTAMP` (stored `LONG`) — which require 
value coercion the typed read would skip — correctly stay on the Object path.
   - **Null docs route through the existing Object path** for byte-for-byte 
parity, including null-value-vector marking and default substitution. The typed 
primitive accessor is only called when `isNextNull()` is false.
   - **Multi-value and non-primitive types are unchanged.**
   
   Net effect: for single-value primitive columns, neither build pass boxes — 
removing the remaining per-primitive boxing on both column reads.
   
   ### Scope
   
   Single-value `INT`/`LONG`/`FLOAT`/`DOUBLE` only. The typed multi-value sinks 
(`addIntMV`, `nextIntMV` / `MultiValueResult`) already exist but are 
intentionally **not** wired here — the multi-value element-null contract is a 
distinct concern and is better handled on its own.
   
   ### Compatibility
   
   Additive and behavior-preserving: **no SPI changes**, no new public API; the 
row-major path and the Object-path column-major fallback are untouched. The 
only non-fast-path edit documents the existing two-pass `rewind()` precondition 
on the `init(config, ColumnReaderFactory)` overload (the consumer that imposes 
it), leaving the `ColumnReaderFactory` SPI itself generic.
   
   ### Testing
   
   - The existing column-major equivalence suites already drive the new typed 
branches and continue to pass — in particular `ColumnarRowMajorEquivalenceTest` 
builds a segment column-major vs. row-major over single-value 
`INT`/`LONG`/`FLOAT`/`DOUBLE` columns with ~10% nulls and asserts per-doc value 
and min/max/cardinality equality.
   - Added two `ArrowColumnarBuildIntegrationTest` cases that pin null-edge 
shapes only an Arrow source can produce, with **absolute** expected values 
(independent of the row-major oracle, so they also catch a regression in the 
shared normalization):
     - element-level multi-value nulls + whole-array null + all-null-elements + 
empty list → null elements dropped; all-null / empty list resolves to the 
column default;
     - an all-null single-value column → cardinality 1, every doc reads the 
column default.
   
   ### References
   
   Tracks #18629 · builds on #18638 · `ColumnReader` SPI #16727
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to