This is an automated email from the ASF dual-hosted git repository.

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new bab970497f8 Compare rendered answers in the aggregation null contract 
test (#19220)
bab970497f8 is described below

commit bab970497f89b7253707e42b98a0e58e19e0f221
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Tue Aug 11 17:17:28 2026 -0700

    Compare rendered answers in the aggregation null contract test (#19220)
---
 .../AggregationFunctionNullContractTest.java       | 75 +++++++++++-----------
 1 file changed, 38 insertions(+), 37 deletions(-)

diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
index 23af0770ee6..60a029c6501 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
@@ -64,7 +64,7 @@ public class AggregationFunctionNullContractTest {
       "(column, column2, 'LONG')",
       // histogram(column, lower, upper, numBins)
       "(column, 0, 1000, 10)",
-      // the funnel family: (timestampColumn, windowMillis, numSteps, 
stepPredicate...)
+      // The funnel family: (timestampColumn, windowMillis, numSteps, 
stepPredicate...)
       "(column, '1000', 2, column2 = 'a', column2 = 'b')",
       // funnelStepDurationStats takes a trailing settings literal
       "(column, '1000', 2, column2 = 'a', column2 = 'b', 
'durationFunctions=count')",
@@ -204,7 +204,7 @@ public class AggregationFunctionNullContractTest {
         AggregationFunctionType.STDDEVPOP, AggregationFunctionType.PERCENTILE,
         AggregationFunctionType.PERCENTILEEST, 
AggregationFunctionType.PERCENTILETDIGEST,
         AggregationFunctionType.PERCENTILEKLL, 
AggregationFunctionType.PERCENTILESMARTTDIGEST}) {
-      // built through the shared argument shapes: the percentile families 
disagree on whether the percentile is a
+      // Built through the shared argument shapes: the percentile families 
disagree on whether the percentile is a
       // name suffix or an argument, and only some accept both
       AggregationFunction function = tryCreate(type, true);
       assertNotNull(function, "Could not construct " + type.getName());
@@ -222,44 +222,36 @@ public class AggregationFunctionNullContractTest {
   /// only if it both has null-aware aggregation and receives the query's 
option, so the set is a direct read-out of
   /// which functions the option actually reaches.
   ///
-  /// The multi-value entries are the interesting ones: they used to hard-code 
the option off, so it had no effect on
-  /// them whatever the query asked for. The raw variants reach the option 
through the function they delegate to, so
-  /// threading it into one of those changes the raw variant alongside it.
+  /// The raw variants reach the option through the function they delegate to, 
so threading it into one of those
+  /// changes the raw variant alongside it. Absence is as meaningful as 
presence: a function missing from here either
+  /// never receives the option, which is the first known deviation on 
[AggregationFunction], or does not skip null
+  /// rows.
   ///
-  /// The set is bounded by what the harness can drive, so it is a read-out of 
the functions the option reaches among
-  /// those, not of every function that takes it — see 
[#NOT_EXERCISABLE_BY_SYNTHETIC_BLOCK].
+  /// Two bounds on how much this set proves. It only covers what the harness 
can drive — see
+  /// [#NOT_EXERCISABLE_BY_SYNTHETIC_BLOCK] — and it compares the **rendered** 
answers, because several functions
+  /// return a serializer that implements no `equals`; comparing those objects 
would compare identities and report
+  /// every one of them as honouring the option.
   private static final Set<AggregationFunctionType> HONOURS_NULL_HANDLING = 
Set.of(
-      // single-value functions, which have always taken the option
       AggregationFunctionType.COUNT, AggregationFunctionType.MIN, 
AggregationFunctionType.MAX,
       AggregationFunctionType.SUM, AggregationFunctionType.SUM0, 
AggregationFunctionType.AVG,
       AggregationFunctionType.MODE, AggregationFunctionType.ANYVALUE, 
AggregationFunctionType.MINMAXRANGE,
       AggregationFunctionType.DISTINCTCOUNT, 
AggregationFunctionType.DISTINCTCOUNTOFFHEAP,
-      AggregationFunctionType.DISTINCTSUM, AggregationFunctionType.DISTINCTAVG,
-      AggregationFunctionType.DISTINCTCOUNTRAWHLL, 
AggregationFunctionType.DISTINCTCOUNTRAWHLLPLUS,
-      AggregationFunctionType.DISTINCTCOUNTRAWULL, 
AggregationFunctionType.PERCENTILE,
+      AggregationFunctionType.DISTINCTSUM, 
AggregationFunctionType.DISTINCTAVG, AggregationFunctionType.PERCENTILE,
       AggregationFunctionType.PERCENTILEEST, 
AggregationFunctionType.PERCENTILERAWEST,
       AggregationFunctionType.PERCENTILETDIGEST, 
AggregationFunctionType.PERCENTILERAWTDIGEST,
       AggregationFunctionType.PERCENTILESMARTTDIGEST, 
AggregationFunctionType.PERCENTILEKLL,
       AggregationFunctionType.PERCENTILERAWKLL, 
AggregationFunctionType.VARPOP, AggregationFunctionType.VARSAMP,
-      AggregationFunctionType.STDDEVPOP, AggregationFunctionType.STDDEVSAMP,
-      // multi-value variants that already took the option
-      AggregationFunctionType.MINMV, AggregationFunctionType.MAXMV, 
AggregationFunctionType.SUMMV,
-      AggregationFunctionType.AVGMV, AggregationFunctionType.MINMAXRANGEMV,
-      AggregationFunctionType.DISTINCTCOUNTRAWHLLMV, 
AggregationFunctionType.DISTINCTCOUNTRAWHLLPLUSMV,
-      // multi-value variants this change threads the option into; they 
hard-coded it off before
-      AggregationFunctionType.DISTINCTCOUNTMV, 
AggregationFunctionType.DISTINCTSUMMV,
-      AggregationFunctionType.DISTINCTAVGMV, 
AggregationFunctionType.PERCENTILEMV,
-      AggregationFunctionType.PERCENTILEESTMV, 
AggregationFunctionType.PERCENTILERAWESTMV,
-      AggregationFunctionType.PERCENTILEKLLMV, 
AggregationFunctionType.PERCENTILETDIGESTMV,
-      AggregationFunctionType.PERCENTILERAWTDIGESTMV, 
AggregationFunctionType.PERCENTILERAWKLLMV,
-      // functions the harness reaches only once it probes block shapes and 
feeds every input column; they have
-      // always taken the option, so these entries are a gain in measurement 
rather than a change in behaviour
-      AggregationFunctionType.MINSTRING, AggregationFunctionType.MAXSTRING, 
AggregationFunctionType.MINLONG,
-      AggregationFunctionType.MAXLONG, AggregationFunctionType.SUMINT, 
AggregationFunctionType.SUMLONG,
-      AggregationFunctionType.SUMPRECISION, 
AggregationFunctionType.FIRSTWITHTIME,
-      AggregationFunctionType.LASTWITHTIME, 
AggregationFunctionType.DISTINCTCOUNTRAWCPCSKETCH,
-      AggregationFunctionType.FREQUENTSTRINGSSKETCH, 
AggregationFunctionType.FREQUENTLONGSSKETCH,
-      AggregationFunctionType.ARRAYAGG, AggregationFunctionType.LISTAGG
+      AggregationFunctionType.STDDEVPOP, AggregationFunctionType.STDDEVSAMP, 
AggregationFunctionType.MINMV,
+      AggregationFunctionType.MAXMV, AggregationFunctionType.SUMMV, 
AggregationFunctionType.AVGMV,
+      AggregationFunctionType.MINMAXRANGEMV, 
AggregationFunctionType.DISTINCTCOUNTMV,
+      AggregationFunctionType.DISTINCTSUMMV, 
AggregationFunctionType.DISTINCTAVGMV,
+      AggregationFunctionType.PERCENTILEMV, 
AggregationFunctionType.PERCENTILEESTMV,
+      AggregationFunctionType.PERCENTILERAWESTMV, 
AggregationFunctionType.PERCENTILEKLLMV,
+      AggregationFunctionType.PERCENTILETDIGESTMV, 
AggregationFunctionType.PERCENTILERAWTDIGESTMV,
+      AggregationFunctionType.PERCENTILERAWKLLMV, 
AggregationFunctionType.MINSTRING, AggregationFunctionType.MAXSTRING,
+      AggregationFunctionType.MINLONG, AggregationFunctionType.MAXLONG, 
AggregationFunctionType.SUMINT,
+      AggregationFunctionType.SUMLONG, AggregationFunctionType.SUMPRECISION, 
AggregationFunctionType.FIRSTWITHTIME,
+      AggregationFunctionType.LASTWITHTIME, AggregationFunctionType.ARRAYAGG, 
AggregationFunctionType.LISTAGG
   );
 
   /// Functions this test cannot drive with a one-column synthetic block, 
pinned so that a silent drop-out is always a
@@ -299,20 +291,22 @@ public class AggregationFunctionNullContractTest {
       boolean driven = false;
       for (Supplier<BlockValSet> shape : BLOCK_SHAPES) {
         try {
-          // the same shape drives both modes, so that the comparison below is 
between the modes and not the inputs
+          // The same shape drives both modes, so that the comparison below is 
between the modes and not the inputs
           enabled = aggregateAllNulls(type, true, shape);
           disabled = aggregateAllNulls(type, false, shape);
           driven = true;
           break;
         } catch (RuntimeException e) {
-          // this shape reads a value type the function rejects; fall through 
to the next
+          // This shape reads a value type the function rejects; fall through 
to the next
         }
       }
       if (!driven) {
         notExercisable.add(type);
         continue;
       }
-      if (!Objects.equals(enabled, disabled)) {
+      // Deep, because a rendered array compares by identity otherwise and 
every array-valued function would look
+      // like it honours the option
+      if (!Objects.deepEquals(enabled, disabled)) {
         honours.add(type);
       }
     }
@@ -336,7 +330,13 @@ public class AggregationFunctionNullContractTest {
             + "\n  No longer honouring it, which is a regression unless the 
entry is stale: " + lost);
   }
 
-  /// Aggregates a block whose rows are all null and returns the final result.
+  /// Aggregates a block whose rows are all null and returns the final result 
**as the broker would render it**.
+  ///
+  /// The rendered form is what the two modes are compared on, not the object 
itself. Several functions answer with a
+  /// serializer — `SerializedKLL`, `SerializedTDigest`, the sketch wrappers — 
and none of those implement `equals`,
+  /// so comparing the objects compares identities: two runs are never equal, 
every such function looks like it
+  /// honours the option, and the check that one has *stopped* honouring it 
can never fail. Converting through
+  /// [AggregationFunction#getFinalResultColumnType] gives the value a client 
would see, which compares properly.
   ///
   /// Every input expression the function declares is given a block, not just 
the first, so that the functions taking
   /// more than one column are driven rather than failing on a missing entry.
@@ -346,17 +346,18 @@ public class AggregationFunctionNullContractTest {
     assertNotNull(function, "Could not construct " + type.getName());
     Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
     for (Object inputExpression : function.getInputExpressions()) {
-      // every declared input gets a block, literals included: a function that 
took a literal for one of its columns
+      // Every declared input gets a block, literals included: a function that 
took a literal for one of its columns
       // still looks that expression up in the map, and a missing entry makes 
it throw and drop out of the census
       blockValSetMap.put((ExpressionContext) inputExpression, 
blockShape.get());
     }
-    // a function whose only argument is a literal, such as COUNT(*), still 
needs one block to read a length from
+    // A function whose only argument is a literal, such as COUNT(*), still 
needs one block to read a length from
     if (blockValSetMap.isEmpty()) {
       blockValSetMap.put(ExpressionContext.forIdentifier("column"), 
blockShape.get());
     }
     AggregationResultHolder resultHolder = 
function.createAggregationResultHolder();
     function.aggregate(NUM_DOCS, resultHolder, blockValSetMap);
-    return 
function.extractFinalResult(function.extractAggregationResult(resultHolder));
+    Object finalResult = 
function.extractFinalResult(function.extractAggregationResult(resultHolder));
+    return finalResult != null ? 
function.getFinalResultColumnType().convert(finalResult) : null;
   }
 
   /// All-null block shapes, tried in order until one drives the function.


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

Reply via email to