|
1 | 1 | package io.github.dfa1.vortex.calcite; |
2 | 2 |
|
3 | | -import io.github.dfa1.vortex.core.error.VortexException; |
4 | 3 | import io.github.dfa1.vortex.core.model.DType; |
5 | 4 | import io.github.dfa1.vortex.reader.ArrayStats; |
6 | 5 | import io.github.dfa1.vortex.reader.Chunk; |
|
10 | 9 | import io.github.dfa1.vortex.reader.VortexReader; |
11 | 10 | import io.github.dfa1.vortex.reader.compute.Compare; |
12 | 11 | import io.github.dfa1.vortex.reader.compute.Compute; |
13 | | -import io.github.dfa1.vortex.reader.compute.Mask; |
| 12 | +import io.github.dfa1.vortex.reader.compute.FilteredAggregate; |
14 | 13 | import io.github.dfa1.vortex.reader.compute.Predicate; |
15 | 14 | import io.github.dfa1.vortex.reader.compute.ZoneReducer; |
16 | | -import io.github.dfa1.vortex.reader.array.Array; |
17 | 15 | import io.github.dfa1.vortex.reader.array.BoolArray; |
18 | 16 | import io.github.dfa1.vortex.reader.array.ByteArray; |
19 | 17 | import io.github.dfa1.vortex.reader.array.DoubleArray; |
|
47 | 45 |
|
48 | 46 | import java.io.IOException; |
49 | 47 | import java.io.UncheckedIOException; |
50 | | -import java.lang.foreign.Arena; |
51 | 48 | import java.nio.file.Path; |
52 | 49 | import java.util.ArrayList; |
53 | 50 | import java.util.List; |
@@ -191,10 +188,11 @@ public record FilteredFold(Number sum, Object min, Object max, Long nullCount, l |
191 | 188 | /// Folds an aggregate over the rows a pushed `WHERE` `filter` selects, in two tiers (ADR 0013 §6). |
192 | 189 | /// A zone the filter selects entirely (or excludes entirely) is folded from — or skipped via — its |
193 | 190 | /// footer statistics with no decode; a boundary zone the filter only partially selects is decoded |
194 | | - /// for the aggregate and filter columns, a row-level selection [Mask] is built by evaluating the |
195 | | - /// filter through the compute kernels, and the aggregate is reduced over the masked rows. The two |
196 | | - /// tiers combine into one fold, so the caller answers the filtered aggregate while decoding only |
197 | | - /// the boundary zones. |
| 191 | + /// for the aggregate and filter columns and reduced in one fused pass — the whole filter and the |
| 192 | + /// aggregate fold evaluated together via |
| 193 | + /// [Compute#filteredAggregate(Chunk, RowFilter, String)], with no intermediate selection bitmap. |
| 194 | + /// The two tiers combine into one fold, so the caller answers the filtered aggregate while decoding |
| 195 | + /// only the boundary zones. |
198 | 196 | /// |
199 | 197 | /// `aggColumn` is the column whose values are reduced over the selected rows (`null` for |
200 | 198 | /// `COUNT(*)`, which needs only the selected row count). The result is empty — abandoning to a |
@@ -317,79 +315,30 @@ public Optional<FilteredFold> filteredFold(RowFilter filter, String aggColumn) { |
317 | 315 | } |
318 | 316 |
|
319 | 317 | /// Decodes one boundary chunk and folds the aggregate over the rows the filter selects in it, |
320 | | - /// contributing to the running [Fold]. The chunk and a fresh confined [Arena] for the mask |
321 | | - /// bitmaps are released per zone via try-with-resources, so nothing outlives the reduction. |
| 318 | + /// contributing to the running [Fold]. The chunk is released per zone via try-with-resources, so |
| 319 | + /// nothing outlives the reduction. |
322 | 320 | /// |
323 | | - /// `SUM` is guarded: the compute kernel throws on a non-numeric aggregate column, so a thrown |
324 | | - /// [VortexException] disables the sum (mirroring a zone that records no usable sum) rather than |
325 | | - /// failing the whole fold — a `MIN`/`MAX` over the same column still folds. The aggregate |
326 | | - /// column's null count among the selected rows is the selected count minus the non-null count. |
327 | | - @SuppressWarnings("removal") // transitional — uses the deprecated Mask until the fused multi-column filteredReduce lands |
| 321 | + /// The whole filter — an n-ary `AND` of column-bound [Predicate] leaves — and the aggregate fold |
| 322 | + /// run in one fused pass over the chunk's rows via |
| 323 | + /// [Compute#filteredAggregate(Chunk, RowFilter, String)], with no intermediate selection bitmap. |
| 324 | + /// `SUM` is guarded: the fused kernel reports a `null` sum for a non-numeric aggregate column, |
| 325 | + /// which disables the sum (mirroring a zone that records no usable sum) rather than failing the |
| 326 | + /// whole fold — a `MIN`/`MAX` over the same column still folds. The aggregate column's null count |
| 327 | + /// among the selected rows is the selected count minus the non-null count. |
328 | 328 | private static void foldBoundaryZone(VortexReader reader, int zone, List<String> columns, |
329 | 329 | RowFilter filter, String aggColumn, Fold fold) { |
330 | | - try (Chunk chunk = reader.decodeChunk(zone, columns); |
331 | | - Arena maskArena = Arena.ofConfined()) { |
332 | | - Mask mask = buildMask(chunk, filter, maskArena); |
333 | | - long selected = mask.trueCount(); |
334 | | - fold.keptRows += selected; |
| 330 | + try (Chunk chunk = reader.decodeChunk(zone, columns)) { |
| 331 | + FilteredAggregate aggregate = Compute.filteredAggregate(chunk, filter, aggColumn); |
| 332 | + fold.keptRows += aggregate.selectedRows(); |
335 | 333 | if (aggColumn == null) { |
336 | 334 | return; |
337 | 335 | } |
338 | | - Array aggArray = chunk.column(aggColumn); |
339 | | - if (fold.sumUsable) { |
340 | | - try { |
341 | | - fold.addSum(Compute.sum(aggArray, mask)); |
342 | | - } catch (VortexException e) { |
343 | | - fold.sumUsable = false; // non-numeric agg column — sum unanswerable, like a missing zone sum |
344 | | - } |
345 | | - } |
346 | | - fold.addMin(Compute.min(aggArray, mask)); |
347 | | - fold.addMax(Compute.max(aggArray, mask)); |
348 | | - fold.addNulls(selected - Compute.count(aggArray, mask)); |
349 | | - } |
350 | | - } |
351 | | - |
352 | | - /// Builds the row-level selection [Mask] for a boundary chunk by evaluating `filter` through the |
353 | | - /// compute kernels: each comparison or null-test leaf becomes a [Predicate] filtered against its |
354 | | - /// column's decoded array, and an n-ary `AND` intersects the per-leaf masks (a row is selected |
355 | | - /// only when every leaf accepts it). The kernels apply SQL three-valued logic, so a null row |
356 | | - /// never satisfies a value predicate — matching the fold's null semantics. |
357 | | - @SuppressWarnings("removal") // transitional — uses the deprecated Mask until the fused multi-column filteredReduce lands |
358 | | - private static Mask buildMask(Chunk chunk, RowFilter filter, Arena arena) { |
359 | | - long n = chunk.rowCount(); |
360 | | - List<Mask> masks = new ArrayList<>(); |
361 | | - collectLeafMasks(chunk, filter, arena, masks); |
362 | | - if (masks.isEmpty()) { |
363 | | - return Mask.allTrue(n); // an AND with no leaves constrains nothing (never reached for a boundary) |
364 | | - } |
365 | | - if (masks.size() == 1) { |
366 | | - return masks.getFirst(); |
367 | | - } |
368 | | - // Fold the per-leaf masks with the reader's Mask.and combinator (logical intersect): a row |
369 | | - // survives only when every leaf accepts it (the n-ary AND). The combinator allocates the |
370 | | - // result bitmap off-heap from this boundary zone's arena. |
371 | | - Mask result = masks.getFirst(); |
372 | | - for (int k = 1; k < masks.size(); k++) { |
373 | | - result = result.and(masks.get(k), arena); |
374 | | - } |
375 | | - return result; |
376 | | - } |
377 | | - |
378 | | - /// Walks the filter tree, evaluating each leaf into a per-column [Mask] and collecting them into |
379 | | - /// `out`; the conjuncts of an n-ary `AND` are flattened so [#buildMask] intersects them together. |
380 | | - /// A [RowFilter.Column] already holds the exact compute [Predicate], so the kernel runs it |
381 | | - /// directly against the bound column — no translation seam, since `>=`, `<=` and `<>` are now |
382 | | - /// first-class predicate variants. |
383 | | - @SuppressWarnings("removal") // transitional — uses the deprecated Mask and Compute.filter until the fused multi-column filteredReduce lands |
384 | | - private static void collectLeafMasks(Chunk chunk, RowFilter filter, Arena arena, List<Mask> out) { |
385 | | - switch (filter) { |
386 | | - case RowFilter.And(var parts) -> { |
387 | | - for (RowFilter part : parts) { |
388 | | - collectLeafMasks(chunk, part, arena, out); |
389 | | - } |
390 | | - } |
391 | | - case RowFilter.Column(var col, var predicate) -> |
392 | | - out.add(Compute.filter(chunk.column(col), predicate, arena)); |
| 336 | + // A null sum marks a non-numeric aggregate column — unanswerable, like a missing zone sum; |
| 337 | + // addSum(null) disables only the sum, leaving MIN/MAX to fold. |
| 338 | + fold.addSum(aggregate.sum()); |
| 339 | + fold.addMin(aggregate.min()); |
| 340 | + fold.addMax(aggregate.max()); |
| 341 | + fold.addNulls(aggregate.selectedRows() - aggregate.aggNonNullCount()); |
393 | 342 | } |
394 | 343 | } |
395 | 344 |
|
@@ -622,9 +571,9 @@ private static boolean isUnsigned(DType.Struct struct, String column) { |
622 | 571 | return idx >= 0 && struct.fieldTypes().get(idx) instanceof DType.Primitive p && p.ptype().isUnsigned(); |
623 | 572 | } |
624 | 573 |
|
625 | | - /// Whether `column` is a floating-point primitive, whose compute-kernel compare ([#buildMask]) |
626 | | - /// sorts NaN as the maximum and so cannot soundly build a boundary row mask (the fold abandons |
627 | | - /// such a filter column to the NaN-correct scan). |
| 574 | + /// Whether `column` is a floating-point primitive, whose compute-kernel compare |
| 575 | + /// ([Compare#values(Object, Object, DType)]) sorts NaN as the maximum and so cannot soundly fold a |
| 576 | + /// boundary partition (the fold abandons such a filter column to the NaN-correct scan). |
628 | 577 | private static boolean isFloating(DType.Struct struct, String column) { |
629 | 578 | int idx = struct.fieldNames().indexOf(column); |
630 | 579 | return idx >= 0 && struct.fieldTypes().get(idx) instanceof DType.Primitive p && p.ptype().isFloating(); |
|
0 commit comments