Docs
What it’s for
hashes is the three checksums and hashes the table formats ask for, in plain Mojo: CRC-32 for Parquet page CRCs and gzip, MurmurHash3 x86-32 for Iceberg’s bucket[N] partition transform, and XXH64 for Parquet’s split-block bloom filters. iceberg and parquet use it. It has no dependencies and calls no C library, so the same input gives the same hash on every platform and toolchain.
Install
A new project needs Modular’s channel, pixi’s build preview and the compiler before the tin:
pixi init app -c https://conda.modular.com/max -c conda-forge && cd app
sed -i.bak 's/^\[workspace\]/[workspace]\npreview = ["pixi-build"]/' pixi.toml
pixi add "mojo-compiler==1.2.0"
pixi shelf add hashes-mojo # no other tins
Quick start
A CRC-32, the Iceberg spec’s own bucket[16] example, and XXH64 fed in chunks:
from hashes import Xxh64, crc32, iceberg_bucket, iceberg_hash_int, xxh64
def main() raises:
var data = "123456789".as_bytes()
print(hex(crc32(data))) # 0xcbf43926
# Iceberg bucket[16] of the int 34, the spec's worked example
var h = iceberg_hash_int(34) # 2017239379
print(iceberg_bucket(h.cast[DType.uint32](), 16)) # 3
# XXH64 in one call, or fed in chunks
var x = Xxh64()
x.update("1234".as_bytes())
x.update("56789".as_bytes())
print(x.finish() == xxh64(data)) # True
API
Everything is exported from hashes. Nothing raises.
CRC-32 (IEEE 802.3, the zlib and gzip one):
crc32(data: Span[UInt8, _], seed: UInt32 = 0) -> UInt32: the CRC ofdata. Pass a previous result asseedto chain:crc32(b, seed=crc32(a))equals the CRC ofafollowed byb.Crc32(seed: UInt32 = 0): the incremental form, withupdate(mut self, data: Span[UInt8, _])andfinish(self) -> UInt32.
MurmurHash3 and the Iceberg bucket transform:
murmur3_x86_32(data: Span[UInt8, _], seed: UInt32 = 0) -> UInt32: MurmurHash3 x86-32, unsigned, asmmh3.hash(..., signed=False)gives it.iceberg_hash_int(v: Int32) -> Int32andiceberg_hash_long(v: Int64) -> Int32: an int is promoted to a long, then hashed as 8 little-endian bytes. Dates, times and timestamps go through these too, as their day or microsecond counts.iceberg_hash_string(s: StringSlice) -> Int32andiceberg_hash_bytes(data: Span[UInt8, _]) -> Int32: the raw UTF-8 or raw bytes.iceberg_hash_uuid(hi: UInt64, lo: UInt64) -> Int32: the 16 bytes, big-endian.iceberg_hash_decimal(unscaled: Int64) -> Int32: the unscaled value’s minimal two’s-complement big-endian bytes, as Java’sBigInteger.toByteArray()writes them.iceberg_bucket(hash: UInt32, n: Int) -> Int:(hash & 0x7FFFFFFF) % n.
XXH64:
xxh64(data: Span[UInt8, _], seed: UInt64 = 0) -> UInt64: XXH64 per its spec.Xxh64(seed: UInt64 = 0): the incremental form, withupdate(mut self, data: Span[UInt8, _])andfinish(self) -> UInt64. Any chunking gives the one-shot result.
Limits
- The
iceberg_hash_*functions return the spec’s signedInt32, whileiceberg_buckettakes theUInt32bits: cast with.cast[DType.uint32]()in between, as the quick start does. - Only MurmurHash3’s x86-32 variant is here, and only the decimal serialization that fits an
Int64unscaled value. - The implementations are scalar and single-core, at roughly 1.3 to 1.7 GB/s each on an M4.
Crc32builds its lookup tables per instance, so you pay that for everycrc32call; reuse oneCrc32for many small inputs.
Tests
pixi run -e stable test (Mojo 1.2.0) and pixi run test (nightly) run tests/test_hashes.mojo. Its vectors were taken from Python’s zlib.crc32, mmh3 and xxhash and from the Iceberg spec’s Appendix B, and are baked in as constants. CI runs the tests and mojolint on both toolchains, and pixi run -e bench bench produces the throughput numbers.
The full reference is the README on GitHub.