fn java_string_hash(key: &str) -> i32 {
key.encode_utf16()
.fold(0i32, |h, unit| h.wrapping_mul(31).wrapping_add(unit as i32))
}
pub(crate) fn file_group_index(key: &str, num_file_groups: usize) -> usize {
if num_file_groups <= 1 {
return 0;
}
let hash = java_string_hash(key);
let folded = hash.wrapping_abs() % (num_file_groups as i32);
folded.wrapping_abs() as usize
}
pub(crate) fn slices_for_keys<'a>(
slices: &'a [crate::file_group::file_slice::FileSlice],
keys: &[&str],
) -> Vec<&'a crate::file_group::file_slice::FileSlice> {
if keys.is_empty() || slices.len() <= 1 {
return slices.iter().collect();
}
let mut ordered: Vec<&crate::file_group::file_slice::FileSlice> = slices.iter().collect();
ordered.sort_by(|a, b| a.file_id().cmp(b.file_id()));
let mut wanted: Vec<usize> = keys
.iter()
.map(|k| file_group_index(k, ordered.len()))
.collect();
wanted.sort_unstable();
wanted.dedup();
wanted.into_iter().map(|i| ordered[i]).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_hash_matches_java_string_hashcode() {
assert_eq!(java_string_hash(""), 0);
assert_eq!(java_string_hash("a"), 97);
assert_eq!(java_string_hash("hello"), 99_162_322);
assert_eq!(java_string_hash("polygenelubricants"), i32::MIN);
}
#[test]
fn a_non_bmp_character_hashes_over_its_surrogates() {
assert_eq!(java_string_hash("\u{1F600}"), 1_772_899);
assert_ne!(java_string_hash("\u{1F600}"), 0x1F600);
assert_eq!(java_string_hash("\u{1F600}x"), 54_959_989);
}
#[test]
fn keys_route_where_java_routes_them() {
for (key, n, expected) in [
("a", 10, 7),
("a", 3, 1),
("hello", 10, 2),
("__all_partitions__", 10, 1),
("__all_partitions__", 3, 2),
("city=chennai", 10, 0),
("\u{1F600}", 10, 9),
("polygenelubricants", 10, 8),
("polygenelubricants", 3, 2),
] {
assert_eq!(
file_group_index(key, n),
expected,
"key {key:?} over {n} file groups"
);
}
}
#[test]
fn the_index_is_always_in_range() {
assert_eq!(file_group_index("anything", 1), 0);
assert_eq!(file_group_index("anything", 0), 0);
for n in 1..=64usize {
for key in ["", "a", "polygenelubricants", "city=sao_paulo", "\u{1F600}"] {
assert!(
file_group_index(key, n) < n,
"key {key:?} over {n} file groups landed out of range"
);
}
}
}
fn shards(n: usize) -> Vec<crate::file_group::file_slice::FileSlice> {
(0..n)
.map(|i| {
crate::file_group::file_slice::FileSlice::new_log_only(
format!("record-index-{i:04}-0"),
"20250101000000000".to_string(),
"record_index".to_string(),
)
})
.collect()
}
#[test]
fn a_key_lookup_opens_only_the_shards_its_keys_route_to() {
let slices = shards(10);
let key = "some-record-key";
let expected = file_group_index(key, 10);
let picked = slices_for_keys(&slices, &[key]);
assert_eq!(picked.len(), 1, "one key routes to exactly one shard");
assert_eq!(
picked[0].file_id(),
slices[expected].file_id(),
"and it must be the shard the hash names, not merely some shard"
);
}
#[test]
fn distinct_shards_are_opened_once_each() {
let slices = shards(10);
let mut a = None;
let mut b = None;
for i in 0..500 {
let k = format!("k{i}");
match file_group_index(&k, 10) {
idx if a.is_none() => a = Some((k, idx)),
idx if b.is_none() && Some(idx) != a.as_ref().map(|(_, i)| *i) => {
b = Some((k, idx))
}
_ => {}
}
if a.is_some() && b.is_some() {
break;
}
}
let (ka, _) = a.expect("a key");
let (kb, _) = b.expect("a key on a different shard");
let picked = slices_for_keys(&slices, &[ka.as_str(), kb.as_str()]);
assert_eq!(picked.len(), 2, "two shards, two slices opened");
let repeated = slices_for_keys(&slices, &[ka.as_str(), ka.as_str()]);
assert_eq!(
repeated.len(),
1,
"a repeated key must not open its shard twice"
);
}
#[test]
fn an_empty_key_set_opens_every_slice() {
let slices = shards(10);
assert_eq!(
slices_for_keys(&slices, &[]).len(),
10,
"a scan must open every shard"
);
}
#[test]
fn selection_does_not_depend_on_listing_order() {
let mut forward = shards(10);
let key = "some-record-key";
let from_forward = slices_for_keys(&forward, &[key])[0].file_id().to_string();
forward.reverse();
let from_reversed = slices_for_keys(&forward, &[key])[0].file_id().to_string();
assert_eq!(
from_forward, from_reversed,
"the shard a key selects must not change with listing order"
);
}
}