use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[cfg(feature = "write-support")]
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use crate::types::RowKey;
use crate::util::cassandra_murmur3::cassandra_murmur3_token;
#[cfg(feature = "write-support")]
pub(crate) static MANAGER_SCAN_FULL_SORTS: AtomicU64 = AtomicU64::new(0);
#[cfg(any(test, feature = "metrics"))]
thread_local! {
static SCAN_KEY_COMPARISONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
struct HeapItem<T> {
cand: Candidate,
val: T,
}
impl<T> PartialEq for HeapItem<T> {
fn eq(&self, other: &Self) -> bool {
self.cand.cmp(&other.cand) == Ordering::Equal
}
}
impl<T> Eq for HeapItem<T> {}
impl<T> PartialOrd for HeapItem<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T> Ord for HeapItem<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.cand.cmp(&other.cand)
}
}
struct Candidate {
token: i64,
key: RowKey,
reader_idx: usize,
}
impl Candidate {
fn cmp(&self, other: &Self) -> Ordering {
#[cfg(any(test, feature = "metrics"))]
SCAN_KEY_COMPARISONS.with(|c| c.set(c.get().wrapping_add(1)));
self.token
.cmp(&other.token)
.then_with(|| self.key.cmp(&other.key))
.then_with(|| self.reader_idx.cmp(&other.reader_idx))
}
}
pub(crate) fn kway_merge_token_order<T>(
per_reader: Vec<Vec<(RowKey, T)>>,
limit: Option<usize>,
) -> Vec<(RowKey, T)> {
let non_empty = per_reader.iter().filter(|s| !s.is_empty()).count();
if non_empty <= 1 {
let mut out: Vec<(RowKey, T)> = per_reader
.into_iter()
.find(|s| !s.is_empty())
.unwrap_or_default();
if let Some(lim) = limit {
out.truncate(lim);
}
return out;
}
let total: usize = per_reader.iter().map(|s| s.len()).sum();
let cap = limit.map_or(total, |l| l.min(total));
if cap == 0 {
return Vec::new();
}
let mut iters: Vec<std::vec::IntoIter<(RowKey, T)>> =
per_reader.into_iter().map(|s| s.into_iter()).collect();
let mut heap: BinaryHeap<std::cmp::Reverse<HeapItem<T>>> =
BinaryHeap::with_capacity(iters.len());
for (idx, it) in iters.iter_mut().enumerate() {
if let Some((key, val)) = it.next() {
let token = cassandra_murmur3_token(key.as_bytes());
heap.push(std::cmp::Reverse(HeapItem {
cand: Candidate {
token,
key,
reader_idx: idx,
},
val,
}));
}
}
let mut out: Vec<(RowKey, T)> = Vec::with_capacity(cap);
while let Some(std::cmp::Reverse(HeapItem { cand, val })) = heap.pop() {
let idx = cand.reader_idx;
out.push((cand.key, val));
if out.len() == cap {
break; }
if let Some((key, val)) = iters[idx].next() {
let token = cassandra_murmur3_token(key.as_bytes());
heap.push(std::cmp::Reverse(HeapItem {
cand: Candidate {
token,
key,
reader_idx: idx,
},
val,
}));
}
}
out
}
#[cfg(feature = "write-support")]
pub(crate) fn sort_by_token_order<E>(
results: &mut Vec<E>,
limit: Option<usize>,
key_of: impl Fn(&E) -> &RowKey,
) {
MANAGER_SCAN_FULL_SORTS.fetch_add(1, AtomicOrdering::Relaxed);
let mut tagged: Vec<(i64, E)> = results
.drain(..)
.map(|e| {
let t = cassandra_murmur3_token(key_of(&e).as_bytes());
(t, e)
})
.collect();
tagged.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| key_of(&a.1).cmp(key_of(&b.1))));
results.extend(tagged.into_iter().map(|(_, e)| e));
if let Some(lim) = limit {
results.truncate(lim);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kway_merge_counting<T>(
per_reader: Vec<Vec<(RowKey, T)>>,
limit: Option<usize>,
) -> (Vec<(RowKey, T)>, u64) {
SCAN_KEY_COMPARISONS.with(|c| c.set(0));
let out = kway_merge_token_order(per_reader, limit);
let count = SCAN_KEY_COMPARISONS.with(|c| c.get());
(out, count)
}
fn uuid_key(b: u8) -> RowKey {
RowKey::new(vec![b; 16])
}
fn keys_in_order<T>(rows: &[(RowKey, T)]) -> Vec<u8> {
let mut out = Vec::new();
for (k, _) in rows {
let first = k.as_bytes()[0];
if out.last() != Some(&first) {
out.push(first);
}
}
out
}
#[test]
fn merge_emits_token_order_across_readers() {
let reader_a = vec![(uuid_key(0x22), 1u32), (uuid_key(0x33), 3u32)];
let reader_b = vec![(uuid_key(0x11), 2u32)];
let merged = kway_merge_token_order(vec![reader_a, reader_b], None);
assert_eq!(keys_in_order(&merged), vec![0x22, 0x11, 0x33]);
}
#[test]
fn merge_preserves_clustering_rows_and_reader_tiebreak() {
let reader0 = vec![(uuid_key(0x11), 0u32), (uuid_key(0x11), 1u32)];
let reader1 = vec![(uuid_key(0x11), 2u32), (uuid_key(0x33), 9u32)];
let merged = kway_merge_token_order(vec![reader0, reader1], None);
let payloads: Vec<u32> = merged.iter().map(|(_, v)| *v).collect();
assert_eq!(payloads, vec![0, 1, 2, 9]);
}
#[test]
fn merge_early_exits_on_limit() {
let reader_a = vec![(uuid_key(0x22), 1u32), (uuid_key(0x33), 3u32)];
let reader_b = vec![(uuid_key(0x11), 2u32)];
let merged = kway_merge_token_order(vec![reader_a, reader_b], Some(2));
assert_eq!(merged.len(), 2);
assert_eq!(keys_in_order(&merged), vec![0x22, 0x11]);
}
#[test]
fn single_reader_is_passthrough_no_comparisons() {
let only = vec![(uuid_key(0x33), 1u32), (uuid_key(0x11), 2u32)];
let (merged, cmps) = kway_merge_counting(vec![only], None);
assert_eq!(merged.len(), 2);
assert_eq!(cmps, 0, "single-stream fast path must not compare keys");
}
#[test]
fn limit_zero_returns_empty_with_multiple_readers() {
let reader_a = vec![(uuid_key(0x22), 1u32), (uuid_key(0x33), 3u32)];
let reader_b = vec![(uuid_key(0x11), 2u32)];
let merged = kway_merge_token_order(vec![reader_a, reader_b], Some(0));
assert!(
merged.is_empty(),
"limit=Some(0) with multiple non-empty readers must return no rows, got {}",
merged.len()
);
}
#[test]
fn merge_comparison_count_is_n_log_k_not_n_log_n() {
let k = 4usize;
let per_stream = 1000usize;
let n = k * per_stream;
let mut per_reader: Vec<Vec<(RowKey, u32)>> = Vec::with_capacity(k);
let mut next: i32 = 0;
for _ in 0..k {
let mut stream: Vec<(RowKey, u32)> = Vec::with_capacity(per_stream);
for _ in 0..per_stream {
let key = RowKey::new(next.to_be_bytes().to_vec());
stream.push((key, next as u32));
next += 1;
}
stream.sort_by_key(|(k, _)| cassandra_murmur3_token(k.as_bytes()));
per_reader.push(stream);
}
let (merged, cmps) = kway_merge_counting(per_reader, None);
assert_eq!(merged.len(), n, "merge must emit every row");
for w in merged.windows(2) {
let ta = cassandra_murmur3_token(w[0].0.as_bytes());
let tb = cassandra_murmur3_token(w[1].0.as_bytes());
assert!(ta <= tb, "merge output must be ascending token order");
}
let log2_k = (usize::BITS - (k - 1).leading_zeros()) as u64; let n_log_k_bound = 4 * (n as u64) * (log2_k + 1);
let n_log_n = (n as f64 * (n as f64).log2()) as u64;
assert!(
cmps <= n_log_k_bound,
"k-way merge did {cmps} comparisons; expected O(n log k) ≤ {n_log_k_bound} \
(n={n}, k={k}); a full O(n log n) re-sort would be ≈ {n_log_n}"
);
assert!(
cmps < n_log_n,
"comparison count {cmps} must be strictly below the O(n log n)≈{n_log_n} \
full-sort cost the k-way merge replaces"
);
}
}