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.assertTrue; 022 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.List; 027import java.util.concurrent.atomic.AtomicInteger; 028import org.apache.hadoop.hbase.Cell; 029import org.apache.hadoop.hbase.CellUtil; 030import org.apache.hadoop.hbase.HBaseTestingUtil; 031import org.apache.hadoop.hbase.PrivateCellUtil; 032import org.apache.hadoop.hbase.TableName; 033import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 034import org.apache.hadoop.hbase.client.Durability; 035import org.apache.hadoop.hbase.client.Put; 036import org.apache.hadoop.hbase.client.RegionInfo; 037import org.apache.hadoop.hbase.client.RegionInfoBuilder; 038import org.apache.hadoop.hbase.client.Scan; 039import org.apache.hadoop.hbase.client.TableDescriptor; 040import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 041import org.apache.hadoop.hbase.regionserver.HRegion; 042import org.apache.hadoop.hbase.regionserver.RegionScanner; 043import org.apache.hadoop.hbase.testclassification.FilterTests; 044import org.apache.hadoop.hbase.testclassification.MediumTests; 045import org.apache.hadoop.hbase.util.Bytes; 046import org.junit.jupiter.api.AfterEach; 047import org.junit.jupiter.api.BeforeEach; 048import org.junit.jupiter.api.Tag; 049import org.junit.jupiter.api.Test; 050import org.junit.jupiter.api.TestInfo; 051 052@Tag(FilterTests.TAG) 053@Tag(MediumTests.TAG) 054public class TestFilterHintForRejectedRow { 055 056 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 057 058 private static final byte[] FAMILY = Bytes.toBytes("f"); 059 private static final byte[] FAMILY2 = Bytes.toBytes("g"); 060 private static final int CELLS_PER_ROW = 10; 061 private static final byte[] VALUE = Bytes.toBytes("value"); 062 063 private HRegion region; 064 065 @BeforeEach 066 public void setUp(TestInfo testInfo) throws Exception { 067 TableDescriptor tableDescriptor = 068 TableDescriptorBuilder.newBuilder(TableName.valueOf(testInfo.getTestMethod().get().getName())) 069 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILY)) 070 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILY2)).build(); 071 RegionInfo info = RegionInfoBuilder.newBuilder(tableDescriptor.getTableName()).build(); 072 this.region = HBaseTestingUtil.createRegionAndWAL(info, TEST_UTIL.getDataTestDir(), 073 TEST_UTIL.getConfiguration(), tableDescriptor); 074 } 075 076 @AfterEach 077 public void tearDown() throws Exception { 078 HBaseTestingUtil.closeRegionAndWAL(this.region); 079 } 080 081 private void writeRows(String prefix, int count) throws IOException { 082 for (int i = 0; i < count; i++) { 083 byte[] row = Bytes.toBytes(String.format("%s-%02d", prefix, i)); 084 Put p = new Put(row); 085 p.setDurability(Durability.SKIP_WAL); 086 for (int j = 0; j < CELLS_PER_ROW; j++) { 087 p.addColumn(FAMILY, Bytes.toBytes(String.format("q-%02d", j)), VALUE); 088 } 089 this.region.put(p); 090 } 091 this.region.flush(true); 092 } 093 094 private void writeRowsMultiFamily(String prefix, int count) throws IOException { 095 for (int i = 0; i < count; i++) { 096 byte[] row = Bytes.toBytes(String.format("%s-%02d", prefix, i)); 097 Put p = new Put(row); 098 p.setDurability(Durability.SKIP_WAL); 099 for (int j = 0; j < CELLS_PER_ROW; j++) { 100 byte[] qual = Bytes.toBytes(String.format("q-%02d", j)); 101 p.addColumn(FAMILY, qual, VALUE); 102 p.addColumn(FAMILY2, qual, VALUE); 103 } 104 this.region.put(p); 105 } 106 this.region.flush(true); 107 } 108 109 private List<Cell> scanAll(Scan scan) throws IOException { 110 List<Cell> results = new ArrayList<>(); 111 try (RegionScanner scanner = region.getScanner(scan)) { 112 List<Cell> rowCells = new ArrayList<>(); 113 boolean hasMore; 114 do { 115 hasMore = scanner.next(rowCells); 116 results.addAll(rowCells); 117 rowCells.clear(); 118 } while (hasMore); 119 } 120 return results; 121 } 122 123 @Test 124 public void testHintAndNoHintReturnSameCellsOnSameData() throws IOException { 125 final String prefix = "row"; 126 final int rejectedCount = 5; 127 final int acceptedCount = 5; 128 writeRows(prefix, rejectedCount + acceptedCount); 129 130 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 131 final AtomicInteger hintCallCount = new AtomicInteger(0); 132 133 FilterBase hintFilter = new FilterBase() { 134 @Override 135 public boolean filterRowKey(Cell cell) { 136 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 137 acceptedStartRow, 0, acceptedStartRow.length) < 0; 138 } 139 140 @Override 141 public Cell getHintForRejectedRow(Cell firstRowCell) { 142 hintCallCount.incrementAndGet(); 143 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 144 } 145 }; 146 147 FilterBase noHintFilter = new FilterBase() { 148 @Override 149 public boolean filterRowKey(Cell cell) { 150 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 151 acceptedStartRow, 0, acceptedStartRow.length) < 0; 152 } 153 }; 154 155 List<Cell> hintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(hintFilter)); 156 List<Cell> noHintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(noHintFilter)); 157 158 assertEquals(noHintResults.size(), hintResults.size(), 159 "Both paths must return the same number of cells"); 160 for (int i = 0; i < hintResults.size(); i++) { 161 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 162 "Cell mismatch at index " + i); 163 } 164 assertEquals(acceptedCount * CELLS_PER_ROW, hintResults.size()); 165 assertEquals(1, hintCallCount.get(), 166 "getHintForRejectedRow must be called exactly once: the hint skips all rejected rows"); 167 } 168 169 @Test 170 public void testHintBeyondLastRowTerminatesScanGracefully() throws IOException { 171 final String prefix = "hint-beyond"; 172 writeRows(prefix, 5); 173 174 final byte[] beyondAllRows = Bytes.toBytes("zzz-beyond-table-end"); 175 176 FilterBase beyondHintFilter = new FilterBase() { 177 @Override 178 public boolean filterRowKey(Cell cell) { 179 return true; 180 } 181 182 @Override 183 public Cell getHintForRejectedRow(Cell firstRowCell) { 184 return PrivateCellUtil.createFirstOnRow(beyondAllRows); 185 } 186 }; 187 188 List<Cell> results = scanAll(new Scan().addFamily(FAMILY).setFilter(beyondHintFilter)); 189 assertTrue(results.isEmpty(), 190 "When the hint is past the last row, no cells should be returned"); 191 } 192 193 @Test 194 public void testPerRowHintCalledOncePerRejectedRow() throws IOException { 195 final String prefix = "hint-perrow"; 196 final int rejectedCount = 4; 197 final int acceptedCount = 2; 198 writeRows(prefix, rejectedCount + acceptedCount); 199 200 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 201 final AtomicInteger hintCallCount = new AtomicInteger(0); 202 203 FilterBase perRowHintFilter = new FilterBase() { 204 @Override 205 public boolean filterRowKey(Cell cell) { 206 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 207 acceptedStartRow, 0, acceptedStartRow.length) < 0; 208 } 209 210 @Override 211 public Cell getHintForRejectedRow(Cell firstRowCell) { 212 hintCallCount.incrementAndGet(); 213 return PrivateCellUtil.createFirstOnNextRow(firstRowCell); 214 } 215 }; 216 217 List<Cell> results = scanAll(new Scan().addFamily(FAMILY).setFilter(perRowHintFilter)); 218 219 assertEquals(acceptedCount * CELLS_PER_ROW, results.size()); 220 for (Cell c : results) { 221 assertTrue( 222 Bytes.compareTo(c.getRowArray(), c.getRowOffset(), c.getRowLength(), acceptedStartRow, 0, 223 acceptedStartRow.length) >= 0, 224 "Every returned cell must belong to the accepted row range"); 225 } 226 assertEquals(rejectedCount, hintCallCount.get(), 227 "getHintForRejectedRow must be called once per rejected row in the per-row hint strategy"); 228 } 229 230 @Test 231 public void testReversedScanWithHint() throws IOException { 232 final String prefix = "row"; 233 final int totalRows = 10; 234 writeRows(prefix, totalRows); 235 236 // Accept rows 00-04 (lower half), reject rows 05-09 (upper half). 237 // In a reversed scan, the scanner starts at row-09 and moves backward. 238 final byte[] rejectThreshold = Bytes.toBytes(String.format("%s-%02d", prefix, 5)); 239 final byte[] hintTarget = Bytes.toBytes(String.format("%s-%02d", prefix, 4)); 240 final AtomicInteger hintCallCount = new AtomicInteger(0); 241 242 FilterBase hintFilter = new FilterBase() { 243 @Override 244 public boolean filterRowKey(Cell cell) { 245 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 246 rejectThreshold, 0, rejectThreshold.length) >= 0; 247 } 248 249 @Override 250 public Cell getHintForRejectedRow(Cell firstRowCell) { 251 hintCallCount.incrementAndGet(); 252 // In reversed scan, hint must point backward (to a smaller row key). 253 return PrivateCellUtil.createFirstOnRow(hintTarget); 254 } 255 }; 256 257 FilterBase noHintFilter = new FilterBase() { 258 @Override 259 public boolean filterRowKey(Cell cell) { 260 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 261 rejectThreshold, 0, rejectThreshold.length) >= 0; 262 } 263 }; 264 265 Scan reversedHintScan = new Scan().addFamily(FAMILY).setReversed(true).setFilter(hintFilter); 266 Scan reversedNoHintScan = 267 new Scan().addFamily(FAMILY).setReversed(true).setFilter(noHintFilter); 268 269 List<Cell> hintResults = scanAll(reversedHintScan); 270 List<Cell> noHintResults = scanAll(reversedNoHintScan); 271 272 assertEquals(noHintResults.size(), hintResults.size(), 273 "Both paths must return the same number of cells"); 274 for (int i = 0; i < hintResults.size(); i++) { 275 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 276 "Cell mismatch at index " + i); 277 } 278 assertEquals(5 * CELLS_PER_ROW, hintResults.size()); 279 assertEquals(1, hintCallCount.get(), 280 "getHintForRejectedRow must be called exactly once for reversed scan"); 281 } 282 283 @Test 284 public void testGetSkipHintWithTimeRangeIntegration() throws IOException { 285 final long insideTs = 2000; 286 final long outsideTs = 500; 287 final int rowCount = 5; 288 289 for (int i = 0; i < rowCount; i++) { 290 byte[] row = Bytes.toBytes(String.format("skiprow-%02d", i)); 291 Put p = new Put(row); 292 p.setDurability(Durability.SKIP_WAL); 293 p.addColumn(FAMILY, Bytes.toBytes("q"), insideTs, VALUE); 294 p.addColumn(FAMILY, Bytes.toBytes("q"), outsideTs, VALUE); 295 region.put(p); 296 } 297 region.flush(true); 298 299 final AtomicInteger skipHintCalls = new AtomicInteger(0); 300 301 FilterBase skipHintFilter = new FilterBase() { 302 @Override 303 public Cell getSkipHint(Cell skippedCell) { 304 skipHintCalls.incrementAndGet(); 305 return PrivateCellUtil.createFirstOnNextRow(skippedCell); 306 } 307 }; 308 309 Scan hintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(skipHintFilter); 310 Scan noHintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000); 311 312 List<Cell> hintResults = scanAll(hintScan); 313 List<Cell> noHintResults = scanAll(noHintScan); 314 315 assertEquals(noHintResults.size(), hintResults.size(), 316 "Both paths must return the same number of cells"); 317 for (int i = 0; i < hintResults.size(); i++) { 318 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 319 "Cell mismatch at index " + i); 320 } 321 assertEquals(rowCount, hintResults.size()); 322 } 323 324 @Test 325 public void testBackwardHintFallsBackToNextRow() throws IOException { 326 final String prefix = "row"; 327 writeRows(prefix, 5); 328 329 final byte[] row00 = Bytes.toBytes(String.format("%s-%02d", prefix, 0)); 330 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, 2)); 331 332 FilterBase backwardHintFilter = new FilterBase() { 333 @Override 334 public boolean filterRowKey(Cell cell) { 335 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 336 acceptedStartRow, 0, acceptedStartRow.length) < 0; 337 } 338 339 @Override 340 public Cell getHintForRejectedRow(Cell firstRowCell) { 341 return PrivateCellUtil.createFirstOnRow(row00); 342 } 343 }; 344 345 FilterBase noHintFilter = new FilterBase() { 346 @Override 347 public boolean filterRowKey(Cell cell) { 348 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 349 acceptedStartRow, 0, acceptedStartRow.length) < 0; 350 } 351 }; 352 353 List<Cell> hintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(backwardHintFilter)); 354 List<Cell> noHintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(noHintFilter)); 355 356 assertEquals(noHintResults.size(), hintResults.size(), 357 "Backward hint must produce same results as no-hint path"); 358 for (int i = 0; i < hintResults.size(); i++) { 359 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 360 "Cell mismatch at index " + i); 361 } 362 } 363 364 @Test 365 public void testSameRowHintFallsBackToNextRow() throws IOException { 366 final String prefix = "row"; 367 writeRows(prefix, 5); 368 369 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, 2)); 370 final AtomicInteger hintCallCount = new AtomicInteger(0); 371 372 FilterBase sameRowHintFilter = new FilterBase() { 373 @Override 374 public boolean filterRowKey(Cell cell) { 375 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 376 acceptedStartRow, 0, acceptedStartRow.length) < 0; 377 } 378 379 @Override 380 public Cell getHintForRejectedRow(Cell firstRowCell) { 381 hintCallCount.incrementAndGet(); 382 return PrivateCellUtil.createFirstOnRow(CellUtil.cloneRow(firstRowCell)); 383 } 384 }; 385 386 FilterBase noHintFilter = new FilterBase() { 387 @Override 388 public boolean filterRowKey(Cell cell) { 389 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 390 acceptedStartRow, 0, acceptedStartRow.length) < 0; 391 } 392 }; 393 394 List<Cell> hintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(sameRowHintFilter)); 395 List<Cell> noHintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(noHintFilter)); 396 397 assertEquals(noHintResults.size(), hintResults.size(), 398 "Same-row hint must produce same results as no-hint path"); 399 for (int i = 0; i < hintResults.size(); i++) { 400 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 401 "Cell mismatch at index " + i); 402 } 403 assertEquals(3 * CELLS_PER_ROW, hintResults.size()); 404 } 405 406 @Test 407 public void testCoprocessorHookCalledOncePerHintedRejection() throws IOException { 408 final String prefix = "row"; 409 final int rejectedCount = 3; 410 final int acceptedCount = 3; 411 writeRows(prefix, rejectedCount + acceptedCount); 412 413 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 414 final AtomicInteger hintCallCount = new AtomicInteger(0); 415 416 FilterBase hintFilter = new FilterBase() { 417 @Override 418 public boolean filterRowKey(Cell cell) { 419 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 420 acceptedStartRow, 0, acceptedStartRow.length) < 0; 421 } 422 423 @Override 424 public Cell getHintForRejectedRow(Cell firstRowCell) { 425 hintCallCount.incrementAndGet(); 426 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 427 } 428 }; 429 430 List<Cell> results = scanAll(new Scan().addFamily(FAMILY).setFilter(hintFilter)); 431 432 assertEquals(1, hintCallCount.get()); 433 assertEquals(acceptedCount * CELLS_PER_ROW, results.size()); 434 } 435 436 @Test 437 public void testMultiFamilyScanWithHint() throws IOException { 438 final String prefix = "row"; 439 final int rejectedCount = 3; 440 final int acceptedCount = 3; 441 writeRowsMultiFamily(prefix, rejectedCount + acceptedCount); 442 443 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 444 final AtomicInteger hintCallCount = new AtomicInteger(0); 445 446 FilterBase hintFilter = new FilterBase() { 447 @Override 448 public boolean filterRowKey(Cell cell) { 449 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 450 acceptedStartRow, 0, acceptedStartRow.length) < 0; 451 } 452 453 @Override 454 public Cell getHintForRejectedRow(Cell firstRowCell) { 455 hintCallCount.incrementAndGet(); 456 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 457 } 458 }; 459 460 FilterBase noHintFilter = new FilterBase() { 461 @Override 462 public boolean filterRowKey(Cell cell) { 463 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 464 acceptedStartRow, 0, acceptedStartRow.length) < 0; 465 } 466 }; 467 468 // Scan both families. 469 Scan hintScan = new Scan().addFamily(FAMILY).addFamily(FAMILY2).setFilter(hintFilter); 470 Scan noHintScan = new Scan().addFamily(FAMILY).addFamily(FAMILY2).setFilter(noHintFilter); 471 472 List<Cell> hintResults = scanAll(hintScan); 473 List<Cell> noHintResults = scanAll(noHintScan); 474 475 assertEquals(noHintResults.size(), hintResults.size(), 476 "Both paths must return the same number of cells"); 477 for (int i = 0; i < hintResults.size(); i++) { 478 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 479 "Cell mismatch at index " + i); 480 } 481 assertEquals(2 * CELLS_PER_ROW * acceptedCount, hintResults.size()); 482 assertEquals(1, hintCallCount.get()); 483 } 484 485 @Test 486 public void testDataCorrectnessVsUnfilteredScan() throws IOException { 487 final String prefix = "row"; 488 final int totalRows = 8; 489 writeRows(prefix, totalRows); 490 491 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, 3)); 492 493 FilterBase hintFilter = new FilterBase() { 494 @Override 495 public boolean filterRowKey(Cell cell) { 496 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 497 acceptedStartRow, 0, acceptedStartRow.length) < 0; 498 } 499 500 @Override 501 public Cell getHintForRejectedRow(Cell firstRowCell) { 502 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 503 } 504 }; 505 506 List<Cell> filteredResults = scanAll(new Scan().addFamily(FAMILY).setFilter(hintFilter)); 507 508 // Unfiltered scan starting at acceptedStartRow — should return the exact same cells. 509 Scan unfilteredScan = new Scan().addFamily(FAMILY).withStartRow(acceptedStartRow); 510 List<Cell> unfilteredResults = scanAll(unfilteredScan); 511 512 assertEquals(unfilteredResults.size(), filteredResults.size(), 513 "Filtered and unfiltered scans must return same cell count"); 514 for (int i = 0; i < filteredResults.size(); i++) { 515 assertTrue(CellUtil.equals(filteredResults.get(i), unfilteredResults.get(i)), 516 "Cell mismatch at index " + i); 517 } 518 } 519 520 @Test 521 public void testHintOvershootingStopRowTerminatesGracefully() throws IOException { 522 final String prefix = "row"; 523 final int totalRows = 10; 524 writeRows(prefix, totalRows); 525 526 // Scan rows 00-06 (stopRow=row-07, exclusive). Reject rows 00-02, hint to row-09. 527 // The hint overshoots the stop row, so the scan should terminate with no results 528 // from the rejected range and only include rows 03-06 from the non-rejected range. 529 final byte[] stopRow = Bytes.toBytes(String.format("%s-%02d", prefix, 7)); 530 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, 3)); 531 final byte[] hintTarget = Bytes.toBytes(String.format("%s-%02d", prefix, 9)); 532 533 FilterBase hintFilter = new FilterBase() { 534 @Override 535 public boolean filterRowKey(Cell cell) { 536 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 537 acceptedStartRow, 0, acceptedStartRow.length) < 0; 538 } 539 540 @Override 541 public Cell getHintForRejectedRow(Cell firstRowCell) { 542 // Hint past the stop row — scanner should terminate gracefully. 543 return PrivateCellUtil.createFirstOnRow(hintTarget); 544 } 545 }; 546 547 Scan scan = new Scan().addFamily(FAMILY).withStopRow(stopRow).setFilter(hintFilter); 548 List<Cell> results = scanAll(scan); 549 550 // The hint jumps past stopRow, so no cells should be returned. 551 assertTrue(results.isEmpty(), "Hint past stop row must terminate scan with no results"); 552 } 553 554 @Test 555 public void testHintWithinStopRowReturnsCorrectResults() throws IOException { 556 final String prefix = "row"; 557 final int totalRows = 10; 558 writeRows(prefix, totalRows); 559 560 // Scan rows 00-06 (stopRow=row-07, exclusive). Reject rows 00-02, hint to row-03. 561 final byte[] stopRow = Bytes.toBytes(String.format("%s-%02d", prefix, 7)); 562 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, 3)); 563 564 FilterBase hintFilter = new FilterBase() { 565 @Override 566 public boolean filterRowKey(Cell cell) { 567 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 568 acceptedStartRow, 0, acceptedStartRow.length) < 0; 569 } 570 571 @Override 572 public Cell getHintForRejectedRow(Cell firstRowCell) { 573 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 574 } 575 }; 576 577 Scan hintScan = new Scan().addFamily(FAMILY).withStopRow(stopRow).setFilter(hintFilter); 578 List<Cell> hintResults = scanAll(hintScan); 579 580 // Should get rows 03-06 (4 rows). 581 Scan baselineScan = 582 new Scan().addFamily(FAMILY).withStartRow(acceptedStartRow).withStopRow(stopRow); 583 List<Cell> baselineResults = scanAll(baselineScan); 584 585 assertEquals(baselineResults.size(), hintResults.size(), 586 "Hint within stop row must return same results as baseline"); 587 for (int i = 0; i < hintResults.size(); i++) { 588 assertTrue(CellUtil.equals(hintResults.get(i), baselineResults.get(i)), 589 "Cell mismatch at index " + i); 590 } 591 assertEquals(4 * CELLS_PER_ROW, hintResults.size()); 592 } 593 594 @Test 595 public void testGetSkipHintCalledForReversedScan() throws IOException { 596 final long insideTs = 1500; 597 final long tooNewTs = 5000; 598 final int rowCount = 5; 599 final byte[] qInside = Bytes.toBytes("q-inside"); 600 final byte[] qNew = Bytes.toBytes("q-new"); 601 602 // Write two qualifiers per row: "q-inside" at ts=1500 (within range) and "q-new" at 603 // ts=5000 (above the range upper bound). The time-range gate fires for the too-new cell 604 // before version tracking can absorb it, ensuring getSkipHint is consulted. 605 for (int i = 0; i < rowCount; i++) { 606 byte[] row = Bytes.toBytes(String.format("skiprev-%02d", i)); 607 Put p = new Put(row); 608 p.setDurability(Durability.SKIP_WAL); 609 p.addColumn(FAMILY, qInside, insideTs, VALUE); 610 p.addColumn(FAMILY, qNew, tooNewTs, VALUE); 611 region.put(p); 612 } 613 region.flush(true); 614 615 final AtomicInteger skipHintCalls = new AtomicInteger(0); 616 617 // Return null to verify the code path is reached without altering scan semantics. 618 // The unit test TestUserScanQueryMatcher#testSkipHintConsultedForReversedScan validates 619 // that a non-null hint is correctly accepted by the reversed-scan guard. 620 FilterBase skipHintFilter = new FilterBase() { 621 @Override 622 public Cell getSkipHint(Cell skippedCell) { 623 skipHintCalls.incrementAndGet(); 624 return null; 625 } 626 }; 627 628 // Reversed scan with time range [1000, 3000): insideTs cells pass, tooNewTs cells are 629 // structurally skipped (tsCmp > 0). The skip hint should be consulted. 630 Scan hintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setReversed(true) 631 .setFilter(skipHintFilter); 632 Scan noHintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setReversed(true); 633 634 List<Cell> hintResults = scanAll(hintScan); 635 List<Cell> noHintResults = scanAll(noHintScan); 636 637 assertEquals(noHintResults.size(), hintResults.size(), 638 "Both paths must return the same number of cells"); 639 for (int i = 0; i < hintResults.size(); i++) { 640 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 641 "Cell mismatch at index " + i); 642 } 643 assertEquals(rowCount, hintResults.size()); 644 assertTrue(skipHintCalls.get() > 0, 645 "getSkipHint must be called at least once for reversed scan"); 646 } 647 648 // ---- FilterList AND hint delegation integration tests ---- 649 650 @Test 651 public void testFilterListANDHintDelegation() throws IOException { 652 final String prefix = "row"; 653 final int rejectedCount = 5; 654 final int acceptedCount = 5; 655 writeRows(prefix, rejectedCount + acceptedCount); 656 657 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 658 final AtomicInteger hintCallsA = new AtomicInteger(0); 659 final AtomicInteger hintCallsB = new AtomicInteger(0); 660 661 // Both filters reject the same rows; both provide hints to the accepted start. 662 // AND merging takes max — both point to the same target here. 663 FilterBase filterA = new FilterBase() { 664 @Override 665 public boolean filterRowKey(Cell cell) { 666 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 667 acceptedStartRow, 0, acceptedStartRow.length) < 0; 668 } 669 670 @Override 671 public Cell getHintForRejectedRow(Cell firstRowCell) { 672 hintCallsA.incrementAndGet(); 673 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 674 } 675 }; 676 677 FilterBase filterB = new FilterBase() { 678 @Override 679 public boolean filterRowKey(Cell cell) { 680 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 681 acceptedStartRow, 0, acceptedStartRow.length) < 0; 682 } 683 684 @Override 685 public Cell getHintForRejectedRow(Cell firstRowCell) { 686 hintCallsB.incrementAndGet(); 687 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 688 } 689 }; 690 691 FilterList andFilter = 692 new FilterList(FilterList.Operator.MUST_PASS_ALL, Arrays.asList(filterA, filterB)); 693 694 FilterBase noHintFilter = new FilterBase() { 695 @Override 696 public boolean filterRowKey(Cell cell) { 697 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 698 acceptedStartRow, 0, acceptedStartRow.length) < 0; 699 } 700 }; 701 702 List<Cell> hintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(andFilter)); 703 List<Cell> noHintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(noHintFilter)); 704 705 assertEquals(noHintResults.size(), hintResults.size(), 706 "AND FilterList with hints must return same cells as no-hint path"); 707 for (int i = 0; i < hintResults.size(); i++) { 708 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 709 "Cell mismatch at index " + i); 710 } 711 assertEquals(acceptedCount * CELLS_PER_ROW, hintResults.size()); 712 assertTrue(hintCallsA.get() > 0, "Sub-filter A hint must be consulted"); 713 assertTrue(hintCallsB.get() > 0, "Sub-filter B hint must be consulted"); 714 } 715 716 @Test 717 public void testFilterListORHintDelegation() throws IOException { 718 final String prefix = "row"; 719 final int rejectedCount = 5; 720 final int acceptedCount = 5; 721 writeRows(prefix, rejectedCount + acceptedCount); 722 723 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 724 725 // Both filters reject the same rows, OR requires ALL to reject. 726 // Both provide hints to the accepted start. OR merging takes min — same target. 727 FilterBase filterA = new FilterBase() { 728 @Override 729 public boolean filterRowKey(Cell cell) { 730 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 731 acceptedStartRow, 0, acceptedStartRow.length) < 0; 732 } 733 734 @Override 735 public Cell getHintForRejectedRow(Cell firstRowCell) { 736 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 737 } 738 }; 739 740 FilterBase filterB = new FilterBase() { 741 @Override 742 public boolean filterRowKey(Cell cell) { 743 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 744 acceptedStartRow, 0, acceptedStartRow.length) < 0; 745 } 746 747 @Override 748 public Cell getHintForRejectedRow(Cell firstRowCell) { 749 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 750 } 751 }; 752 753 FilterList orFilter = 754 new FilterList(FilterList.Operator.MUST_PASS_ONE, Arrays.asList(filterA, filterB)); 755 756 FilterBase noHintFilter = new FilterBase() { 757 @Override 758 public boolean filterRowKey(Cell cell) { 759 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 760 acceptedStartRow, 0, acceptedStartRow.length) < 0; 761 } 762 }; 763 764 List<Cell> hintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(orFilter)); 765 List<Cell> noHintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(noHintFilter)); 766 767 assertEquals(noHintResults.size(), hintResults.size(), 768 "OR FilterList with hints must return same cells as no-hint path"); 769 for (int i = 0; i < hintResults.size(); i++) { 770 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 771 "Cell mismatch at index " + i); 772 } 773 assertEquals(acceptedCount * CELLS_PER_ROW, hintResults.size()); 774 } 775 776 @Test 777 public void testFilterListANDWithOneNullHintSubFilter() throws IOException { 778 final String prefix = "row"; 779 final int rejectedCount = 3; 780 final int acceptedCount = 3; 781 writeRows(prefix, rejectedCount + acceptedCount); 782 783 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 784 final AtomicInteger hintCalls = new AtomicInteger(0); 785 786 // One sub-filter provides a hint, the other returns null. 787 // AND ignores nulls, so the non-null hint should be used. 788 FilterBase hintProvider = new FilterBase() { 789 @Override 790 public boolean filterRowKey(Cell cell) { 791 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 792 acceptedStartRow, 0, acceptedStartRow.length) < 0; 793 } 794 795 @Override 796 public Cell getHintForRejectedRow(Cell firstRowCell) { 797 hintCalls.incrementAndGet(); 798 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 799 } 800 }; 801 802 FilterBase noHintProvider = new FilterBase() { 803 @Override 804 public boolean filterRowKey(Cell cell) { 805 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 806 acceptedStartRow, 0, acceptedStartRow.length) < 0; 807 } 808 }; 809 810 FilterList andFilter = new FilterList(FilterList.Operator.MUST_PASS_ALL, 811 Arrays.asList(hintProvider, noHintProvider)); 812 813 List<Cell> results = scanAll(new Scan().addFamily(FAMILY).setFilter(andFilter)); 814 assertEquals(acceptedCount * CELLS_PER_ROW, results.size()); 815 assertEquals(1, hintCalls.get(), 816 "Hint provider must be called; AND ignores the null from the other sub-filter"); 817 } 818 819 @Test 820 public void testFilterListORWithOneNullHintSubFilter() throws IOException { 821 final String prefix = "row"; 822 final int rejectedCount = 3; 823 final int acceptedCount = 3; 824 writeRows(prefix, rejectedCount + acceptedCount); 825 826 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 827 828 // One sub-filter provides a hint, the other returns null. 829 // OR returns null if ANY sub-filter returns null, so no hint optimization. 830 FilterBase hintProvider = new FilterBase() { 831 @Override 832 public boolean filterRowKey(Cell cell) { 833 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 834 acceptedStartRow, 0, acceptedStartRow.length) < 0; 835 } 836 837 @Override 838 public Cell getHintForRejectedRow(Cell firstRowCell) { 839 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 840 } 841 }; 842 843 FilterBase noHintProvider = new FilterBase() { 844 @Override 845 public boolean filterRowKey(Cell cell) { 846 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 847 acceptedStartRow, 0, acceptedStartRow.length) < 0; 848 } 849 }; 850 851 FilterList orFilter = new FilterList(FilterList.Operator.MUST_PASS_ONE, 852 Arrays.asList(hintProvider, noHintProvider)); 853 854 FilterBase baseline = new FilterBase() { 855 @Override 856 public boolean filterRowKey(Cell cell) { 857 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 858 acceptedStartRow, 0, acceptedStartRow.length) < 0; 859 } 860 }; 861 862 List<Cell> orResults = scanAll(new Scan().addFamily(FAMILY).setFilter(orFilter)); 863 List<Cell> baselineResults = scanAll(new Scan().addFamily(FAMILY).setFilter(baseline)); 864 865 assertEquals(baselineResults.size(), orResults.size(), 866 "OR with one null hint must still return correct results (falls back to no-hint path)"); 867 for (int i = 0; i < orResults.size(); i++) { 868 assertTrue(CellUtil.equals(orResults.get(i), baselineResults.get(i)), 869 "Cell mismatch at index " + i); 870 } 871 } 872 873 @Test 874 public void testNestedFilterListHintDelegation() throws IOException { 875 final String prefix = "row"; 876 final int rejectedCount = 4; 877 final int acceptedCount = 4; 878 writeRows(prefix, rejectedCount + acceptedCount); 879 880 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 881 882 // Nested: AND(OR(hintA, hintB), hintC) 883 // All filters reject the same rows and hint to the same target. 884 FilterBase hintFilter = new FilterBase() { 885 @Override 886 public boolean filterRowKey(Cell cell) { 887 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 888 acceptedStartRow, 0, acceptedStartRow.length) < 0; 889 } 890 891 @Override 892 public Cell getHintForRejectedRow(Cell firstRowCell) { 893 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 894 } 895 }; 896 897 FilterBase hintFilter2 = new FilterBase() { 898 @Override 899 public boolean filterRowKey(Cell cell) { 900 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 901 acceptedStartRow, 0, acceptedStartRow.length) < 0; 902 } 903 904 @Override 905 public Cell getHintForRejectedRow(Cell firstRowCell) { 906 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 907 } 908 }; 909 910 FilterBase hintFilter3 = new FilterBase() { 911 @Override 912 public boolean filterRowKey(Cell cell) { 913 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 914 acceptedStartRow, 0, acceptedStartRow.length) < 0; 915 } 916 917 @Override 918 public Cell getHintForRejectedRow(Cell firstRowCell) { 919 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 920 } 921 }; 922 923 FilterList innerOR = 924 new FilterList(FilterList.Operator.MUST_PASS_ONE, Arrays.asList(hintFilter, hintFilter2)); 925 FilterList outerAND = 926 new FilterList(FilterList.Operator.MUST_PASS_ALL, Arrays.asList(innerOR, hintFilter3)); 927 928 FilterBase baseline = new FilterBase() { 929 @Override 930 public boolean filterRowKey(Cell cell) { 931 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 932 acceptedStartRow, 0, acceptedStartRow.length) < 0; 933 } 934 }; 935 936 List<Cell> nestedResults = scanAll(new Scan().addFamily(FAMILY).setFilter(outerAND)); 937 List<Cell> baselineResults = scanAll(new Scan().addFamily(FAMILY).setFilter(baseline)); 938 939 assertEquals(baselineResults.size(), nestedResults.size(), 940 "Nested FilterList must return same results as baseline"); 941 for (int i = 0; i < nestedResults.size(); i++) { 942 assertTrue(CellUtil.equals(nestedResults.get(i), baselineResults.get(i)), 943 "Cell mismatch at index " + i); 944 } 945 assertEquals(acceptedCount * CELLS_PER_ROW, nestedResults.size()); 946 } 947 948 @Test 949 public void testWhileMatchFilterHintDelegation() throws IOException { 950 final String prefix = "row"; 951 final int rejectedCount = 3; 952 final int acceptedCount = 3; 953 writeRows(prefix, rejectedCount + acceptedCount); 954 955 final byte[] acceptedStartRow = Bytes.toBytes(String.format("%s-%02d", prefix, rejectedCount)); 956 final AtomicInteger hintCalls = new AtomicInteger(0); 957 958 FilterBase innerHintFilter = new FilterBase() { 959 @Override 960 public boolean filterRowKey(Cell cell) { 961 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 962 acceptedStartRow, 0, acceptedStartRow.length) < 0; 963 } 964 965 @Override 966 public Cell getHintForRejectedRow(Cell firstRowCell) { 967 hintCalls.incrementAndGet(); 968 return PrivateCellUtil.createFirstOnRow(acceptedStartRow); 969 } 970 }; 971 972 WhileMatchFilter wmFilter = new WhileMatchFilter(innerHintFilter); 973 974 // WhileMatchFilter delegates filterRowKey and sets filterAllRemaining on first true. 975 // The scanner checks isFilterDoneInternal() BEFORE calling getHintForRejectedRow, 976 // so the hint path is short-circuited and the scan terminates immediately. 977 List<Cell> results = scanAll(new Scan().addFamily(FAMILY).setFilter(wmFilter)); 978 979 assertTrue(results.isEmpty(), "WhileMatchFilter terminates scan on first rejection"); 980 assertEquals(0, hintCalls.get(), 981 "WhileMatchFilter sets filterAllRemaining before getHintForRejectedRow is consulted"); 982 } 983 984 @Test 985 public void testFilterListANDReversedScanHint() throws IOException { 986 final String prefix = "row"; 987 final int totalRows = 10; 988 writeRows(prefix, totalRows); 989 990 // Accept rows 00-04, reject rows 05-09. 991 // In reversed scan, scanner starts at row-09 and moves backward. 992 final byte[] rejectThreshold = Bytes.toBytes(String.format("%s-%02d", prefix, 5)); 993 final byte[] hintTarget = Bytes.toBytes(String.format("%s-%02d", prefix, 4)); 994 995 FilterBase rejectFilter = new FilterBase() { 996 @Override 997 public boolean filterRowKey(Cell cell) { 998 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 999 rejectThreshold, 0, rejectThreshold.length) >= 0; 1000 } 1001 1002 @Override 1003 public Cell getHintForRejectedRow(Cell firstRowCell) { 1004 return PrivateCellUtil.createFirstOnRow(hintTarget); 1005 } 1006 }; 1007 1008 FilterList andFilter = 1009 new FilterList(FilterList.Operator.MUST_PASS_ALL, Arrays.asList(rejectFilter)); 1010 1011 FilterBase noHintFilter = new FilterBase() { 1012 @Override 1013 public boolean filterRowKey(Cell cell) { 1014 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 1015 rejectThreshold, 0, rejectThreshold.length) >= 0; 1016 } 1017 }; 1018 1019 Scan hintScan = new Scan().addFamily(FAMILY).setReversed(true).setFilter(andFilter); 1020 Scan noHintScan = new Scan().addFamily(FAMILY).setReversed(true).setFilter(noHintFilter); 1021 1022 List<Cell> hintResults = scanAll(hintScan); 1023 List<Cell> noHintResults = scanAll(noHintScan); 1024 1025 assertEquals(noHintResults.size(), hintResults.size(), 1026 "Reversed AND FilterList must return same cells"); 1027 for (int i = 0; i < hintResults.size(); i++) { 1028 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 1029 "Cell mismatch at index " + i); 1030 } 1031 assertEquals(5 * CELLS_PER_ROW, hintResults.size()); 1032 } 1033 1034 @Test 1035 public void testFilterListORReversedScanHint() throws IOException { 1036 final String prefix = "row"; 1037 final int totalRows = 10; 1038 writeRows(prefix, totalRows); 1039 1040 final byte[] rejectThreshold = Bytes.toBytes(String.format("%s-%02d", prefix, 5)); 1041 final byte[] hintTarget = Bytes.toBytes(String.format("%s-%02d", prefix, 4)); 1042 1043 FilterBase rejectFilterA = new FilterBase() { 1044 @Override 1045 public boolean filterRowKey(Cell cell) { 1046 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 1047 rejectThreshold, 0, rejectThreshold.length) >= 0; 1048 } 1049 1050 @Override 1051 public Cell getHintForRejectedRow(Cell firstRowCell) { 1052 return PrivateCellUtil.createFirstOnRow(hintTarget); 1053 } 1054 }; 1055 1056 FilterBase rejectFilterB = new FilterBase() { 1057 @Override 1058 public boolean filterRowKey(Cell cell) { 1059 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 1060 rejectThreshold, 0, rejectThreshold.length) >= 0; 1061 } 1062 1063 @Override 1064 public Cell getHintForRejectedRow(Cell firstRowCell) { 1065 return PrivateCellUtil.createFirstOnRow(hintTarget); 1066 } 1067 }; 1068 1069 FilterList orFilter = new FilterList(FilterList.Operator.MUST_PASS_ONE, 1070 Arrays.asList(rejectFilterA, rejectFilterB)); 1071 1072 FilterBase noHintFilter = new FilterBase() { 1073 @Override 1074 public boolean filterRowKey(Cell cell) { 1075 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 1076 rejectThreshold, 0, rejectThreshold.length) >= 0; 1077 } 1078 }; 1079 1080 Scan hintScan = new Scan().addFamily(FAMILY).setReversed(true).setFilter(orFilter); 1081 Scan noHintScan = new Scan().addFamily(FAMILY).setReversed(true).setFilter(noHintFilter); 1082 1083 List<Cell> hintResults = scanAll(hintScan); 1084 List<Cell> noHintResults = scanAll(noHintScan); 1085 1086 assertEquals(noHintResults.size(), hintResults.size(), 1087 "Reversed OR FilterList must return same cells"); 1088 for (int i = 0; i < hintResults.size(); i++) { 1089 assertTrue(CellUtil.equals(hintResults.get(i), noHintResults.get(i)), 1090 "Cell mismatch at index " + i); 1091 } 1092 assertEquals(5 * CELLS_PER_ROW, hintResults.size()); 1093 } 1094 1095 @Test 1096 public void testColumnRangeFilterGetSkipHintIntegration() throws IOException { 1097 final long insideTs = 2000; 1098 final long outsideTs = 500; 1099 final int rowCount = 5; 1100 1101 for (int i = 0; i < rowCount; i++) { 1102 byte[] row = Bytes.toBytes(String.format("colrange-%02d", i)); 1103 Put p = new Put(row); 1104 p.setDurability(Durability.SKIP_WAL); 1105 // Qualifiers: "a", "b", "c", "d" — ColumnRangeFilter will select "b" to "c". 1106 p.addColumn(FAMILY, Bytes.toBytes("a"), insideTs, VALUE); 1107 p.addColumn(FAMILY, Bytes.toBytes("a"), outsideTs, VALUE); 1108 p.addColumn(FAMILY, Bytes.toBytes("b"), insideTs, VALUE); 1109 p.addColumn(FAMILY, Bytes.toBytes("b"), outsideTs, VALUE); 1110 p.addColumn(FAMILY, Bytes.toBytes("c"), insideTs, VALUE); 1111 p.addColumn(FAMILY, Bytes.toBytes("c"), outsideTs, VALUE); 1112 p.addColumn(FAMILY, Bytes.toBytes("d"), insideTs, VALUE); 1113 p.addColumn(FAMILY, Bytes.toBytes("d"), outsideTs, VALUE); 1114 region.put(p); 1115 } 1116 region.flush(true); 1117 1118 ColumnRangeFilter colFilter = 1119 new ColumnRangeFilter(Bytes.toBytes("b"), true, Bytes.toBytes("c"), true); 1120 1121 // Baseline: same column range logic via filterCell, but no getSkipHint override. 1122 FilterBase noHintBaseline = new FilterBase() { 1123 private final ColumnRangeFilter delegate = 1124 new ColumnRangeFilter(Bytes.toBytes("b"), true, Bytes.toBytes("c"), true); 1125 1126 @Override 1127 public ReturnCode filterCell(Cell c) throws IOException { 1128 return delegate.filterCell(c); 1129 } 1130 }; 1131 1132 // Time range [1000, 3000): insideTs cells pass, outsideTs cells hit the time-range gate. 1133 // ColumnRangeFilter.getSkipHint() should be consulted for structurally skipped cells. 1134 Scan hintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(colFilter); 1135 Scan noHintScan = 1136 new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(noHintBaseline); 1137 1138 List<Cell> hintResults = scanAll(hintScan); 1139 List<Cell> noHintResults = scanAll(noHintScan); 1140 1141 assertEquals(noHintResults.size(), hintResults.size(), 1142 "Hint-aware scan must return same cells as no-hint baseline"); 1143 // Should get "b" and "c" qualifiers for each row, only insideTs versions. 1144 assertEquals(rowCount * 2, hintResults.size()); 1145 } 1146 1147 @Test 1148 public void testColumnPrefixFilterGetSkipHintIntegration() throws IOException { 1149 final long insideTs = 2000; 1150 final long outsideTs = 500; 1151 final int rowCount = 5; 1152 1153 for (int i = 0; i < rowCount; i++) { 1154 byte[] row = Bytes.toBytes(String.format("colpfx-%02d", i)); 1155 Put p = new Put(row); 1156 p.setDurability(Durability.SKIP_WAL); 1157 // Qualifiers: "aaa", "abc", "abd", "xyz" 1158 p.addColumn(FAMILY, Bytes.toBytes("aaa"), insideTs, VALUE); 1159 p.addColumn(FAMILY, Bytes.toBytes("aaa"), outsideTs, VALUE); 1160 p.addColumn(FAMILY, Bytes.toBytes("abc"), insideTs, VALUE); 1161 p.addColumn(FAMILY, Bytes.toBytes("abc"), outsideTs, VALUE); 1162 p.addColumn(FAMILY, Bytes.toBytes("abd"), insideTs, VALUE); 1163 p.addColumn(FAMILY, Bytes.toBytes("abd"), outsideTs, VALUE); 1164 p.addColumn(FAMILY, Bytes.toBytes("xyz"), insideTs, VALUE); 1165 p.addColumn(FAMILY, Bytes.toBytes("xyz"), outsideTs, VALUE); 1166 region.put(p); 1167 } 1168 region.flush(true); 1169 1170 // ColumnPrefixFilter with prefix "ab" should match "abc" and "abd". 1171 ColumnPrefixFilter prefixFilter = new ColumnPrefixFilter(Bytes.toBytes("ab")); 1172 1173 // Baseline: same prefix logic via filterCell, but no getSkipHint override. 1174 FilterBase noHintBaseline = new FilterBase() { 1175 private final ColumnPrefixFilter delegate = new ColumnPrefixFilter(Bytes.toBytes("ab")); 1176 1177 @Override 1178 public ReturnCode filterCell(Cell c) throws IOException { 1179 return delegate.filterCell(c); 1180 } 1181 }; 1182 1183 Scan hintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(prefixFilter); 1184 Scan noHintScan = 1185 new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(noHintBaseline); 1186 1187 List<Cell> hintResults = scanAll(hintScan); 1188 List<Cell> noHintResults = scanAll(noHintScan); 1189 1190 assertEquals(noHintResults.size(), hintResults.size(), 1191 "Hint-aware scan must return same cells as no-hint baseline"); 1192 // Should get "abc" and "abd" qualifiers for each row, only insideTs versions. 1193 assertEquals(rowCount * 2, hintResults.size()); 1194 } 1195 1196 @Test 1197 public void testMultipleColumnPrefixFilterGetSkipHintIntegration() throws IOException { 1198 final long insideTs = 2000; 1199 final long outsideTs = 500; 1200 final int rowCount = 5; 1201 1202 for (int i = 0; i < rowCount; i++) { 1203 byte[] row = Bytes.toBytes(String.format("mcpfx-%02d", i)); 1204 Put p = new Put(row); 1205 p.setDurability(Durability.SKIP_WAL); 1206 // Qualifiers: "aaa", "abc", "abd", "bbb", "xyz" 1207 p.addColumn(FAMILY, Bytes.toBytes("aaa"), insideTs, VALUE); 1208 p.addColumn(FAMILY, Bytes.toBytes("aaa"), outsideTs, VALUE); 1209 p.addColumn(FAMILY, Bytes.toBytes("abc"), insideTs, VALUE); 1210 p.addColumn(FAMILY, Bytes.toBytes("abc"), outsideTs, VALUE); 1211 p.addColumn(FAMILY, Bytes.toBytes("abd"), insideTs, VALUE); 1212 p.addColumn(FAMILY, Bytes.toBytes("abd"), outsideTs, VALUE); 1213 p.addColumn(FAMILY, Bytes.toBytes("bbb"), insideTs, VALUE); 1214 p.addColumn(FAMILY, Bytes.toBytes("bbb"), outsideTs, VALUE); 1215 p.addColumn(FAMILY, Bytes.toBytes("xyz"), insideTs, VALUE); 1216 p.addColumn(FAMILY, Bytes.toBytes("xyz"), outsideTs, VALUE); 1217 region.put(p); 1218 } 1219 region.flush(true); 1220 1221 // MultipleColumnPrefixFilter with prefixes "ab" and "bb" should match "abc", "abd", "bbb". 1222 MultipleColumnPrefixFilter mcpFilter = 1223 new MultipleColumnPrefixFilter(new byte[][] { Bytes.toBytes("ab"), Bytes.toBytes("bb") }); 1224 1225 // Baseline: same prefix logic via filterCell, but no getSkipHint override. 1226 FilterBase noHintBaseline = new FilterBase() { 1227 private final MultipleColumnPrefixFilter delegate = 1228 new MultipleColumnPrefixFilter(new byte[][] { Bytes.toBytes("ab"), Bytes.toBytes("bb") }); 1229 1230 @Override 1231 public ReturnCode filterCell(Cell c) throws IOException { 1232 return delegate.filterCell(c); 1233 } 1234 }; 1235 1236 Scan hintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(mcpFilter); 1237 Scan noHintScan = 1238 new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(noHintBaseline); 1239 1240 List<Cell> hintResults = scanAll(hintScan); 1241 List<Cell> noHintResults = scanAll(noHintScan); 1242 1243 assertEquals(noHintResults.size(), hintResults.size(), 1244 "Hint-aware scan must return same cells as no-hint baseline"); 1245 // Should get "abc", "abd", "bbb" qualifiers for each row, only insideTs versions. 1246 assertEquals(rowCount * 3, hintResults.size()); 1247 } 1248 1249 @Test 1250 public void testFilterListANDGetSkipHintComposition() throws IOException { 1251 final long insideTs = 2000; 1252 final long outsideTs = 500; 1253 final int rowCount = 5; 1254 1255 for (int i = 0; i < rowCount; i++) { 1256 byte[] row = Bytes.toBytes(String.format("composed-%02d", i)); 1257 Put p = new Put(row); 1258 p.setDurability(Durability.SKIP_WAL); 1259 p.addColumn(FAMILY, Bytes.toBytes("a"), insideTs, VALUE); 1260 p.addColumn(FAMILY, Bytes.toBytes("a"), outsideTs, VALUE); 1261 p.addColumn(FAMILY, Bytes.toBytes("b"), insideTs, VALUE); 1262 p.addColumn(FAMILY, Bytes.toBytes("b"), outsideTs, VALUE); 1263 p.addColumn(FAMILY, Bytes.toBytes("c"), insideTs, VALUE); 1264 p.addColumn(FAMILY, Bytes.toBytes("c"), outsideTs, VALUE); 1265 region.put(p); 1266 } 1267 region.flush(true); 1268 1269 // Compose ColumnRangeFilter("b","c") AND a custom skip-hint filter. 1270 // The AND composition should take the max hint. 1271 final AtomicInteger skipHintCalls = new AtomicInteger(0); 1272 ColumnRangeFilter colRange = 1273 new ColumnRangeFilter(Bytes.toBytes("b"), true, Bytes.toBytes("c"), true); 1274 FilterBase customSkipHint = new FilterBase() { 1275 @Override 1276 public Cell getSkipHint(Cell skippedCell) { 1277 skipHintCalls.incrementAndGet(); 1278 return PrivateCellUtil.createFirstOnNextRow(skippedCell); 1279 } 1280 }; 1281 1282 FilterList andFilter = 1283 new FilterList(FilterList.Operator.MUST_PASS_ALL, Arrays.asList(colRange, customSkipHint)); 1284 1285 // Baseline: same column range via filterCell, no getSkipHint. 1286 FilterBase noHintBaseline = new FilterBase() { 1287 private final ColumnRangeFilter delegate = 1288 new ColumnRangeFilter(Bytes.toBytes("b"), true, Bytes.toBytes("c"), true); 1289 1290 @Override 1291 public ReturnCode filterCell(Cell c) throws IOException { 1292 return delegate.filterCell(c); 1293 } 1294 }; 1295 1296 Scan hintScan = new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(andFilter); 1297 Scan noHintScan = 1298 new Scan().addFamily(FAMILY).setTimeRange(1000, 3000).setFilter(noHintBaseline); 1299 1300 List<Cell> hintResults = scanAll(hintScan); 1301 List<Cell> noHintResults = scanAll(noHintScan); 1302 1303 assertEquals(noHintResults.size(), hintResults.size(), 1304 "AND composed skip-hint must return same cells as no-hint baseline"); 1305 // Should get "b" and "c" for each row, only insideTs. 1306 assertEquals(rowCount * 2, hintResults.size()); 1307 } 1308 1309 @Test 1310 public void testFilterListANDDivergentHints() throws IOException { 1311 final String prefix = "row"; 1312 final int totalRows = 10; 1313 writeRows(prefix, totalRows); 1314 1315 // Filter A rejects rows 0-4, hints to row-03 (a conservative hint). 1316 // Filter B rejects rows 0-6, hints to row-07 (a more aggressive hint). 1317 // Both reject rows 0-4 (overlap). AND merges => takes max => row-07. 1318 // Rows 0-6 are rejected by the composite (at least one rejects each). 1319 // Scan should return rows 07-09. 1320 final byte[] rejectThresholdA = Bytes.toBytes(String.format("%s-%02d", prefix, 5)); 1321 final byte[] hintTargetA = Bytes.toBytes(String.format("%s-%02d", prefix, 3)); 1322 final byte[] rejectThresholdB = Bytes.toBytes(String.format("%s-%02d", prefix, 7)); 1323 final byte[] hintTargetB = Bytes.toBytes(String.format("%s-%02d", prefix, 7)); 1324 1325 FilterBase filterA = new FilterBase() { 1326 @Override 1327 public boolean filterRowKey(Cell cell) { 1328 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 1329 rejectThresholdA, 0, rejectThresholdA.length) < 0; 1330 } 1331 1332 @Override 1333 public Cell getHintForRejectedRow(Cell firstRowCell) { 1334 return PrivateCellUtil.createFirstOnRow(hintTargetA); 1335 } 1336 }; 1337 1338 FilterBase filterB = new FilterBase() { 1339 @Override 1340 public boolean filterRowKey(Cell cell) { 1341 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 1342 rejectThresholdB, 0, rejectThresholdB.length) < 0; 1343 } 1344 1345 @Override 1346 public Cell getHintForRejectedRow(Cell firstRowCell) { 1347 return PrivateCellUtil.createFirstOnRow(hintTargetB); 1348 } 1349 }; 1350 1351 FilterList andFilter = 1352 new FilterList(FilterList.Operator.MUST_PASS_ALL, Arrays.asList(filterA, filterB)); 1353 1354 FilterBase baseline = new FilterBase() { 1355 @Override 1356 public boolean filterRowKey(Cell cell) { 1357 return Bytes.compareTo(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(), 1358 rejectThresholdB, 0, rejectThresholdB.length) < 0; 1359 } 1360 }; 1361 1362 List<Cell> hintResults = scanAll(new Scan().addFamily(FAMILY).setFilter(andFilter)); 1363 List<Cell> baselineResults = scanAll(new Scan().addFamily(FAMILY).setFilter(baseline)); 1364 1365 assertEquals(baselineResults.size(), hintResults.size(), 1366 "AND with divergent hints must return same cells as baseline"); 1367 for (int i = 0; i < hintResults.size(); i++) { 1368 assertTrue(CellUtil.equals(hintResults.get(i), baselineResults.get(i)), 1369 "Cell mismatch at index " + i); 1370 } 1371 assertEquals(3 * CELLS_PER_ROW, hintResults.size()); 1372 } 1373}