| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| #include "iceberg/deletes/position_delete_range_consumer.h" | |
| #include <cstdint> | |
| #include <limits> | |
| #include <set> | |
| #include <span> | |
| #include <vector> | |
| #include <gtest/gtest.h> | |
| #include "iceberg/deletes/position_delete_index.h" | |
| #include "iceberg/deletes/roaring_position_bitmap.h" | |
| namespace iceberg { | |
| namespace { | |
| // Reference set: positions a per-pos `Delete(pos)` loop would accept. | |
| std::set<int64_t> ExpectedValidSet(const std::vector<int64_t>& positions) { | |
| std::set<int64_t> expected; | |
| for (int64_t pos : positions) { | |
| if (pos >= 0 && pos <= RoaringPositionBitmap::kMaxPosition) { | |
| expected.insert(pos); | |
| } | |
| } | |
| return expected; | |
| } | |
| // Strict contents check: cardinality plus per-position membership. | |
| // Weaker checks would miss divergences at the 32-bit key boundary. | |
| void AssertMatchesBaseline(const std::vector<int64_t>& positions) { | |
| PositionDeleteIndex index; | |
| std::vector<uint32_t> scratch; | |
| ForEachPositionDelete(std::span<const int64_t>(positions), index, scratch); | |
| const auto expected = ExpectedValidSet(positions); | |
| ASSERT_EQ(index.Cardinality(), static_cast<int64_t>(expected.size())) | |
| << "input size=" << positions.size(); | |
| for (int64_t pos : expected) { | |
| ASSERT_TRUE(index.IsDeleted(pos)) << "missing pos=" << pos; | |
| } | |
| } | |
| } // namespace | |
| TEST(PositionDeleteRangeConsumerTest, EmptySpan) { AssertMatchesBaseline({}); } | |
| TEST(PositionDeleteRangeConsumerTest, SinglePosition) { AssertMatchesBaseline({42}); } | |
| TEST(PositionDeleteRangeConsumerTest, FullyContiguousRunBecomesSingleRange) { | |
| std::vector<int64_t> positions; | |
| for (int64_t i = 100; i < 200; ++i) { | |
| positions.push_back(i); | |
| } | |
| AssertMatchesBaseline(positions); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, AlternatingPositionsProduceNoCoalescing) { | |
| std::vector<int64_t> positions; | |
| for (int64_t i = 0; i < 50; ++i) { | |
| positions.push_back(i * 2); | |
| } | |
| AssertMatchesBaseline(positions); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, MixedShortAndLongRuns) { | |
| AssertMatchesBaseline({1, 2, 3, 7, 10, 11, 20, 30, 31, 32, 33, 34}); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, UnsortedInputStillCorrect) { | |
| AssertMatchesBaseline({10, 5, 11, 12, 4, 13, 100}); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, DuplicatesAreIdempotent) { | |
| AssertMatchesBaseline({5, 5, 5, 6, 6, 7}); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, InvalidPositionsSilentlySkipped) { | |
| // Invalids at the edges, mid-run, and mixed with valid contiguous runs | |
| // must all be dropped without breaking coalescing around them. We stay | |
| // well below `kMaxPosition` to avoid forcing the bitmap to resize its | |
| // backing vector to ~2^31 empty containers. | |
| AssertMatchesBaseline({std::numeric_limits<int64_t>::min(), -5, -4, 10, 11, -999, 12, | |
| 13, RoaringPositionBitmap::kMaxPosition + 1, | |
| std::numeric_limits<int64_t>::max()}); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, ContiguousRunAcrossKeyBoundary) { | |
| // Pins `last_position + 1` and the adjacency check at a non-zero | |
| // high-32 key. The coalesced run must survive the key transition. | |
| constexpr int64_t kBoundary = int64_t{1} << 32; | |
| std::vector<int64_t> positions; | |
| for (int64_t i = kBoundary - 3; i < kBoundary + 3; ++i) { | |
| positions.push_back(i); | |
| } | |
| AssertMatchesBaseline(positions); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, DispatcherAgreesAtBothDensities) { | |
| // Above the sniff threshold at densities below and above the 10% | |
| // cutoff. We can't observe the choice directly; agreement with the | |
| // baseline is the contract. | |
| std::vector<int64_t> low_density; | |
| std::vector<int64_t> high_density; | |
| int64_t lo = 0; | |
| int64_t hi = 0; | |
| for (int64_t i = 0; i < 2'048; ++i) { | |
| low_density.push_back(lo); | |
| ++lo; | |
| if ((i + 1) % 20 == 0) { | |
| lo += 5; | |
| } | |
| high_density.push_back(hi); | |
| hi += ((i % 5 == 0) ? 5 : 1); | |
| } | |
| AssertMatchesBaseline(low_density); | |
| AssertMatchesBaseline(high_density); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, DispatcherSkipsSniffOnSmallInputs) { | |
| // Below the 64-element threshold the dispatcher bypasses the sniff. | |
| // Exercise both a scattered tiny input (where bulk would win at large | |
| // n) and a contiguous tiny input (the range path always wins). | |
| std::vector<int64_t> scattered; | |
| std::vector<int64_t> contiguous; | |
| for (int64_t i = 0; i < 32; ++i) { | |
| scattered.push_back(i * 100); | |
| contiguous.push_back(i); | |
| } | |
| AssertMatchesBaseline(scattered); | |
| AssertMatchesBaseline(contiguous); | |
| } | |
| TEST(PositionDeleteRangeConsumerTest, DispatcherAgreesAtThresholdBoundary) { | |
| // The dispatcher selects the bulk path when | |
| // boundaries * 100 > (sniff - 1) * kBulkThresholdPercent | |
| // With `sniff = 1024` and `kBulkThresholdPercent = 10`, the cutoff is | |
| // 102.3 boundaries: 102 stays on coalesce, 103 flips to bulk. Both | |
| // inputs must still produce the same cardinality and membership as | |
| // the per-position baseline; this test guards against arithmetic | |
| // regressions around the threshold constant. | |
| auto build = [](int64_t target_boundaries) { | |
| std::vector<int64_t> positions; | |
| positions.reserve(1024); | |
| int64_t pos = 0; | |
| positions.push_back(pos); | |
| for (int64_t i = 1; i < 1024; ++i) { | |
| pos += (i <= target_boundaries) ? 2 : 1; | |
| positions.push_back(pos); | |
| } | |
| return positions; | |
| }; | |
| AssertMatchesBaseline(build(/*target_boundaries=*/102)); | |
| AssertMatchesBaseline(build(/*target_boundaries=*/103)); | |
| } | |
| } // namespace iceberg |