blob: 8be6989c3a16caa43357a897d0fe35d9cb36bc3e [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/util/bucket_util.h"
#include <array>
#include <chrono>
#include <cstdint>
#include <limits>
#include <numeric>
#include <type_traits>
#include <gtest/gtest.h>
#include "iceberg/expression/literal.h"
#include "iceberg/test/temporal_test_helper.h"
#include "iceberg/util/decimal.h"
#include "iceberg/util/murmurhash3_internal.h"
#include "iceberg/util/uuid.h"
namespace iceberg {
static_assert(std::is_convertible_v<decltype(&MurmurHash3_x86_32),
uint32_t (*)(const void*, size_t, uint32_t)>);
// The following tests are from
// https://iceberg.apache.org/spec/#appendix-b-32-bit-hash-requirements
TEST(BucketUtilsTest, HashHelper) {
// int and long
EXPECT_EQ(BucketUtils::HashInt(34), 2017239379);
EXPECT_EQ(BucketUtils::HashLong(34L), 2017239379);
const auto bucket = BucketUtils::BucketIndex(Literal::Int(34), 100);
ASSERT_TRUE(bucket.has_value());
EXPECT_EQ(*bucket, 79);
// decimal hash
auto decimal = Decimal::FromString("14.20");
ASSERT_TRUE(decimal.has_value());
EXPECT_EQ(BucketUtils::HashBytes(decimal->ToBigEndian()), -500754589);
// date hash
EXPECT_EQ(BucketUtils::HashInt(
TemporalTestHelper::CreateDate({.year = 2017, .month = 11, .day = 16})),
-653330422);
// time
EXPECT_EQ(BucketUtils::HashLong(
TemporalTestHelper::CreateTime({.hour = 22, .minute = 31, .second = 8})),
-662762989);
// timestamp
// 2017-11-16T22:31:08 in microseconds
EXPECT_EQ(
BucketUtils::HashLong(TemporalTestHelper::CreateTimestamp(
{.year = 2017, .month = 11, .day = 16, .hour = 22, .minute = 31, .second = 8})),
-2047944441);
// 2017-11-16T22:31:08.000001 in microseconds
EXPECT_EQ(
BucketUtils::HashLong(TemporalTestHelper::CreateTimestamp({.year = 2017,
.month = 11,
.day = 16,
.hour = 22,
.minute = 31,
.second = 8,
.microsecond = 1})),
-1207196810);
// 2017-11-16T14:31:08-08:00 in microseconds
EXPECT_EQ(BucketUtils::HashLong(
TemporalTestHelper::CreateTimestampTz({.year = 2017,
.month = 11,
.day = 16,
.hour = 14,
.minute = 31,
.second = 8,
.tz_offset_minutes = -480})),
-2047944441);
// 2017-11-16T14:31:08.000001-08:00 in microseconds
EXPECT_EQ(BucketUtils::HashLong(
TemporalTestHelper::CreateTimestampTz({.year = 2017,
.month = 11,
.day = 16,
.hour = 14,
.minute = 31,
.second = 8,
.microsecond = 1,
.tz_offset_minutes = -480})),
-1207196810);
// string
std::string str = "iceberg";
EXPECT_EQ(BucketUtils::HashBytes(std::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(str.data()), str.size())),
1210000089);
// uuid
auto uuid = Uuid::FromString("f79c3e09-677c-4bbd-a479-3f349cb785e7");
EXPECT_EQ(BucketUtils::HashBytes(uuid->bytes()), 1488055340);
// fixed & binary
std::vector<uint8_t> fixed = {0, 1, 2, 3};
EXPECT_EQ(BucketUtils::HashBytes(fixed), -188683207);
}
TEST(BucketUtilsTest, BucketByteLiterals) {
struct BucketCase {
Literal literal;
int32_t bucket;
};
const std::array<BucketCase, 3> cases = {{
{.literal = Literal::String("iceberg"), .bucket = 89},
{.literal = Literal::Binary({0, 1, 2, 3}), .bucket = 41},
{.literal = Literal::Fixed({0, 1, 2, 3}), .bucket = 41},
}};
for (const auto& test : cases) {
const auto bucket = BucketUtils::BucketIndex(test.literal, 100);
ASSERT_TRUE(bucket.has_value());
EXPECT_EQ(*bucket, test.bucket);
}
}
TEST(BucketUtilsTest, CanonicalLittleEndianBlocks) {
constexpr size_t kMaxLength = 33;
constexpr size_t kAlignment = alignof(uint64_t);
struct HashCase {
size_t length;
uint32_t hash;
};
constexpr std::array<HashCase, 14> cases = {{
{.length = 0, .hash = 0x00000000u},
{.length = 1, .hash = 0x514e28b7u},
{.length = 2, .hash = 0x70e1a2c0u},
{.length = 3, .hash = 0x51d4d0d7u},
{.length = 4, .hash = 0xf4c0ec39u},
{.length = 5, .hash = 0xcca4dccbu},
{.length = 6, .hash = 0x901cc302u},
{.length = 7, .hash = 0x8d7e4914u},
{.length = 8, .hash = 0xd161d673u},
{.length = 15, .hash = 0x5bd6952du},
{.length = 16, .hash = 0x191573ddu},
{.length = 17, .hash = 0xcbe58dc6u},
{.length = 32, .hash = 0xcac37638u},
{.length = kMaxLength, .hash = 0x5460867au},
}};
for (const auto& test : cases) {
SCOPED_TRACE(test.length);
for (size_t offset = 0; offset < kAlignment; ++offset) {
SCOPED_TRACE(offset);
alignas(uint64_t) std::array<uint8_t, kMaxLength + kAlignment - 1> input{};
std::iota(input.begin() + offset, input.begin() + offset + test.length, uint8_t{0});
EXPECT_EQ(static_cast<uint32_t>(
BucketUtils::HashBytes({input.data() + offset, test.length})),
test.hash);
}
}
}
TEST(BucketUtilsTest, HashLongMatchesCanonicalBytes) {
constexpr std::array<int64_t, 9> values = {0,
34,
-1,
std::numeric_limits<int32_t>::min(),
std::numeric_limits<int32_t>::max(),
std::numeric_limits<int64_t>::min(),
std::numeric_limits<int64_t>::max(),
0x0123456789abcdefLL,
static_cast<int64_t>(0xfedcba9876543210ULL)};
for (const auto value : values) {
SCOPED_TRACE(value);
std::array<uint8_t, sizeof(value)> bytes{};
for (size_t i = 0; i < bytes.size(); ++i) {
bytes[i] = static_cast<uint8_t>(static_cast<uint64_t>(value) >> (i * 8));
}
EXPECT_EQ(BucketUtils::HashLong(value), BucketUtils::HashBytes(bytes));
}
}
TEST(BucketUtilsTest, BucketTimestampNanosMatchesMicros) {
constexpr int32_t kNumBuckets = 1000;
const auto ts_micros = TemporalTestHelper::CreateTimestamp({.year = 2017,
.month = 11,
.day = 16,
.hour = 22,
.minute = 31,
.second = 8,
.microsecond = 1});
const auto ts_nanos = TemporalTestHelper::CreateTimestampNanos({.year = 2017,
.month = 11,
.day = 16,
.hour = 22,
.minute = 31,
.second = 8,
.nanosecond = 1000});
const auto micros_bucket =
BucketUtils::BucketIndex(Literal::Timestamp(ts_micros), kNumBuckets);
const auto nanos_bucket =
BucketUtils::BucketIndex(Literal::TimestampNs(ts_nanos), kNumBuckets);
ASSERT_TRUE(micros_bucket.has_value());
ASSERT_TRUE(nanos_bucket.has_value());
EXPECT_EQ(micros_bucket.value(), nanos_bucket.value());
const auto ts_tz_micros =
TemporalTestHelper::CreateTimestampTz({.year = 2017,
.month = 11,
.day = 16,
.hour = 14,
.minute = 31,
.second = 8,
.microsecond = 1,
.tz_offset_minutes = -480});
const auto ts_tz_nanos =
TemporalTestHelper::CreateTimestampTzNanos({.year = 2017,
.month = 11,
.day = 16,
.hour = 14,
.minute = 31,
.second = 8,
.nanosecond = 1000,
.tz_offset_minutes = -480});
const auto tz_micros_bucket =
BucketUtils::BucketIndex(Literal::TimestampTz(ts_tz_micros), kNumBuckets);
const auto tz_nanos_bucket =
BucketUtils::BucketIndex(Literal::TimestampTzNs(ts_tz_nanos), kNumBuckets);
ASSERT_TRUE(tz_micros_bucket.has_value());
ASSERT_TRUE(tz_nanos_bucket.has_value());
EXPECT_EQ(tz_micros_bucket.value(), tz_nanos_bucket.value());
const auto pre_epoch_micros_bucket =
BucketUtils::BucketIndex(Literal::Timestamp(-876544), kNumBuckets);
const auto pre_epoch_nanos_bucket =
BucketUtils::BucketIndex(Literal::TimestampNs(-876543211), kNumBuckets);
ASSERT_TRUE(pre_epoch_micros_bucket.has_value());
ASSERT_TRUE(pre_epoch_nanos_bucket.has_value());
EXPECT_EQ(pre_epoch_micros_bucket.value(), pre_epoch_nanos_bucket.value());
const auto pre_epoch_tz_micros_bucket =
BucketUtils::BucketIndex(Literal::TimestampTz(-876544), kNumBuckets);
const auto pre_epoch_tz_nanos_bucket =
BucketUtils::BucketIndex(Literal::TimestampTzNs(-876543211), kNumBuckets);
ASSERT_TRUE(pre_epoch_tz_micros_bucket.has_value());
ASSERT_TRUE(pre_epoch_tz_nanos_bucket.has_value());
EXPECT_EQ(pre_epoch_tz_micros_bucket.value(), pre_epoch_tz_nanos_bucket.value());
}
} // namespace iceberg