use super::hash::{HashIndex, ProbeParams, probe_hash_index};
pub(super) struct GraceSpec<'a> {
pub(super) build_keys: &'a [&'a str],
pub(super) probe_keys: &'a [&'a str],
pub(super) join_type: &'a str,
pub(super) limit: usize,
pub(super) probe_collection: &'a str,
pub(super) index_collection: &'a str,
pub(super) emit_unmatched_right: bool,
}
pub(super) fn partition_hash<S: AsRef<str>>(doc: &[u8], keys: &[S]) -> u64 {
nodedb_query::partition_hash(doc, keys)
}
pub(super) fn partition_hash_seeded<S: AsRef<str>>(doc: &[u8], keys: &[S], seed: u64) -> u64 {
nodedb_query::partition_hash_seeded(doc, keys, seed)
}
#[allow(dead_code)]
pub(super) fn grace_join_in_memory(
build_docs: Vec<(String, Vec<u8>)>,
probe_docs: Vec<(String, Vec<u8>)>,
partitions: usize,
spec: &GraceSpec,
) -> Vec<Vec<u8>> {
let build_keys = spec.build_keys;
let probe_keys = spec.probe_keys;
let join_type = spec.join_type;
let limit = spec.limit;
let probe_collection = spec.probe_collection;
let index_collection = spec.index_collection;
let emit_unmatched_right = spec.emit_unmatched_right;
if join_type == "cross" || build_keys.is_empty() || probe_keys.is_empty() || partitions <= 1 {
let index = HashIndex::build(&build_docs, build_keys);
return probe_hash_index(&ProbeParams {
probe_docs: &probe_docs,
index: &index,
index_docs: &build_docs,
probe_keys,
join_type,
limit,
probe_collection,
index_collection,
join_filters: &[],
emit_unmatched_right,
});
}
let mut build_part: Vec<Vec<(String, Vec<u8>)>> = vec![vec![]; partitions];
let mut probe_part: Vec<Vec<(String, Vec<u8>)>> = vec![vec![]; partitions];
for row in build_docs {
let idx = (partition_hash(&row.1, build_keys) % partitions as u64) as usize;
build_part[idx].push(row);
}
for row in probe_docs {
let idx = (partition_hash(&row.1, probe_keys) % partitions as u64) as usize;
probe_part[idx].push(row);
}
let mut results: Vec<Vec<u8>> = Vec::new();
for i in 0..partitions {
let index = HashIndex::build(&build_part[i], build_keys);
let mut part_results = probe_hash_index(&ProbeParams {
probe_docs: &probe_part[i],
index: &index,
index_docs: &build_part[i],
probe_keys,
join_type,
limit: usize::MAX,
probe_collection,
index_collection,
join_filters: &[],
emit_unmatched_right,
});
results.append(&mut part_results);
}
results.truncate(limit);
results
}
#[cfg(test)]
mod tests {
use super::*;
type DocSet = Vec<(String, Vec<u8>)>;
fn msgpack_row(fields: &[(&str, serde_json::Value)]) -> Vec<u8> {
let mut map = serde_json::Map::new();
for (k, v) in fields {
map.insert((*k).to_string(), v.clone());
}
nodedb_types::json_to_msgpack(&serde_json::Value::Object(map)).unwrap()
}
fn as_multiset(mut rows: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
rows.sort();
rows
}
fn reference(
build_docs: &[(String, Vec<u8>)],
probe_docs: &[(String, Vec<u8>)],
spec: &GraceSpec,
) -> Vec<Vec<u8>> {
let index = HashIndex::build(build_docs, spec.build_keys);
probe_hash_index(&ProbeParams {
probe_docs,
index: &index,
index_docs: build_docs,
probe_keys: spec.probe_keys,
join_type: spec.join_type,
limit: spec.limit,
probe_collection: spec.probe_collection,
index_collection: spec.index_collection,
join_filters: &[],
emit_unmatched_right: spec.emit_unmatched_right,
})
}
fn single_key_fixtures() -> (DocSet, DocSet) {
let build = vec![
(
"b1".into(),
msgpack_row(&[("k", serde_json::json!(1)), ("rv", serde_json::json!("r1"))]),
),
(
"b2".into(),
msgpack_row(&[
("k", serde_json::json!(1)),
("rv", serde_json::json!("r1dup")),
]),
), (
"b3".into(),
msgpack_row(&[("k", serde_json::json!(2)), ("rv", serde_json::json!("r2"))]),
),
(
"b4".into(),
msgpack_row(&[("k", serde_json::json!(9)), ("rv", serde_json::json!("r9"))]),
), (
"b5".into(),
msgpack_row(&[("rv", serde_json::json!("r-nokey"))]),
), ];
let probe = vec![
(
"p1".into(),
msgpack_row(&[("k", serde_json::json!(1)), ("lv", serde_json::json!("l1"))]),
),
(
"p2".into(),
msgpack_row(&[
("k", serde_json::json!(1)),
("lv", serde_json::json!("l1dup")),
]),
), (
"p3".into(),
msgpack_row(&[("k", serde_json::json!(2)), ("lv", serde_json::json!("l2"))]),
),
(
"p4".into(),
msgpack_row(&[("k", serde_json::json!(7)), ("lv", serde_json::json!("l7"))]),
), (
"p5".into(),
msgpack_row(&[("lv", serde_json::json!("l-nokey"))]),
), ];
(build, probe)
}
const ALL_JOIN_TYPES: [&str; 7] = ["inner", "left", "right", "full", "semi", "anti", "cross"];
#[test]
fn multiset_equivalence_all_join_types_all_partition_counts() {
let (build, probe) = single_key_fixtures();
let build_keys = ["k"];
let probe_keys = ["k"];
for jt in ALL_JOIN_TYPES {
let spec = GraceSpec {
build_keys: &build_keys,
probe_keys: &probe_keys,
join_type: jt,
limit: usize::MAX,
probe_collection: "l",
index_collection: "r",
emit_unmatched_right: true,
};
let want = as_multiset(reference(&build, &probe, &spec));
for p in [1usize, 2, 4, 8] {
let candidate = grace_join_in_memory(build.clone(), probe.clone(), p, &spec);
assert_eq!(
want,
as_multiset(candidate),
"join_type={jt} partitions={p} multiset mismatch"
);
}
}
}
#[test]
fn composite_key_equivalence_inner_and_left() {
let build = vec![
(
"b1".into(),
msgpack_row(&[
("a", serde_json::json!(1)),
("b", serde_json::json!("x")),
("rv", serde_json::json!("r1")),
]),
),
(
"b2".into(),
msgpack_row(&[
("a", serde_json::json!(1)),
("b", serde_json::json!("y")),
("rv", serde_json::json!("r2")),
]),
),
(
"b3".into(),
msgpack_row(&[
("a", serde_json::json!(2)),
("b", serde_json::json!("x")),
("rv", serde_json::json!("r3")),
]),
),
(
"b4".into(),
msgpack_row(&[
("a", serde_json::json!(1)),
("b", serde_json::json!("x")),
("rv", serde_json::json!("r1dup")),
]),
), ];
let probe = vec![
(
"p1".into(),
msgpack_row(&[
("a", serde_json::json!(1)),
("b", serde_json::json!("x")),
("lv", serde_json::json!("l1")),
]),
),
(
"p2".into(),
msgpack_row(&[
("a", serde_json::json!(2)),
("b", serde_json::json!("x")),
("lv", serde_json::json!("l3")),
]),
),
(
"p3".into(),
msgpack_row(&[
("a", serde_json::json!(5)),
("b", serde_json::json!("z")),
("lv", serde_json::json!("nomatch")),
]),
),
];
let build_keys = ["a", "b"];
let probe_keys = ["a", "b"];
for jt in ["inner", "left"] {
let spec = GraceSpec {
build_keys: &build_keys,
probe_keys: &probe_keys,
join_type: jt,
limit: usize::MAX,
probe_collection: "l",
index_collection: "r",
emit_unmatched_right: true,
};
let want = as_multiset(reference(&build, &probe, &spec));
for p in [1usize, 2, 4, 8] {
let candidate = grace_join_in_memory(build.clone(), probe.clone(), p, &spec);
assert_eq!(
want,
as_multiset(candidate),
"composite join_type={jt} partitions={p}"
);
}
}
}
#[test]
fn empty_build_docs_matches_reference() {
let (_, probe) = single_key_fixtures();
let build: Vec<(String, Vec<u8>)> = Vec::new();
let build_keys = ["k"];
let probe_keys = ["k"];
for jt in ALL_JOIN_TYPES {
let spec = GraceSpec {
build_keys: &build_keys,
probe_keys: &probe_keys,
join_type: jt,
limit: usize::MAX,
probe_collection: "l",
index_collection: "r",
emit_unmatched_right: true,
};
let want = as_multiset(reference(&build, &probe, &spec));
for p in [1usize, 2, 4, 8] {
let candidate = grace_join_in_memory(build.clone(), probe.clone(), p, &spec);
assert_eq!(
want,
as_multiset(candidate),
"empty build join_type={jt} p={p}"
);
}
}
}
#[test]
fn empty_probe_docs_matches_reference() {
let (build, _) = single_key_fixtures();
let probe: Vec<(String, Vec<u8>)> = Vec::new();
let build_keys = ["k"];
let probe_keys = ["k"];
for jt in ALL_JOIN_TYPES {
let spec = GraceSpec {
build_keys: &build_keys,
probe_keys: &probe_keys,
join_type: jt,
limit: usize::MAX,
probe_collection: "l",
index_collection: "r",
emit_unmatched_right: true,
};
let want = as_multiset(reference(&build, &probe, &spec));
for p in [1usize, 2, 4, 8] {
let candidate = grace_join_in_memory(build.clone(), probe.clone(), p, &spec);
assert_eq!(
want,
as_multiset(candidate),
"empty probe join_type={jt} p={p}"
);
}
}
}
#[test]
fn limit_truncation_caps_output() {
let (build, probe) = single_key_fixtures();
let build_keys = ["k"];
let probe_keys = ["k"];
let unbounded_spec = GraceSpec {
build_keys: &build_keys,
probe_keys: &probe_keys,
join_type: "inner",
limit: usize::MAX,
probe_collection: "l",
index_collection: "r",
emit_unmatched_right: true,
};
let full = reference(&build, &probe, &unbounded_spec);
assert!(full.len() >= 2, "fixture must produce >= 2 inner rows");
let limit = full.len() - 1;
let limited_spec = GraceSpec {
limit,
..unbounded_spec
};
for p in [1usize, 2, 4, 8] {
let candidate = grace_join_in_memory(build.clone(), probe.clone(), p, &limited_spec);
assert_eq!(candidate.len(), limit, "limit truncation p={p}");
}
}
#[test]
fn partition_hash_is_stable_for_equal_key_bytes() {
let a = msgpack_row(&[("k", serde_json::json!(42)), ("x", serde_json::json!("a"))]);
let b = msgpack_row(&[
("k", serde_json::json!(42)),
("y", serde_json::json!("different")),
]);
let keys = ["k"];
assert_eq!(partition_hash(&a, &keys), partition_hash(&b, &keys));
let m1 = msgpack_row(&[("other", serde_json::json!(1))]);
let m2 = msgpack_row(&[("nope", serde_json::json!(2))]);
assert_eq!(partition_hash(&m1, &keys), partition_hash(&m2, &keys));
}
#[test]
fn partition_hash_delegates_to_seed_zero() {
let keys = ["k"];
for v in 0..32i64 {
let row = msgpack_row(&[("k", serde_json::json!(v))]);
assert_eq!(
partition_hash(&row, &keys),
partition_hash_seeded(&row, &keys, 0),
"partition_hash must equal seed=0 for v={v}"
);
}
}
#[test]
fn partition_hash_seeded_redistributes_distinct_keys() {
let keys = ["k"];
const BUCKETS: u64 = 8;
let dist = |seed: u64| -> Vec<u64> {
(0..64i64)
.map(|v| {
let row = msgpack_row(&[("k", serde_json::json!(v))]);
partition_hash_seeded(&row, &keys, seed) % BUCKETS
})
.collect()
};
let d0 = dist(0);
let d1 = dist(1);
assert_ne!(d0, d1, "seed change must redistribute distinct keys");
let a = msgpack_row(&[("k", serde_json::json!(42)), ("x", serde_json::json!("a"))]);
let b = msgpack_row(&[("k", serde_json::json!(42)), ("y", serde_json::json!("b"))]);
for seed in [0u64, 1, 7, 99] {
assert_eq!(
partition_hash_seeded(&a, &keys, seed),
partition_hash_seeded(&b, &keys, seed),
"equal key bytes must co-locate within seed={seed}"
);
}
}
}