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}