blob: 5a58fa5a2dbf9b5cada002a52b68f246d44bc291 [file]
/*
* 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