11package io .github .dfa1 .vortex .reader .compute ;
22
3+ import io .github .dfa1 .vortex .core .error .VortexException ;
34import io .github .dfa1 .vortex .core .io .PTypeIO ;
45import io .github .dfa1 .vortex .core .model .DType ;
56import io .github .dfa1 .vortex .reader .array .Array ;
67import io .github .dfa1 .vortex .reader .array .MaskedArray ;
8+ import io .github .dfa1 .vortex .reader .array .MaterializedByteArray ;
79import io .github .dfa1 .vortex .reader .array .MaterializedDoubleArray ;
10+ import io .github .dfa1 .vortex .reader .array .MaterializedFloatArray ;
811import io .github .dfa1 .vortex .reader .array .MaterializedIntArray ;
912import io .github .dfa1 .vortex .reader .array .MaterializedLongArray ;
13+ import io .github .dfa1 .vortex .reader .array .MaterializedShortArray ;
1014import org .junit .jupiter .api .Test ;
1115import org .junit .jupiter .params .ParameterizedTest ;
1216import org .junit .jupiter .params .provider .MethodSource ;
1317
1418import java .lang .foreign .Arena ;
1519import java .lang .foreign .MemorySegment ;
20+ import java .lang .foreign .ValueLayout ;
1621import java .util .Random ;
1722import java .util .stream .IntStream ;
1823import java .util .stream .Stream ;
1924
2025import static org .assertj .core .api .Assertions .assertThat ;
26+ import static org .assertj .core .api .Assertions .assertThatExceptionOfType ;
2127
2228/// Oracle test for the fused [Compute#filteredSum(Array, Predicate, Array)] kernel: the one-pass
2329/// fused result must be bit-identical to an independent boxing reference — a plain per-element
2430/// `if (predicate) acc += value` loop over the same columns. The fused hot lane shares no code with
2531/// that boxing reference, so any divergence is a correctness bug.
2632///
27- /// The randomized cases pair a random primitive filter column (`i64` / `i32` / `f64`, with and
28- /// without nulls) against a random primitive aggregate column, under each comparison predicate, so
29- /// every filter/aggregate domain crossing (long/double, hot lane and the narrow-int generic
30- /// fall-back) is exercised. The explicit cases pin the corners — empty, an all-null filter, an
31- /// all-null aggregate, zero matches, and a full match.
33+ /// The randomized cases pair a random primitive filter column (`i64` / `i32` / `i16` / `i8` — each
34+ /// narrow width randomly unsigned — plus `f32` / `f64`, with and without nulls) against a random
35+ /// primitive aggregate column, under each comparison predicate and under composite / null-test
36+ /// predicates (the generic boxing path), so every filter/aggregate domain crossing (long/double,
37+ /// signed/unsigned, hot lane and the narrow-int generic fall-back) is exercised. The explicit cases
38+ /// pin the corners — empty, an all-null filter, an all-null aggregate, zero matches, a full match,
39+ /// mismatched column lengths, and the empty-range boundary constants of the range lowering.
3240class FusedFilterSumTest {
3341
3442 private static final Arena ARENA = Arena .ofAuto ();
@@ -137,6 +145,37 @@ void doubleFilterLongAggregateCrossesDomains() {
137145 assertFusedMatchesTwoPass (filter , new Predicate .Gt (1.0 ), agg );
138146 }
139147
148+ @ Test
149+ void mismatchedColumnLengthsFailFast () {
150+ // Given columns of different lengths — the kernel must reject them before any scan
151+ Array filter = longColumn (new long []{1 , 2 , 3 }, null );
152+ Array agg = longColumn (new long []{1 , 2 }, null );
153+
154+ // When the fused kernel is asked to fold misaligned columns
155+ // Then it fails fast instead of reading out of bounds
156+ assertThatExceptionOfType (VortexException .class )
157+ .isThrownBy (() -> Compute .filteredSum (filter , new Predicate .Gt (0L ), agg ))
158+ .withMessageContaining ("filter length" );
159+ }
160+
161+ @ Test
162+ void boundaryConstantsLowerToEmptyRanges () {
163+ // Given the two range-lowering guards: Lt(min long) and Gt(max long) can match no value,
164+ // so both must lower to the canonical empty range instead of under/overflowing the bound
165+ Array filter = longColumn (new long []{Long .MIN_VALUE , -1 , 0 , 1 , Long .MAX_VALUE }, null );
166+ Array agg = longColumn (new long []{1 , 2 , 4 , 8 , 16 }, null );
167+
168+ // When the fused kernel filters with each boundary constant
169+ Number ltMin = Compute .filteredSum (filter , new Predicate .Lt (Long .MIN_VALUE ), agg );
170+ Number gtMax = Compute .filteredSum (filter , new Predicate .Gt (Long .MAX_VALUE ), agg );
171+
172+ // Then nothing is selected either way, matching the boxing reference
173+ assertThat (ltMin ).isEqualTo (0L );
174+ assertThat (gtMax ).isEqualTo (0L );
175+ assertFusedMatchesTwoPass (filter , new Predicate .Lt (Long .MIN_VALUE ), agg );
176+ assertFusedMatchesTwoPass (filter , new Predicate .Gt (Long .MAX_VALUE ), agg );
177+ }
178+
140179 private static void assertFusedMatchesTwoPass (Array filter , Predicate predicate , Array agg ) {
141180 Number reference = referenceFilteredSum (filter , predicate , agg );
142181 Number fused = Compute .filteredSum (filter , predicate , agg );
@@ -182,7 +221,13 @@ private static Predicate[] everyComparison(Random random, Array filter) {
182221 new Predicate .Gt (a ),
183222 new Predicate .Lte (a ),
184223 new Predicate .Gte (a ),
185- new Predicate .Between (lo , hi )
224+ new Predicate .Between (lo , hi ),
225+ // Composite and null-test predicates have no fast lane, so these drive the whole
226+ // sweep through the generic boxing path (PredicateEvaluator + Values + Compare).
227+ new Predicate .And (new Predicate .Gte (lo ), new Predicate .Lte (hi )),
228+ new Predicate .Or (new Predicate .Lt (lo ), new Predicate .Gt (hi )),
229+ new Predicate .IsNull (),
230+ new Predicate .IsNotNull ()
186231 };
187232 }
188233
@@ -204,9 +249,12 @@ private static Array randomColumn(Random random, boolean nullable) {
204249
205250 private static Array randomColumn (Random random , boolean nullable , int n ) {
206251 boolean [] valid = nullable ? randomValidity (random , n ) : null ;
207- return switch (random .nextInt (3 )) {
252+ return switch (random .nextInt (6 )) {
208253 case 0 -> longColumn (randomLongs (random , n ), valid );
209254 case 1 -> intColumn (randomLongs (random , n ), valid );
255+ case 2 -> shortColumn (randomLongs (random , n ), valid , random .nextBoolean ());
256+ case 3 -> byteColumn (randomLongs (random , n ), valid , random .nextBoolean ());
257+ case 4 -> floatColumn (randomDoubles (random , n ), valid );
210258 default -> doubleColumn (randomDoubles (random , n ), valid );
211259 };
212260 }
@@ -251,6 +299,30 @@ private static Array intColumn(long[] values, boolean[] valid) {
251299 return wrap (new MaterializedIntArray (DType .I32 , values .length , seg ), valid );
252300 }
253301
302+ private static Array shortColumn (long [] values , boolean [] valid , boolean unsigned ) {
303+ MemorySegment seg = ARENA .allocate (Math .max (2L , values .length * 2L ), 2 );
304+ for (int i = 0 ; i < values .length ; i ++) {
305+ seg .setAtIndex (PTypeIO .LE_SHORT , i , (short ) values [i ]);
306+ }
307+ return wrap (new MaterializedShortArray (unsigned ? DType .U16 : DType .I16 , values .length , seg ), valid );
308+ }
309+
310+ private static Array byteColumn (long [] values , boolean [] valid , boolean unsigned ) {
311+ MemorySegment seg = ARENA .allocate (Math .max (1L , values .length ));
312+ for (int i = 0 ; i < values .length ; i ++) {
313+ seg .set (ValueLayout .JAVA_BYTE , i , (byte ) values [i ]);
314+ }
315+ return wrap (new MaterializedByteArray (unsigned ? DType .U8 : DType .I8 , values .length , seg ), valid );
316+ }
317+
318+ private static Array floatColumn (double [] values , boolean [] valid ) {
319+ MemorySegment seg = ARENA .allocate (Math .max (4L , values .length * 4L ), 4 );
320+ for (int i = 0 ; i < values .length ; i ++) {
321+ seg .setAtIndex (PTypeIO .LE_FLOAT , i , (float ) values [i ]);
322+ }
323+ return wrap (new MaterializedFloatArray (DType .F32 , values .length , seg ), valid );
324+ }
325+
254326 private static Array doubleColumn (double [] values , boolean [] valid ) {
255327 MemorySegment seg = ARENA .allocate (Math .max (8L , values .length * 8L ), 8 );
256328 for (int i = 0 ; i < values .length ; i ++) {
0 commit comments