001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.filter;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertNotNull;
022import static org.junit.jupiter.api.Assertions.assertNull;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.IOException;
026import java.util.Arrays;
027import org.apache.hadoop.hbase.Cell;
028import org.apache.hadoop.hbase.CellComparator;
029import org.apache.hadoop.hbase.KeyValue;
030import org.apache.hadoop.hbase.filter.FilterList.Operator;
031import org.apache.hadoop.hbase.testclassification.FilterTests;
032import org.apache.hadoop.hbase.testclassification.SmallTests;
033import org.apache.hadoop.hbase.util.Bytes;
034import org.junit.jupiter.api.Tag;
035import org.junit.jupiter.api.Test;
036
037/**
038 * Unit tests for {@code getHintForRejectedRow} and {@code getSkipHint} delegation in composite
039 * filters ({@link FilterList}, {@link SkipFilter}, {@link WhileMatchFilter}).
040 */
041@Tag(FilterTests.TAG)
042@Tag(SmallTests.TAG)
043public class TestFilterListHintDelegation {
044
045  private static final byte[] ROW_A = Bytes.toBytes("rowA");
046  private static final byte[] ROW_B = Bytes.toBytes("rowB");
047  private static final byte[] ROW_C = Bytes.toBytes("rowC");
048  private static final byte[] FAMILY = Bytes.toBytes("f");
049  private static final byte[] QUALIFIER = Bytes.toBytes("q");
050
051  private static KeyValue kv(byte[] row) {
052    return new KeyValue(row, FAMILY, QUALIFIER, 1L, KeyValue.Type.Put, Bytes.toBytes("v"));
053  }
054
055  /**
056   * Filter that returns a fixed hint from {@code getHintForRejectedRow} without overriding
057   * {@code filterRowKey}. Used in OR / SkipFilter / WhileMatchFilter tests where the AND
058   * per-sub-filter rejection tracking does not apply.
059   */
060  private static FilterBase fixedRejectedRowHintFilter(Cell hint) {
061    return new FilterBase() {
062      @Override
063      public Cell getHintForRejectedRow(Cell firstRowCell) {
064        return hint;
065      }
066    };
067  }
068
069  /**
070   * Filter that rejects every row via {@code filterRowKey} and returns a fixed hint. Used in AND
071   * tests where {@code FilterListWithAND.getHintForRejectedRow} requires sub-filters to have
072   * individually rejected via {@code filterRowKey} before being consulted.
073   */
074  private static FilterBase rejectingRowHintFilter(Cell hint) {
075    return new FilterBase() {
076      @Override
077      public boolean filterRowKey(Cell cell) {
078        return true;
079      }
080
081      @Override
082      public Cell getHintForRejectedRow(Cell firstRowCell) {
083        return hint;
084      }
085    };
086  }
087
088  /** Filter that returns a fixed hint from {@code getSkipHint}. */
089  private static FilterBase fixedSkipHintFilter(Cell hint) {
090    return new FilterBase() {
091      @Override
092      public Cell getSkipHint(Cell skippedCell) {
093        return hint;
094      }
095    };
096  }
097
098  /** Filter that claims {@code filterAllRemaining() == true}. */
099  private static FilterBase terminatedFilter(Cell hint) {
100    return new FilterBase() {
101      @Override
102      public boolean filterAllRemaining() {
103        return true;
104      }
105
106      @Override
107      public Cell getHintForRejectedRow(Cell firstRowCell) {
108        return hint;
109      }
110
111      @Override
112      public Cell getSkipHint(Cell skippedCell) {
113        return hint;
114      }
115    };
116  }
117
118  // ---- AND (MUST_PASS_ALL) getHintForRejectedRow ----
119
120  @Test
121  public void testANDGetHintForRejectedRow_takesMax() throws IOException {
122    Cell hintA = kv(ROW_A);
123    Cell hintC = kv(ROW_C);
124    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
125      Arrays.asList(rejectingRowHintFilter(hintA), rejectingRowHintFilter(hintC)));
126    fl.filterRowKey(kv(ROW_A));
127
128    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
129    assertNotNull(result);
130    assertEquals(0, CellComparator.getInstance().compare(hintC, result),
131      "AND must return the farthest (max) hint");
132  }
133
134  @Test
135  public void testANDGetHintForRejectedRow_ignoresNull() throws IOException {
136    Cell hintB = kv(ROW_B);
137    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
138      Arrays.asList(rejectingRowHintFilter(null), rejectingRowHintFilter(hintB)));
139    fl.filterRowKey(kv(ROW_A));
140
141    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
142    assertNotNull(result);
143    assertEquals(0, CellComparator.getInstance().compare(hintB, result),
144      "AND must ignore null hints and return the non-null one");
145  }
146
147  @Test
148  public void testANDGetHintForRejectedRow_allNull() throws IOException {
149    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
150      Arrays.asList(rejectingRowHintFilter(null), rejectingRowHintFilter(null)));
151    fl.filterRowKey(kv(ROW_A));
152
153    assertNull(fl.getHintForRejectedRow(kv(ROW_A)), "AND with all-null hints must return null");
154  }
155
156  @Test
157  public void testANDGetHintForRejectedRow_reversed() throws IOException {
158    Cell hintA = kv(ROW_A);
159    Cell hintC = kv(ROW_C);
160    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
161      Arrays.asList(rejectingRowHintFilter(hintA), rejectingRowHintFilter(hintC)));
162    fl.setReversed(true);
163    fl.filterRowKey(kv(ROW_C));
164
165    Cell result = fl.getHintForRejectedRow(kv(ROW_C));
166    assertNotNull(result);
167    assertEquals(0, CellComparator.getInstance().compare(hintA, result),
168      "Reversed AND must return the smaller row key (farthest in reverse direction)");
169  }
170
171  // ---- AND (MUST_PASS_ALL) getSkipHint ----
172
173  @Test
174  public void testANDGetSkipHint_takesMax() throws IOException {
175    Cell hintA = kv(ROW_A);
176    Cell hintC = kv(ROW_C);
177    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
178      Arrays.asList(fixedSkipHintFilter(hintA), fixedSkipHintFilter(hintC)));
179
180    Cell result = fl.getSkipHint(kv(ROW_A));
181    assertNotNull(result);
182    assertEquals(0, CellComparator.getInstance().compare(hintC, result),
183      "AND getSkipHint must return the farthest (max) hint");
184  }
185
186  @Test
187  public void testANDGetSkipHint_ignoresNull() throws IOException {
188    Cell hintB = kv(ROW_B);
189    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
190      Arrays.asList(fixedSkipHintFilter(null), fixedSkipHintFilter(hintB)));
191
192    Cell result = fl.getSkipHint(kv(ROW_A));
193    assertNotNull(result);
194    assertEquals(0, CellComparator.getInstance().compare(hintB, result),
195      "AND getSkipHint must ignore null and return the non-null hint");
196  }
197
198  @Test
199  public void testANDGetSkipHint_allNull() throws IOException {
200    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
201      Arrays.asList(fixedSkipHintFilter(null), fixedSkipHintFilter(null)));
202
203    assertNull(fl.getSkipHint(kv(ROW_A)), "AND with all-null skip hints must return null");
204  }
205
206  @Test
207  public void testANDGetSkipHint_reversed() throws IOException {
208    Cell hintA = kv(ROW_A);
209    Cell hintC = kv(ROW_C);
210    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
211      Arrays.asList(fixedSkipHintFilter(hintA), fixedSkipHintFilter(hintC)));
212    fl.setReversed(true);
213
214    Cell result = fl.getSkipHint(kv(ROW_C));
215    assertNotNull(result);
216    assertEquals(0, CellComparator.getInstance().compare(hintA, result),
217      "Reversed AND getSkipHint must return the smaller row key (farthest in reverse direction)");
218  }
219
220  // ---- OR (MUST_PASS_ONE) getHintForRejectedRow ----
221
222  @Test
223  public void testORGetHintForRejectedRow_takesMin() throws IOException {
224    Cell hintA = kv(ROW_A);
225    Cell hintC = kv(ROW_C);
226    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
227      Arrays.asList(fixedRejectedRowHintFilter(hintA), fixedRejectedRowHintFilter(hintC)));
228
229    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
230    assertNotNull(result);
231    assertEquals(0, CellComparator.getInstance().compare(hintA, result),
232      "OR must return the nearest (min) hint");
233  }
234
235  @Test
236  public void testORGetHintForRejectedRow_nullReturnsNull() throws IOException {
237    Cell hintC = kv(ROW_C);
238    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
239      Arrays.asList(fixedRejectedRowHintFilter(null), fixedRejectedRowHintFilter(hintC)));
240
241    assertNull(fl.getHintForRejectedRow(kv(ROW_A)),
242      "OR must return null if any sub-filter returns null (can't safely skip)");
243  }
244
245  @Test
246  public void testORGetHintForRejectedRow_allHints() throws IOException {
247    Cell hintA = kv(ROW_A);
248    Cell hintB = kv(ROW_B);
249    Cell hintC = kv(ROW_C);
250    FilterList fl =
251      new FilterList(Operator.MUST_PASS_ONE, Arrays.asList(fixedRejectedRowHintFilter(hintB),
252        fixedRejectedRowHintFilter(hintA), fixedRejectedRowHintFilter(hintC)));
253
254    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
255    assertNotNull(result);
256    assertEquals(0, CellComparator.getInstance().compare(hintA, result),
257      "OR with all hints must return the minimum");
258  }
259
260  @Test
261  public void testORGetHintForRejectedRow_reversed() throws IOException {
262    // In reversed scan, "min" in scan direction means the larger row key.
263    Cell hintA = kv(ROW_A);
264    Cell hintC = kv(ROW_C);
265    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
266      Arrays.asList(fixedRejectedRowHintFilter(hintA), fixedRejectedRowHintFilter(hintC)));
267    fl.setReversed(true);
268
269    Cell result = fl.getHintForRejectedRow(kv(ROW_C));
270    assertNotNull(result);
271    assertEquals(0, CellComparator.getInstance().compare(hintC, result),
272      "Reversed OR must return the larger row key (nearest in reverse direction)");
273  }
274
275  // ---- OR (MUST_PASS_ONE) getSkipHint ----
276
277  @Test
278  public void testORGetSkipHint_takesMin() throws IOException {
279    Cell hintA = kv(ROW_A);
280    Cell hintC = kv(ROW_C);
281    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
282      Arrays.asList(fixedSkipHintFilter(hintA), fixedSkipHintFilter(hintC)));
283
284    Cell result = fl.getSkipHint(kv(ROW_A));
285    assertNotNull(result);
286    assertEquals(0, CellComparator.getInstance().compare(hintA, result),
287      "OR getSkipHint must return the nearest (min) hint");
288  }
289
290  @Test
291  public void testORGetSkipHint_nullReturnsNull() throws IOException {
292    Cell hintC = kv(ROW_C);
293    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
294      Arrays.asList(fixedSkipHintFilter(null), fixedSkipHintFilter(hintC)));
295
296    assertNull(fl.getSkipHint(kv(ROW_A)),
297      "OR getSkipHint must return null if any sub-filter returns null");
298  }
299
300  @Test
301  public void testORGetSkipHint_allHints() throws IOException {
302    Cell hintA = kv(ROW_A);
303    Cell hintB = kv(ROW_B);
304    Cell hintC = kv(ROW_C);
305    FilterList fl = new FilterList(Operator.MUST_PASS_ONE, Arrays.asList(fixedSkipHintFilter(hintB),
306      fixedSkipHintFilter(hintA), fixedSkipHintFilter(hintC)));
307
308    Cell result = fl.getSkipHint(kv(ROW_A));
309    assertNotNull(result);
310    assertEquals(0, CellComparator.getInstance().compare(hintA, result),
311      "OR getSkipHint with all hints must return the minimum");
312  }
313
314  @Test
315  public void testORGetSkipHint_reversed() throws IOException {
316    Cell hintA = kv(ROW_A);
317    Cell hintC = kv(ROW_C);
318    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
319      Arrays.asList(fixedSkipHintFilter(hintA), fixedSkipHintFilter(hintC)));
320    fl.setReversed(true);
321
322    Cell result = fl.getSkipHint(kv(ROW_C));
323    assertNotNull(result);
324    assertEquals(0, CellComparator.getInstance().compare(hintC, result),
325      "Reversed OR getSkipHint must return the larger row key (nearest in reverse direction)");
326  }
327
328  // ---- filterAllRemaining for getSkipHint ----
329
330  @Test
331  public void testFilterAllRemainingSubFilterSkippedForGetSkipHint() throws IOException {
332    Cell terminatedHint = kv(ROW_C);
333    Cell activeHint = kv(ROW_B);
334
335    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
336      Arrays.asList(terminatedFilter(terminatedHint), fixedSkipHintFilter(activeHint)));
337
338    Cell result = fl.getSkipHint(kv(ROW_A));
339    assertNotNull(result);
340    assertEquals(0, CellComparator.getInstance().compare(activeHint, result),
341      "Terminated sub-filters must be skipped for getSkipHint too");
342  }
343
344  @Test
345  public void testORFilterAllRemainingSubFilterSkippedForGetHintForRejectedRow()
346    throws IOException {
347    Cell terminatedHint = kv(ROW_C);
348    Cell activeHint = kv(ROW_B);
349
350    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
351      Arrays.asList(terminatedFilter(terminatedHint), fixedRejectedRowHintFilter(activeHint)));
352
353    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
354    assertNotNull(result);
355    assertEquals(0, CellComparator.getInstance().compare(activeHint, result),
356      "OR must skip terminated sub-filters and return the active filter's hint");
357  }
358
359  @Test
360  public void testORFilterAllRemainingSubFilterSkippedForGetSkipHint() throws IOException {
361    Cell terminatedHint = kv(ROW_C);
362    Cell activeHint = kv(ROW_B);
363
364    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
365      Arrays.asList(terminatedFilter(terminatedHint), fixedSkipHintFilter(activeHint)));
366
367    Cell result = fl.getSkipHint(kv(ROW_A));
368    assertNotNull(result);
369    assertEquals(0, CellComparator.getInstance().compare(activeHint, result),
370      "OR must skip terminated sub-filters for getSkipHint too");
371  }
372
373  // ---- SkipFilter delegation ----
374
375  @Test
376  public void testSkipFilterDelegatesGetHintForRejectedRow() throws IOException {
377    Cell hint = kv(ROW_B);
378    SkipFilter sf = new SkipFilter(fixedRejectedRowHintFilter(hint));
379
380    Cell result = sf.getHintForRejectedRow(kv(ROW_A));
381    assertNotNull(result);
382    assertEquals(0, CellComparator.getInstance().compare(hint, result),
383      "SkipFilter must delegate getHintForRejectedRow to wrapped filter");
384  }
385
386  @Test
387  public void testSkipFilterDelegatesGetSkipHint() throws IOException {
388    Cell hint = kv(ROW_B);
389    SkipFilter sf = new SkipFilter(fixedSkipHintFilter(hint));
390
391    Cell result = sf.getSkipHint(kv(ROW_A));
392    assertNotNull(result);
393    assertEquals(0, CellComparator.getInstance().compare(hint, result),
394      "SkipFilter must delegate getSkipHint to wrapped filter");
395  }
396
397  // ---- WhileMatchFilter delegation ----
398
399  @Test
400  public void testWhileMatchFilterDelegatesGetHintForRejectedRow() throws IOException {
401    Cell hint = kv(ROW_B);
402    WhileMatchFilter wmf = new WhileMatchFilter(fixedRejectedRowHintFilter(hint));
403
404    Cell result = wmf.getHintForRejectedRow(kv(ROW_A));
405    assertNotNull(result);
406    assertEquals(0, CellComparator.getInstance().compare(hint, result),
407      "WhileMatchFilter must delegate getHintForRejectedRow to wrapped filter");
408  }
409
410  @Test
411  public void testWhileMatchFilterDelegatesGetSkipHint() throws IOException {
412    Cell hint = kv(ROW_B);
413    WhileMatchFilter wmf = new WhileMatchFilter(fixedSkipHintFilter(hint));
414
415    Cell result = wmf.getSkipHint(kv(ROW_A));
416    assertNotNull(result);
417    assertEquals(0, CellComparator.getInstance().compare(hint, result),
418      "WhileMatchFilter must delegate getSkipHint to wrapped filter");
419  }
420
421  // ---- FilterList facade delegation ----
422
423  @Test
424  public void testFilterListDelegatesToFilterListBase() throws IOException {
425    Cell hint = kv(ROW_B);
426    // AND variant
427    FilterList andList = new FilterList(Operator.MUST_PASS_ALL, rejectingRowHintFilter(hint));
428    andList.filterRowKey(kv(ROW_A));
429    assertNotNull(andList.getHintForRejectedRow(kv(ROW_A)),
430      "FilterList(AND) must delegate getHintForRejectedRow");
431    // OR variant
432    FilterList orList = new FilterList(Operator.MUST_PASS_ONE, fixedSkipHintFilter(hint));
433    assertNotNull(orList.getSkipHint(kv(ROW_A)), "FilterList(OR) must delegate getSkipHint");
434  }
435
436  // ---- Nested FilterList ----
437
438  @Test
439  public void testNestedFilterList() throws IOException {
440    Cell hintA = kv(ROW_A);
441    Cell hintB = kv(ROW_B);
442    Cell hintC = kv(ROW_C);
443
444    // Inner OR: all sub-filters reject, so OR rejects too. Returns min(hintA, hintC) = hintA.
445    FilterList innerOR = new FilterList(Operator.MUST_PASS_ONE,
446      Arrays.asList(rejectingRowHintFilter(hintA), rejectingRowHintFilter(hintC)));
447    // Outer AND: returns max(innerOR=hintA, hintB) = hintB
448    FilterList outerAND =
449      new FilterList(Operator.MUST_PASS_ALL, Arrays.asList(innerOR, rejectingRowHintFilter(hintB)));
450    outerAND.filterRowKey(kv(ROW_A));
451
452    Cell result = outerAND.getHintForRejectedRow(kv(ROW_A));
453    assertNotNull(result);
454    assertEquals(0, CellComparator.getInstance().compare(hintB, result),
455      "Nested AND(OR(A,C), B) must return max(min(A,C), B) = max(A, B) = B");
456  }
457
458  // ---- filterAllRemaining sub-filter is skipped ----
459
460  @Test
461  public void testFilterAllRemainingSubFilterSkipped() throws IOException {
462    Cell terminatedHint = kv(ROW_C);
463    Cell activeHint = kv(ROW_B);
464
465    // Place the active rejecting filter first so filterRowKey reaches it before
466    // encountering the terminated filter (which causes early return).
467    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
468      Arrays.asList(rejectingRowHintFilter(activeHint), terminatedFilter(terminatedHint)));
469    fl.filterRowKey(kv(ROW_A));
470
471    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
472    assertNotNull(result);
473    assertEquals(0, CellComparator.getInstance().compare(activeHint, result),
474      "Terminated sub-filters (filterAllRemaining=true) must be skipped");
475  }
476
477  // ---- All sub-filters terminated ----
478
479  @Test
480  public void testORAllSubFiltersTerminated_getHintForRejectedRow() throws IOException {
481    Cell hint = kv(ROW_B);
482    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
483      Arrays.asList(terminatedFilter(hint), terminatedFilter(hint)));
484
485    assertNull(fl.getHintForRejectedRow(kv(ROW_A)),
486      "OR with all terminated sub-filters must return null for getHintForRejectedRow");
487  }
488
489  @Test
490  public void testORAllSubFiltersTerminated_getSkipHint() throws IOException {
491    Cell hint = kv(ROW_B);
492    FilterList fl = new FilterList(Operator.MUST_PASS_ONE,
493      Arrays.asList(terminatedFilter(hint), terminatedFilter(hint)));
494
495    assertNull(fl.getSkipHint(kv(ROW_A)),
496      "OR with all terminated sub-filters must return null for getSkipHint");
497  }
498
499  @Test
500  public void testANDAllSubFiltersTerminated_getHintForRejectedRow() throws IOException {
501    Cell hint = kv(ROW_B);
502    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
503      Arrays.asList(terminatedFilter(hint), terminatedFilter(hint)));
504
505    assertNull(fl.getHintForRejectedRow(kv(ROW_A)),
506      "AND with all terminated sub-filters must return null for getHintForRejectedRow");
507  }
508
509  @Test
510  public void testANDAllSubFiltersTerminated_getSkipHint() throws IOException {
511    Cell hint = kv(ROW_B);
512    FilterList fl = new FilterList(Operator.MUST_PASS_ALL,
513      Arrays.asList(terminatedFilter(hint), terminatedFilter(hint)));
514
515    assertNull(fl.getSkipHint(kv(ROW_A)),
516      "AND with all terminated sub-filters must return null for getSkipHint");
517  }
518
519  // ---- Empty FilterList ----
520
521  @Test
522  public void testEmptyFilterListReturnsNull() throws IOException {
523    FilterList emptyAND = new FilterList(Operator.MUST_PASS_ALL);
524    FilterList emptyOR = new FilterList(Operator.MUST_PASS_ONE);
525
526    assertNull(emptyAND.getHintForRejectedRow(kv(ROW_A)),
527      "Empty AND FilterList must return null for getHintForRejectedRow");
528    assertNull(emptyAND.getSkipHint(kv(ROW_A)),
529      "Empty AND FilterList must return null for getSkipHint");
530    assertNull(emptyOR.getHintForRejectedRow(kv(ROW_A)),
531      "Empty OR FilterList must return null for getHintForRejectedRow");
532    assertNull(emptyOR.getSkipHint(kv(ROW_A)),
533      "Empty OR FilterList must return null for getSkipHint");
534  }
535
536  // ---- Single filter pass-through ----
537
538  @Test
539  public void testSingleFilterAND() throws IOException {
540    Cell hint = kv(ROW_B);
541    FilterList fl = new FilterList(Operator.MUST_PASS_ALL, rejectingRowHintFilter(hint));
542    fl.filterRowKey(kv(ROW_A));
543
544    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
545    assertNotNull(result);
546    assertEquals(0, CellComparator.getInstance().compare(hint, result),
547      "Single-filter AND must pass through the hint unchanged");
548  }
549
550  @Test
551  public void testSingleFilterOR() throws IOException {
552    Cell hint = kv(ROW_B);
553    FilterList fl = new FilterList(Operator.MUST_PASS_ONE, fixedRejectedRowHintFilter(hint));
554
555    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
556    assertNotNull(result);
557    assertEquals(0, CellComparator.getInstance().compare(hint, result),
558      "Single-filter OR must pass through the hint unchanged");
559  }
560
561  // ---- AND contract: only consult rejecting sub-filters for getHintForRejectedRow ----
562
563  @Test
564  public void testANDGetHintForRejectedRow_onlyConsultsRejectingSubFilters() throws IOException {
565    Cell hint = kv(ROW_C);
566    FilterBase rejectingFilter = new FilterBase() {
567      @Override
568      public boolean filterRowKey(Cell cell) {
569        return true;
570      }
571
572      @Override
573      public Cell getHintForRejectedRow(Cell firstRowCell) {
574        return hint;
575      }
576    };
577    FilterBase acceptingFilter = new FilterBase() {
578      @Override
579      public boolean filterRowKey(Cell cell) {
580        return false;
581      }
582
583      @Override
584      public Cell getHintForRejectedRow(Cell firstRowCell) {
585        throw new IllegalStateException(
586          "Contract violation: getHintForRejectedRow called on non-rejecting filter");
587      }
588    };
589
590    FilterList fl =
591      new FilterList(Operator.MUST_PASS_ALL, Arrays.asList(rejectingFilter, acceptingFilter));
592    assertTrue(fl.filterRowKey(kv(ROW_A)), "AND must reject when at least one sub-filter rejects");
593
594    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
595    assertNotNull(result);
596    assertEquals(0, CellComparator.getInstance().compare(hint, result),
597      "AND must return the hint from the rejecting sub-filter only");
598  }
599
600  @Test
601  public void testANDGetHintForRejectedRow_resetClearsRejectionState() throws IOException {
602    Cell hint = kv(ROW_C);
603    FilterBase sometimesRejects = new FilterBase() {
604      private boolean shouldReject = true;
605
606      @Override
607      public boolean filterRowKey(Cell cell) {
608        if (shouldReject) {
609          shouldReject = false;
610          return true;
611        }
612        return false;
613      }
614
615      @Override
616      public Cell getHintForRejectedRow(Cell firstRowCell) {
617        return hint;
618      }
619    };
620
621    FilterList fl = new FilterList(Operator.MUST_PASS_ALL, sometimesRejects);
622
623    // Row 1: filter rejects, hint is available
624    assertTrue(fl.filterRowKey(kv(ROW_A)));
625    assertNotNull(fl.getHintForRejectedRow(kv(ROW_A)));
626
627    // Reset between rows (as the scanner does)
628    fl.reset();
629
630    // Row 2: filter accepts, no rejection state should remain
631    fl.filterRowKey(kv(ROW_B));
632    assertNull(fl.getHintForRejectedRow(kv(ROW_B)),
633      "After reset(), rejection state must be cleared — no stale hints");
634  }
635
636  @Test
637  public void testANDGetHintForRejectedRow_takesMaxFromRejectingFilters() throws IOException {
638    Cell hintA = kv(ROW_A);
639    Cell hintC = kv(ROW_C);
640    FilterBase rejectToA = new FilterBase() {
641      @Override
642      public boolean filterRowKey(Cell cell) {
643        return true;
644      }
645
646      @Override
647      public Cell getHintForRejectedRow(Cell firstRowCell) {
648        return hintA;
649      }
650    };
651    FilterBase rejectToC = new FilterBase() {
652      @Override
653      public boolean filterRowKey(Cell cell) {
654        return true;
655      }
656
657      @Override
658      public Cell getHintForRejectedRow(Cell firstRowCell) {
659        return hintC;
660      }
661    };
662
663    FilterList fl = new FilterList(Operator.MUST_PASS_ALL, Arrays.asList(rejectToA, rejectToC));
664    fl.filterRowKey(kv(ROW_A));
665
666    Cell result = fl.getHintForRejectedRow(kv(ROW_A));
667    assertNotNull(result);
668    assertEquals(0, CellComparator.getInstance().compare(hintC, result),
669      "AND must return max hint from rejecting sub-filters");
670  }
671
672  @Test
673  public void testANDGetHintForRejectedRow_reversedTakesMaxFromRejectingFilters()
674    throws IOException {
675    Cell hintA = kv(ROW_A);
676    Cell hintC = kv(ROW_C);
677    FilterBase rejectToA = new FilterBase() {
678      @Override
679      public boolean filterRowKey(Cell cell) {
680        return true;
681      }
682
683      @Override
684      public Cell getHintForRejectedRow(Cell firstRowCell) {
685        return hintA;
686      }
687    };
688    FilterBase rejectToC = new FilterBase() {
689      @Override
690      public boolean filterRowKey(Cell cell) {
691        return true;
692      }
693
694      @Override
695      public Cell getHintForRejectedRow(Cell firstRowCell) {
696        return hintC;
697      }
698    };
699
700    FilterList fl = new FilterList(Operator.MUST_PASS_ALL, Arrays.asList(rejectToA, rejectToC));
701    fl.setReversed(true);
702    fl.filterRowKey(kv(ROW_C));
703
704    Cell result = fl.getHintForRejectedRow(kv(ROW_C));
705    assertNotNull(result);
706    assertEquals(0, CellComparator.getInstance().compare(hintA, result),
707      "Reversed AND must return smallest row key (farthest in reverse) from rejecting filters");
708  }
709}