Skip to content

Commit bb25bd9

Browse files
committed
#177 Add In predicate support for row group and page level filter pushdown
1 parent 45cc93f commit bb25bd9

5 files changed

Lines changed: 297 additions & 0 deletions

File tree

core/src/main/java/dev/hardwood/internal/reader/PageFilterEvaluator.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
import dev.hardwood.reader.FilterPredicate;
2222
import dev.hardwood.reader.FilterPredicate.And;
2323
import dev.hardwood.reader.FilterPredicate.BinaryColumnPredicate;
24+
import dev.hardwood.reader.FilterPredicate.BinaryInPredicate;
2425
import dev.hardwood.reader.FilterPredicate.BooleanColumnPredicate;
2526
import dev.hardwood.reader.FilterPredicate.DoubleColumnPredicate;
2627
import dev.hardwood.reader.FilterPredicate.FloatColumnPredicate;
2728
import dev.hardwood.reader.FilterPredicate.IntColumnPredicate;
29+
import dev.hardwood.reader.FilterPredicate.IntInPredicate;
2830
import dev.hardwood.reader.FilterPredicate.LongColumnPredicate;
31+
import dev.hardwood.reader.FilterPredicate.LongInPredicate;
2932
import dev.hardwood.reader.FilterPredicate.Not;
3033
import dev.hardwood.reader.FilterPredicate.Or;
3134
import dev.hardwood.schema.FileSchema;
@@ -98,6 +101,24 @@ private static RowRanges evaluate(FilterPredicate predicate, RowGroup rowGroup,
98101
return RowGroupFilterEvaluator.canDropCompared(p.op(), cmpMin, cmpMax,
99102
StatisticsDecoder.compareBinary(min, max));
100103
});
104+
case IntInPredicate p -> evaluateLeaf(p.column(), rowGroup, schema, indexBuffers, rowCount,
105+
(ci, i) -> {
106+
int min = StatisticsDecoder.decodeInt(ci.minValues().get(i));
107+
int max = StatisticsDecoder.decodeInt(ci.maxValues().get(i));
108+
return RowGroupFilterEvaluator.canDropIntIn(p.values(), min, max);
109+
});
110+
case LongInPredicate p -> evaluateLeaf(p.column(), rowGroup, schema, indexBuffers, rowCount,
111+
(ci, i) -> {
112+
long min = StatisticsDecoder.decodeLong(ci.minValues().get(i));
113+
long max = StatisticsDecoder.decodeLong(ci.maxValues().get(i));
114+
return RowGroupFilterEvaluator.canDropLongIn(p.values(), min, max);
115+
});
116+
case BinaryInPredicate p -> evaluateLeaf(p.column(), rowGroup, schema, indexBuffers, rowCount,
117+
(ci, i) -> {
118+
byte[] min = ci.minValues().get(i);
119+
byte[] max = ci.maxValues().get(i);
120+
return RowGroupFilterEvaluator.canDropBinaryIn(p.values(), min, max);
121+
});
101122
case And a -> {
102123
RowRanges result = RowRanges.all(rowCount);
103124
for (FilterPredicate child : a.filters()) {

core/src/main/java/dev/hardwood/internal/reader/RowGroupFilterEvaluator.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
import dev.hardwood.reader.FilterPredicate;
1616
import dev.hardwood.reader.FilterPredicate.And;
1717
import dev.hardwood.reader.FilterPredicate.BinaryColumnPredicate;
18+
import dev.hardwood.reader.FilterPredicate.BinaryInPredicate;
1819
import dev.hardwood.reader.FilterPredicate.BooleanColumnPredicate;
1920
import dev.hardwood.reader.FilterPredicate.DoubleColumnPredicate;
2021
import dev.hardwood.reader.FilterPredicate.FloatColumnPredicate;
2122
import dev.hardwood.reader.FilterPredicate.IntColumnPredicate;
23+
import dev.hardwood.reader.FilterPredicate.IntInPredicate;
2224
import dev.hardwood.reader.FilterPredicate.LongColumnPredicate;
25+
import dev.hardwood.reader.FilterPredicate.LongInPredicate;
2326
import dev.hardwood.reader.FilterPredicate.Not;
2427
import dev.hardwood.reader.FilterPredicate.Or;
2528
import dev.hardwood.schema.FileSchema;
@@ -46,6 +49,9 @@ public static boolean canDropRowGroup(FilterPredicate predicate, RowGroup rowGro
4649
case DoubleColumnPredicate p -> evaluateDouble(p, rowGroup, schema);
4750
case BooleanColumnPredicate p -> evaluateBoolean(p, rowGroup, schema);
4851
case BinaryColumnPredicate p -> evaluateBinary(p, rowGroup, schema);
52+
case IntInPredicate p -> evaluateIntIn(p, rowGroup, schema);
53+
case LongInPredicate p -> evaluateLongIn(p, rowGroup, schema);
54+
case BinaryInPredicate p -> evaluateBinaryIn(p, rowGroup, schema);
4955
case And a -> {
5056
for (FilterPredicate f : a.filters()) {
5157
if (canDropRowGroup(f, rowGroup, schema)) {
@@ -202,6 +208,80 @@ private static boolean evaluateBinary(BinaryColumnPredicate p, RowGroup rowGroup
202208
StatisticsDecoder.compareBinary(min, max));
203209
}
204210

211+
private static boolean evaluateIntIn(IntInPredicate p, RowGroup rowGroup, FileSchema schema) {
212+
Statistics stats = findStatistics(p.column(), rowGroup, schema);
213+
if (stats == null || stats.minValue() == null || stats.maxValue() == null) {
214+
return false;
215+
}
216+
int min = StatisticsDecoder.decodeInt(stats.minValue());
217+
int max = StatisticsDecoder.decodeInt(stats.maxValue());
218+
for (int value : p.values()) {
219+
if (value >= min && value <= max) {
220+
return false;
221+
}
222+
}
223+
return true;
224+
}
225+
226+
private static boolean evaluateLongIn(LongInPredicate p, RowGroup rowGroup, FileSchema schema) {
227+
Statistics stats = findStatistics(p.column(), rowGroup, schema);
228+
if (stats == null || stats.minValue() == null || stats.maxValue() == null) {
229+
return false;
230+
}
231+
long min = StatisticsDecoder.decodeLong(stats.minValue());
232+
long max = StatisticsDecoder.decodeLong(stats.maxValue());
233+
for (long value : p.values()) {
234+
if (value >= min && value <= max) {
235+
return false;
236+
}
237+
}
238+
return true;
239+
}
240+
241+
private static boolean evaluateBinaryIn(BinaryInPredicate p, RowGroup rowGroup, FileSchema schema) {
242+
Statistics stats = findStatistics(p.column(), rowGroup, schema);
243+
if (stats == null || stats.minValue() == null || stats.maxValue() == null) {
244+
return false;
245+
}
246+
byte[] min = stats.minValue();
247+
byte[] max = stats.maxValue();
248+
for (byte[] value : p.values()) {
249+
if (StatisticsDecoder.compareBinary(value, min) >= 0
250+
&& StatisticsDecoder.compareBinary(value, max) <= 0) {
251+
return false;
252+
}
253+
}
254+
return true;
255+
}
256+
257+
static boolean canDropIntIn(int[] values, int min, int max) {
258+
for (int value : values) {
259+
if (value >= min && value <= max) {
260+
return false;
261+
}
262+
}
263+
return true;
264+
}
265+
266+
static boolean canDropLongIn(long[] values, long min, long max) {
267+
for (long value : values) {
268+
if (value >= min && value <= max) {
269+
return false;
270+
}
271+
}
272+
return true;
273+
}
274+
275+
static boolean canDropBinaryIn(byte[][] values, byte[] min, byte[] max) {
276+
for (byte[] value : values) {
277+
if (StatisticsDecoder.compareBinary(value, min) >= 0
278+
&& StatisticsDecoder.compareBinary(value, max) <= 0) {
279+
return false;
280+
}
281+
}
282+
return true;
283+
}
284+
205285
// ==================== Generic comparison logic ====================
206286

207287
/// Determines if a range can be dropped given integer-comparable min/max statistics.

core/src/main/java/dev/hardwood/reader/FilterPredicate.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public sealed interface FilterPredicate
4040
FilterPredicate.DoubleColumnPredicate,
4141
FilterPredicate.BooleanColumnPredicate,
4242
FilterPredicate.BinaryColumnPredicate,
43+
FilterPredicate.IntInPredicate,
44+
FilterPredicate.LongInPredicate,
45+
FilterPredicate.BinaryInPredicate,
4346
FilterPredicate.And,
4447
FilterPredicate.Or,
4548
FilterPredicate.Not {
@@ -190,6 +193,22 @@ static FilterPredicate gtEq(String column, String value) {
190193
return new BinaryColumnPredicate(column, Operator.GT_EQ, value.getBytes(StandardCharsets.UTF_8));
191194
}
192195

196+
static FilterPredicate in(String column, int... values) {
197+
return new IntInPredicate(column, values);
198+
}
199+
200+
static FilterPredicate in(String column, long... values) {
201+
return new LongInPredicate(column, values);
202+
}
203+
204+
static FilterPredicate inStrings(String column, String... values) {
205+
byte[][] encoded = new byte[values.length][];
206+
for (int i = 0; i < values.length; i++) {
207+
encoded[i] = values[i].getBytes(StandardCharsets.UTF_8);
208+
}
209+
return new BinaryInPredicate(column, encoded);
210+
}
211+
193212
// ==================== Logical Combinators ====================
194213

195214
static FilterPredicate and(FilterPredicate left, FilterPredicate right) {
@@ -247,6 +266,51 @@ public int hashCode() {
247266
}
248267
}
249268

269+
record IntInPredicate(String column, int[] values) implements FilterPredicate {
270+
271+
@Override
272+
public boolean equals(Object o) {
273+
if (this == o) return true;
274+
if (!(o instanceof IntInPredicate that)) return false;
275+
return column.equals(that.column) && Arrays.equals(values, that.values);
276+
}
277+
278+
@Override
279+
public int hashCode() {
280+
return 31 * column.hashCode() + Arrays.hashCode(values);
281+
}
282+
}
283+
284+
record LongInPredicate(String column, long[] values) implements FilterPredicate {
285+
286+
@Override
287+
public boolean equals(Object o) {
288+
if (this == o) return true;
289+
if (!(o instanceof LongInPredicate that)) return false;
290+
return column.equals(that.column) && Arrays.equals(values, that.values);
291+
}
292+
293+
@Override
294+
public int hashCode() {
295+
return 31 * column.hashCode() + Arrays.hashCode(values);
296+
}
297+
}
298+
299+
record BinaryInPredicate(String column, byte[][] values) implements FilterPredicate {
300+
301+
@Override
302+
public boolean equals(Object o) {
303+
if (this == o) return true;
304+
if (!(o instanceof BinaryInPredicate that)) return false;
305+
return column.equals(that.column) && Arrays.deepEquals(values, that.values);
306+
}
307+
308+
@Override
309+
public int hashCode() {
310+
return 31 * column.hashCode() + Arrays.deepHashCode(values);
311+
}
312+
}
313+
250314
// ==================== Logical Combinator Records ====================
251315

252316
record And(List<FilterPredicate> filters) implements FilterPredicate {

core/src/test/java/dev/hardwood/FilterPredicateTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,77 @@ void testUnknownColumnNeverDrop() {
370370
FilterPredicate.eq("nonexistent", 42), rg, schema)).isFalse();
371371
}
372372

373+
@Test
374+
void testIntInPredicateCreation() {
375+
FilterPredicate p = FilterPredicate.in("id", 1, 5, 10);
376+
assertThat(p).isInstanceOf(FilterPredicate.IntInPredicate.class);
377+
FilterPredicate.IntInPredicate ip = (FilterPredicate.IntInPredicate) p;
378+
assertThat(ip.column()).isEqualTo("id");
379+
assertThat(ip.values()).containsExactly(1, 5, 10);
380+
}
381+
382+
@Test
383+
void testLongInPredicateCreation() {
384+
FilterPredicate p = FilterPredicate.in("ts", 100L, 200L);
385+
assertThat(p).isInstanceOf(FilterPredicate.LongInPredicate.class);
386+
}
387+
388+
@Test
389+
void testStringInPredicateCreation() {
390+
FilterPredicate p = FilterPredicate.inStrings("city", "NYC", "LA");
391+
assertThat(p).isInstanceOf(FilterPredicate.BinaryInPredicate.class);
392+
}
393+
394+
@Test
395+
void testCanDropWithIntIn() {
396+
RowGroup rg = createIntRowGroup(10, 20);
397+
FileSchema schema = createIntSchema();
398+
399+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
400+
FilterPredicate.in("col", 1, 5, 8), rg, schema)).isTrue();
401+
402+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
403+
FilterPredicate.in("col", 25, 30), rg, schema)).isTrue();
404+
405+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
406+
FilterPredicate.in("col", 5, 15, 25), rg, schema)).isFalse();
407+
408+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
409+
FilterPredicate.in("col", 1, 10), rg, schema)).isFalse();
410+
}
411+
412+
@Test
413+
void testCanDropWithLongIn() {
414+
RowGroup rg = createLongRowGroup(100L, 200L);
415+
FileSchema schema = createLongSchema();
416+
417+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
418+
FilterPredicate.in("col", 50L, 80L), rg, schema)).isTrue();
419+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
420+
FilterPredicate.in("col", 50L, 150L), rg, schema)).isFalse();
421+
}
422+
423+
@Test
424+
void testCanDropWithStringIn() {
425+
RowGroup rg = createBinaryRowGroup("banana".getBytes(), "date".getBytes());
426+
FileSchema schema = createBinarySchema();
427+
428+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
429+
FilterPredicate.inStrings("col", "apple", "elderberry"), rg, schema)).isTrue();
430+
431+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
432+
FilterPredicate.inStrings("col", "apple", "cherry"), rg, schema)).isFalse();
433+
}
434+
435+
@Test
436+
void testCanDropWithInMissingStatistics() {
437+
RowGroup rg = createRowGroupWithoutStatistics();
438+
FileSchema schema = createIntSchema();
439+
440+
assertThat(RowGroupFilterEvaluator.canDropRowGroup(
441+
FilterPredicate.in("col", 1, 2, 3), rg, schema)).isFalse();
442+
}
443+
373444
// ==================== Helpers ====================
374445

375446
private static RowGroup createIntRowGroup(int min, int max) {

core/src/test/java/dev/hardwood/internal/reader/PageFilterEvaluatorTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,67 @@ private static ColumnIndex longColumnIndex(long[] mins, long[] maxs) {
479479
ColumnIndex.BoundaryOrder.UNORDERED, null);
480480
}
481481

482+
@Test
483+
void testIntInPageFiltering() {
484+
RowRanges ranges = PageFilterEvaluator.evaluatePages(INT_COLUMN_INDEX, THREE_PAGE_OFFSET_INDEX, THREE_PAGE_ROW_COUNT,
485+
(ci, i) -> {
486+
int min = StatisticsDecoder.decodeInt(ci.minValues().get(i));
487+
int max = StatisticsDecoder.decodeInt(ci.maxValues().get(i));
488+
return RowGroupFilterEvaluator.canDropIntIn(new int[]{ 5, 15 }, min, max);
489+
});
490+
assertTrue(ranges.overlapsPage(0, 30));
491+
assertTrue(ranges.overlapsPage(30, 60));
492+
assertFalse(ranges.overlapsPage(60, 90));
493+
}
494+
495+
@Test
496+
void testIntInPageFilteringAllOutside() {
497+
RowRanges ranges = PageFilterEvaluator.evaluatePages(INT_COLUMN_INDEX, THREE_PAGE_OFFSET_INDEX, THREE_PAGE_ROW_COUNT,
498+
(ci, i) -> {
499+
int min = StatisticsDecoder.decodeInt(ci.minValues().get(i));
500+
int max = StatisticsDecoder.decodeInt(ci.maxValues().get(i));
501+
return RowGroupFilterEvaluator.canDropIntIn(new int[]{ 50, 60 }, min, max);
502+
});
503+
assertFalse(ranges.overlapsPage(0, 30));
504+
assertFalse(ranges.overlapsPage(30, 60));
505+
assertFalse(ranges.overlapsPage(60, 90));
506+
}
507+
508+
@Test
509+
void testLongInPageFiltering() {
510+
ColumnIndex longIdx = longColumnIndex(new long[]{ 100, 200, 300 }, new long[]{ 199, 299, 399 });
511+
OffsetIndex oi = offsetIndex(30, 30, 30);
512+
513+
RowRanges ranges = PageFilterEvaluator.evaluatePages(longIdx, oi, 90,
514+
(ci, i) -> {
515+
long min = StatisticsDecoder.decodeLong(ci.minValues().get(i));
516+
long max = StatisticsDecoder.decodeLong(ci.maxValues().get(i));
517+
return RowGroupFilterEvaluator.canDropLongIn(new long[]{ 150, 350 }, min, max);
518+
});
519+
assertTrue(ranges.overlapsPage(0, 30));
520+
assertFalse(ranges.overlapsPage(30, 60));
521+
assertTrue(ranges.overlapsPage(60, 90));
522+
}
523+
524+
@Test
525+
void testBinaryInPageFiltering() {
526+
ColumnIndex binIdx = binaryColumnIndex(
527+
List.of("apple".getBytes(StandardCharsets.UTF_8), "date".getBytes(StandardCharsets.UTF_8)),
528+
List.of("cherry".getBytes(StandardCharsets.UTF_8), "fig".getBytes(StandardCharsets.UTF_8)));
529+
OffsetIndex oi = offsetIndex(30, 30);
530+
531+
RowRanges ranges = PageFilterEvaluator.evaluatePages(binIdx, oi, 60,
532+
(ci, i) -> {
533+
byte[] min = ci.minValues().get(i);
534+
byte[] max = ci.maxValues().get(i);
535+
return RowGroupFilterEvaluator.canDropBinaryIn(
536+
new byte[][]{ "banana".getBytes(StandardCharsets.UTF_8), "zebra".getBytes(StandardCharsets.UTF_8) },
537+
min, max);
538+
});
539+
assertTrue(ranges.overlapsPage(0, 30));
540+
assertFalse(ranges.overlapsPage(30, 60));
541+
}
542+
482543
private static ColumnIndex floatColumnIndex(float[] mins, float[] maxs) {
483544
List<byte[]> minValues = new ArrayList<>();
484545
List<byte[]> maxValues = new ArrayList<>();

0 commit comments

Comments
 (0)