Jackie-Jiang commented on code in PR #19158:
URL: https://github.com/apache/pinot/pull/19158#discussion_r3725226334


##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/CountAggregationFunction.java:
##########
@@ -86,12 +86,17 @@ public void aggregate(int length, AggregationResultHolder 
aggregationResultHolde
     if (blockValSetMap.isEmpty()) {
       
aggregationResultHolder.setValue(aggregationResultHolder.getDoubleResult() + 
length);
     } else if (blockValSetMap.containsKey(STAR_TREE_COUNT_STAR_EXPRESSION)) {
-      // Star-tree pre-aggregated values
-      long[] valueArray = 
blockValSetMap.get(STAR_TREE_COUNT_STAR_EXPRESSION).getLongValuesSV();
-      long count = 0;
-      for (int i = 0; i < length; i++) {
-        count += valueArray[i];
-      }
+      // Star-tree pre-aggregated values. A null-aware star-tree marks the 
groups that aggregated over no non-null

Review Comment:
   Agreed, and looking into it properly the answer turned out to be stronger 
than "not yet" for most of them — I have dropped all four star-tree changes.
   
   For `COUNT` it is never needed: `__COUNT_STAR` is a row count, and a node 
exists precisely because rows mapped to it. The same holds one level up for 
`COUNT_MV`, which reads the pre-aggregated `COUNT` for the column — `COUNT` is 
a counting function whose answer over no input is `0`, so a null-aware 
star-tree would store `0`, never a null marker. `AVG` is also fine as-is, 
because `AvgPair` already encodes emptiness in its own count field (`getCount() 
!= 0` is on master today) and so needs no null vector.
   
   `SUM_MV` is the only one where a null could genuinely arise later — a 
primitive `double` sum cannot distinguish "sum of nothing" from `0` — but as 
you say that is untestable until the star-tree side exists, so it belongs with 
that work.
   
   Codecov agrees with the outcome: those four accounted for 41 of the 53 
uncovered lines in the patch.



##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunction.java:
##########
@@ -36,6 +36,53 @@
 /// The implementation should be stateless, and can be shared among multiple 
segments in multiple threads. The result
 /// for each segment should be stored and passed in via the result holder.
 ///
+/// ## Null contract
+///
+/// Null handling is a per-query flag, and the two modes place different 
requirements on an implementation.
+///
+/// ### Null handling disabled
+///
+/// Null values are read as the column's default, so no input value is ever 
null. An implementation keeps a primitive
+/// result holder, performs no null tracking, and needs no null check while 
aggregating.
+///
+/// An untouched accumulator is indistinguishable from one that aggregated to 
the type's identity, so the empty
+/// multiset has no representation in this mode: its answer is whatever the 
accumulator's initial state renders to,
+/// `0` for `SUM` or `+Infinity` for `MIN`. Where the type has no identity to 
render, the intermediate result is
+/// `null` instead: `MAXSTRING`, `MINSTRING` and `ANYVALUE` are object-backed 
and have no empty value to return. So
+/// [#extractFinalResult] must accept `null` in this mode as well.
+///
+/// ### Null handling enabled
+///
+/// SQL evaluates an aggregate over the multiset of its **non-null** input 
values, and separately defines a result for
+/// the empty multiset. This mode models those as two distinct things:
+/// - A `null` **intermediate result** means the empty multiset: nothing was 
aggregated, either because no row matched
+///   or because every matching value was null. It carries no per-function 
meaning, which is what makes it correct
+///   for the aggregation methods to skip null rows outright rather than fold 
them in.
+/// - [#extractFinalResult] decides what the empty multiset means for this 
aggregation, and is the only method that
+///   does. `COUNT` and the distinct counts return `0`; `SUM`, `MIN`, `MAX`, 
`AVG` and the percentiles return `null`.
+///
+/// An implementation switches to a nullable result holder only in this mode, 
which keeps the boxing cost on the
+/// opt-in path.
+///
+/// ### Both modes
+///
+/// The empty multiset is the identity of merging, and that carries no 
per-function meaning, so it is resolved once by

Review Comment:
   Fair point, and it prompted a wider fix than the one line — the term is gone 
from the change entirely (44 occurrences across 30 files), replaced with the 
plainer "nothing was aggregated" that the contract was already using in its own 
definition. It is SQL vocabulary that is not otherwise used in Pinot, so it was 
buying nothing.
   
   On the substance of your question: the split is that the *representation* is 
uniform across every implementation — a `null` intermediate result means 
nothing was aggregated — which is what makes it correct to settle the merge 
identity once in the caller, and that part does belong on the interface. What 
is per-function is the *answer*, and the contract delegates that explicitly to 
`extractFinalResult` (`0` for the counting functions, `null` for the value 
functions).
   
   The line you flagged is about merging, where there is no per-function 
meaning at all, so it now names the concrete thing instead of the concept:
   
   > A `null` operand is the identity of merging, and that carries no 
per-function meaning, so it is resolved once by the caller rather than in every 
implementation.



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