Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/iceberg/location_provider.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ std::string DirsFromHash(int32_t hash) {
}

std::string ComputeHash(std::string_view file_name) {
int32_t hash_value = 0;
MurmurHash3_x86_32(file_name.data(), file_name.size(), 0, &hash_value);
return DirsFromHash(hash_value);
return DirsFromHash(
static_cast<int32_t>(MurmurHash3_x86_32(file_name.data(), file_name.size(), 0)));
Comment thread
manuzhang marked this conversation as resolved.
}

} // namespace
Expand Down
85 changes: 85 additions & 0 deletions src/iceberg/test/bucket_util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,35 @@

#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");
Expand Down Expand Up @@ -108,6 +120,79 @@ TEST(BucketUtilsTest, HashHelper) {
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,
Expand Down
4 changes: 4 additions & 0 deletions src/iceberg/test/location_provider_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ TEST_F(LocationProviderTest, HashInjection) {

auto location_d = provider->NewDataLocation("d");
EXPECT_THAT(location_d, testing::EndsWith("/data/1001/0001/0100/01110011/d"));

auto location_file = provider->NewDataLocation("test.parquet");
EXPECT_THAT(location_file,
testing::EndsWith("/data/0110/1010/0011/11101000/test.parquet"));
}

} // namespace iceberg
16 changes: 4 additions & 12 deletions src/iceberg/util/bucket_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,15 @@

#include "iceberg/exception.h"
#include "iceberg/expression/literal.h"
#include "iceberg/util/endian.h"
#include "iceberg/util/murmurhash3_internal.h"
#include "iceberg/util/temporal_util.h"

namespace iceberg {

namespace {

/// MurmurHash3_x86_32 takes a signed 32-bit length. Iceberg's bucket transform is
/// defined on byte sequences of at most 2^31 - 1 bytes (Java arrays are int-indexed),
/// and longer inputs would wrap the length negative and make the hash loop read far
/// outside the buffer, so they are rejected instead.
// Keep bucket inputs compatible with Java's signed 32-bit array lengths.
// The underlying MurmurHash3 implementation accepts size_t lengths.
constexpr size_t kMaxHashBytes = static_cast<size_t>(std::numeric_limits<int32_t>::max());

/// \brief Returns the number of bytes `HashLiteral` will hash for `literal`.
Expand Down Expand Up @@ -141,16 +138,11 @@ int32_t BucketUtils::HashBytes(std::span<const uint8_t> bytes) {
"BucketUtils::HashBytes: input length {} exceeds the maximum supported "
"length {}",
bytes.size(), kMaxHashBytes);
int32_t hash_value = 0;
MurmurHash3_x86_32(bytes.data(), static_cast<int>(bytes.size()), 0, &hash_value);
return hash_value;
return static_cast<int32_t>(MurmurHash3_x86_32(bytes.data(), bytes.size(), 0));
}

int32_t BucketUtils::HashLong(int64_t value) {
int32_t hash_value = 0;
value = ToLittleEndian(value);
MurmurHash3_x86_32(&value, sizeof(int64_t), 0, &hash_value);
return hash_value;
return static_cast<int32_t>(MurmurHash3_x86_32_Long(static_cast<uint64_t>(value), 0));
}

#define DISPATCH_HASH_LITERAL(TYPE_ID) \
Expand Down
10 changes: 5 additions & 5 deletions src/iceberg/util/bucket_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ class ICEBERG_EXPORT BucketUtils {
/// \brief Hash a byte array using MurmurHash3 and return a 32-bit hash value.
/// \param bytes The input byte array to hash.
/// \return A 32-bit hash value.
/// \throws IcebergError if `bytes.size()` exceeds `INT32_MAX`. MurmurHash3 takes a
/// signed 32-bit length, and the bucket transform is only defined for byte
/// sequences up to that size, so longer inputs are rejected rather than
/// truncated. Java cannot produce such a value because its arrays are
/// int-indexed, so nothing iceberg-java could hash is rejected here.
/// \throws IcebergError if `bytes.size()` exceeds `INT32_MAX`. This limit keeps
/// bucket inputs compatible with Java's int-indexed arrays; the underlying
/// MurmurHash3 implementation accepts `size_t` lengths.
static int32_t HashBytes(std::span<const uint8_t> bytes);

/// \brief Compute the bucket index for a given literal and number of buckets.
/// \param literal The input literal to hash.
/// \param num_buckets The number of buckets to hash into.
/// \return (murmur3_x86_32_hash(literal) & Integer.MAX_VALUE) % num_buckets
/// \retval InvalidArgument if num_buckets is not positive or the literal's byte
/// length exceeds `INT32_MAX`; see HashBytes.
static Result<int32_t> BucketIndex(const Literal& literal, int32_t num_buckets);
};

Expand Down
Loading
Loading