use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
#[inline]
fn canonical_siphash(s: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct MemtableKey {
pub series_key_hash: u64,
pub timestamp: i64,
series_canonical_hash2: u64,
pub series_canonical: Arc<str>,
}
impl Hash for MemtableKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.series_key_hash.hash(state);
self.timestamp.hash(state);
self.series_canonical.hash(state);
}
}
impl MemtableKey {
#[inline]
#[must_use]
pub fn new(series_key_hash: u64, timestamp: i64, series_canonical: Arc<str>) -> Self {
let series_canonical_hash2 = canonical_siphash(&series_canonical);
Self {
series_key_hash,
timestamp,
series_canonical_hash2,
series_canonical,
}
}
#[inline]
#[must_use]
pub fn lower_bound(series_key_hash: u64, timestamp: i64) -> Self {
Self {
series_key_hash,
timestamp,
series_canonical_hash2: 0,
series_canonical: Arc::from(""),
}
}
#[inline]
#[must_use]
pub fn upper_bound(series_key_hash: u64, timestamp: i64) -> Self {
Self {
series_key_hash,
timestamp,
series_canonical_hash2: u64::MAX,
series_canonical: Arc::from("\u{10FFFF}"),
}
}
}
impl Ord for MemtableKey {
fn cmp(&self, other: &Self) -> Ordering {
self.series_key_hash
.cmp(&other.series_key_hash)
.then(self.timestamp.cmp(&other.timestamp))
.then(self.series_canonical_hash2.cmp(&other.series_canonical_hash2))
.then_with(|| self.series_canonical.cmp(&other.series_canonical))
}
}
impl PartialOrd for MemtableKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone)]
pub struct MemtableEntry {
pub measurement: Arc<str>,
pub tags: Arc<[(Arc<str>, Arc<str>)]>,
pub fields: Box<[(Arc<str>, chronix_core::FieldValue)]>,
}
pub const NODE_OVERHEAD: usize = 12;
impl MemtableEntry {
#[must_use]
pub fn estimated_size(&self) -> usize {
let mut size = std::mem::size_of::<Self>();
size += self.fields.len() * std::mem::size_of::<(Arc<str>, chronix_core::FieldValue)>();
for (_, v) in &self.fields {
if let chronix_core::FieldValue::String(s) = v {
size += s.len();
}
}
size
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_ordering_by_hash_first() {
let k1 = MemtableKey::new(1, 100, Arc::from("a"));
let k2 = MemtableKey::new(2, 50, Arc::from("a"));
assert!(k1 < k2, "lower hash should sort first");
}
#[test]
fn key_ordering_by_timestamp_second() {
let k1 = MemtableKey::new(1, 100, Arc::from("a"));
let k2 = MemtableKey::new(1, 200, Arc::from("a"));
assert!(k1 < k2, "same hash → lower timestamp first");
}
#[test]
fn key_ordering_by_canonical_third() {
let k1 = MemtableKey::new(1, 100, Arc::from("cpu\0host=a"));
let k2 = MemtableKey::new(1, 100, Arc::from("cpu\0host=b"));
assert_ne!(k1, k2, "different canonical forms are distinct keys");
}
#[test]
fn key_equality() {
let k1 = MemtableKey::new(42, 999, Arc::from("cpu"));
let k2 = MemtableKey::new(42, 999, Arc::from("cpu"));
assert_eq!(k1, k2);
}
#[test]
fn hash_collision_does_not_merge_keys() {
let k1 = MemtableKey::new(42, 1000, Arc::from("series_a\0tag=1"));
let k2 = MemtableKey::new(42, 1000, Arc::from("series_b\0tag=2"));
assert_ne!(
k1, k2,
"collision-proof: different canonical forms are different keys"
);
}
#[test]
fn entry_estimated_size() {
let entry = MemtableEntry {
measurement: Arc::from("cpu"),
tags: vec![(Arc::from("host"), Arc::from("a"))].into(),
fields: vec![(Arc::from("value"), chronix_core::FieldValue::F64(42.0))].into(),
};
let size = entry.estimated_size();
assert!(size >= std::mem::size_of::<MemtableEntry>() + 16);
let with_string = MemtableEntry {
fields: vec![(
Arc::from("s"),
chronix_core::FieldValue::String("hello".into()),
)]
.into(),
..entry
};
assert!(with_string.estimated_size() >= size + 5);
}
}