#![forbid(unsafe_code)]
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone)]
pub struct QuotientFilter {
q: u32,
r: u32,
slots: Vec<Option<(u32, u64)>>,
count: usize,
capacity: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct QuotientFilterConfig {
pub q: u32,
pub r: u32,
}
impl QuotientFilterConfig {
#[must_use]
pub fn for_capacity(expected_items: usize, fp_rate: f64) -> Self {
let fp_rate = if fp_rate.is_finite() && fp_rate > 0.0 {
fp_rate.min(1.0 - f64::EPSILON)
} else {
0.01
};
let r = (-fp_rate.log2()).ceil() as u32;
let r = r.clamp(2, 32);
let needed = ((expected_items as f64 / 0.75).ceil()) as u64;
let q = (64 - needed.leading_zeros()).clamp(4, 28);
Self { q, r }
}
}
impl Default for QuotientFilterConfig {
fn default() -> Self {
Self { q: 10, r: 8 } }
}
impl QuotientFilter {
#[must_use]
pub fn new(config: QuotientFilterConfig) -> Self {
let q = config.q.min(28); let r = config.r.clamp(1, 32);
let capacity = 1usize << q;
Self {
q,
r,
slots: vec![None; capacity],
count: 0,
capacity,
}
}
#[must_use]
pub fn with_defaults() -> Self {
Self::new(QuotientFilterConfig::default())
}
#[must_use]
pub fn len(&self) -> usize {
self.count
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.count == 0
}
#[must_use]
pub fn load_factor(&self) -> f64 {
self.count as f64 / self.capacity as f64
}
#[must_use]
pub fn capacity(&self) -> usize {
self.capacity
}
#[must_use]
pub fn theoretical_fp_rate(&self) -> f64 {
let base_rate = 1.0 / (1u64 << self.r) as f64;
1.0 - (1.0 - base_rate).powi(self.count as i32)
}
fn fingerprint<T: Hash>(&self, item: &T) -> (u32, u64) {
let mut hasher = DefaultHasher::new();
item.hash(&mut hasher);
let h = hasher.finish();
let q_mask = (1u32 << self.q) - 1;
let r_mask = (1u64 << self.r) - 1;
let quotient = ((h >> self.r) as u32) & q_mask;
let remainder = h & r_mask;
(quotient, remainder)
}
pub fn insert<T: Hash>(&mut self, item: &T) -> bool {
if self.count >= self.capacity {
return false;
}
let (quotient, remainder) = self.fingerprint(item);
let mut pos = quotient as usize;
for _ in 0..self.capacity {
match self.slots[pos] {
None => {
self.slots[pos] = Some((quotient, remainder));
self.count += 1;
return true;
}
Some((q, r)) if q == quotient && r == remainder => {
return false;
}
_ => {
pos = (pos + 1) % self.capacity;
}
}
}
false }
#[must_use]
pub fn contains<T: Hash>(&self, item: &T) -> bool {
let (quotient, remainder) = self.fingerprint(item);
let mut pos = quotient as usize;
for _ in 0..self.capacity {
match self.slots[pos] {
None => return false, Some((q, r)) if q == quotient && r == remainder => return true,
_ => pos = (pos + 1) % self.capacity,
}
}
false
}
pub fn remove<T: Hash>(&mut self, item: &T) -> bool {
let (quotient, remainder) = self.fingerprint(item);
let mut pos = quotient as usize;
let mut found_pos = None;
for _ in 0..self.capacity {
match self.slots[pos] {
None => break,
Some((q, r)) if q == quotient && r == remainder => {
found_pos = Some(pos);
break;
}
_ => pos = (pos + 1) % self.capacity,
}
}
let mut pos = match found_pos {
Some(p) => p,
None => return false,
};
self.slots[pos] = None;
self.count -= 1;
let mut current = (pos + 1) % self.capacity;
loop {
match self.slots[current] {
None => break, Some((q, _r)) => {
let canonical = q as usize;
let should_shift = if canonical <= pos {
current > pos || current < canonical
} else {
current > pos && current < canonical
};
if !should_shift {
break;
}
self.slots[pos] = self.slots[current];
self.slots[current] = None;
pos = current;
}
}
current = (current + 1) % self.capacity;
}
true
}
pub fn clear(&mut self) {
self.slots.fill(None);
self.count = 0;
}
pub fn merge(&mut self, other: &QuotientFilter) -> usize {
if self.q != other.q || self.r != other.r {
return 0;
}
let mut added = 0;
for slot in &other.slots {
if let &Some((q, r)) = slot {
let mut pos = q as usize;
let mut found = false;
for _ in 0..self.capacity {
match self.slots[pos] {
None => break,
Some((eq, er)) if eq == q && er == r => {
found = true;
break;
}
_ => pos = (pos + 1) % self.capacity,
}
}
if !found {
let mut ipos = q as usize;
for _ in 0..self.capacity {
if self.slots[ipos].is_none() {
self.slots[ipos] = Some((q, r));
self.count += 1;
added += 1;
break;
}
ipos = (ipos + 1) % self.capacity;
}
}
}
}
added
}
}
impl Default for QuotientFilter {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_filter() {
let qf = QuotientFilter::with_defaults();
assert!(qf.is_empty());
assert_eq!(qf.len(), 0);
assert_eq!(qf.capacity(), 1024);
assert!(!qf.contains(&42u64));
}
#[test]
fn insert_and_lookup() {
let mut qf = QuotientFilter::with_defaults();
assert!(qf.insert(&100u64));
assert!(qf.contains(&100u64));
assert_eq!(qf.len(), 1);
}
#[test]
fn duplicate_insert_returns_false() {
let mut qf = QuotientFilter::with_defaults();
assert!(qf.insert(&42u64));
assert!(!qf.insert(&42u64));
assert_eq!(qf.len(), 1);
}
#[test]
fn insert_multiple() {
let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 8, r: 8 });
for i in 0u64..50 {
qf.insert(&i);
}
assert_eq!(qf.len(), 50);
for i in 0u64..50 {
assert!(qf.contains(&i), "element {i} should be found");
}
}
#[test]
fn no_false_negatives() {
let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 10 });
let items: Vec<u64> = (0..200).collect();
for item in &items {
qf.insert(item);
}
for item in &items {
assert!(qf.contains(item), "false negative for {item}");
}
}
#[test]
fn false_positive_rate_bounded() {
let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 12, r: 8 });
for i in 0u64..500 {
qf.insert(&i);
}
let mut false_positives = 0;
for i in 10000u64..20000 {
if qf.contains(&i) {
false_positives += 1;
}
}
let fp_rate = false_positives as f64 / 10000.0;
assert!(
fp_rate < 0.05,
"false positive rate too high: {fp_rate:.4} ({false_positives}/10000)"
);
}
#[test]
fn remove_element() {
let mut qf = QuotientFilter::with_defaults();
qf.insert(&42u64);
assert!(qf.contains(&42u64));
assert!(qf.remove(&42u64));
assert_eq!(qf.len(), 0);
assert!(!qf.contains(&42u64));
}
#[test]
fn remove_nonexistent() {
let mut qf = QuotientFilter::with_defaults();
assert!(!qf.remove(&42u64));
}
#[test]
fn remove_preserves_others() {
let mut qf = QuotientFilter::with_defaults();
for i in 0u64..20 {
qf.insert(&i);
}
for i in (0u64..20).step_by(2) {
qf.remove(&i);
}
for i in (1u64..20).step_by(2) {
assert!(qf.contains(&i), "odd {i} should survive removal");
}
for i in (0u64..20).step_by(2) {
assert!(!qf.contains(&i), "even {i} should be removed");
}
}
#[test]
fn clear_filter() {
let mut qf = QuotientFilter::with_defaults();
for i in 0u64..100 {
qf.insert(&i);
}
assert_eq!(qf.len(), 100);
qf.clear();
assert!(qf.is_empty());
assert_eq!(qf.len(), 0);
for i in 0u64..100 {
assert!(!qf.contains(&i));
}
}
#[test]
fn load_factor() {
let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 4, r: 4 }); assert!((qf.load_factor() - 0.0).abs() < f64::EPSILON);
for i in 0u64..8 {
qf.insert(&i);
}
assert!((qf.load_factor() - 0.5).abs() < 0.01);
}
#[test]
fn config_for_capacity() {
let config = QuotientFilterConfig::for_capacity(10000, 0.01);
assert!(config.r >= 7, "r should be at least 7 for 1% FP rate");
assert!(
(1usize << config.q) >= 10000,
"capacity should exceed expected items"
);
}
#[test]
fn config_for_capacity_sanitizes_invalid_fp_rates() {
let zero = QuotientFilterConfig::for_capacity(128, 0.0);
let nan = QuotientFilterConfig::for_capacity(128, f64::NAN);
assert!(zero.r >= 7);
assert!(nan.r >= 7);
assert!((1usize << zero.q) >= 128);
assert!((1usize << nan.q) >= 128);
}
#[test]
fn string_keys() {
let mut qf = QuotientFilter::with_defaults();
qf.insert(&"hello");
qf.insert(&"world");
assert!(qf.contains(&"hello"));
assert!(qf.contains(&"world"));
assert!(!qf.contains(&"foo"));
}
#[test]
fn row_id_tracking() {
let mut dirty = QuotientFilter::new(QuotientFilterConfig { q: 12, r: 8 });
let dirty_rows = [5u32, 42, 100, 255, 1000];
for &row in &dirty_rows {
dirty.insert(&row);
}
for row in 0u32..2000 {
if dirty_rows.contains(&row) {
assert!(dirty.contains(&row), "dirty row {row} not found");
}
}
for &row in &dirty_rows {
dirty.remove(&row);
}
assert!(dirty.is_empty());
}
#[test]
fn merge_filters() {
let config = QuotientFilterConfig { q: 8, r: 8 };
let mut qf1 = QuotientFilter::new(config);
let mut qf2 = QuotientFilter::new(config);
for i in 0u64..10 {
qf1.insert(&i);
}
for i in 5u64..15 {
qf2.insert(&i);
}
let added = qf1.merge(&qf2);
assert!(added > 0, "merge should add elements");
for i in 0u64..15 {
assert!(qf1.contains(&i), "merged filter should contain {i}");
}
}
#[test]
fn merge_mismatched_config_is_noop() {
let mut qf1 = QuotientFilter::new(QuotientFilterConfig { q: 8, r: 8 });
let qf2 = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 8 });
qf1.insert(&1u64);
let added = qf1.merge(&qf2);
assert_eq!(added, 0, "mismatched configs should not merge");
}
#[test]
fn theoretical_fp_rate_increases_with_load() {
let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 8 });
let rate_empty = qf.theoretical_fp_rate();
for i in 0u64..100 {
qf.insert(&i);
}
let rate_loaded = qf.theoretical_fp_rate();
assert!(
rate_empty < rate_loaded,
"FP rate should increase with load"
);
}
#[test]
fn space_comparison_vs_bitset() {
let dirty_config = QuotientFilterConfig::for_capacity(1000, 0.01);
let dirty_qf_bits = (dirty_config.r as usize + 3) * (1usize << dirty_config.q);
let bitset_bits = 1_000_000usize;
assert!(
dirty_qf_bits < bitset_bits,
"QF ({dirty_qf_bits} bits) should be smaller than bitset ({bitset_bits} bits) for sparse dirty sets"
);
}
#[test]
fn default_config() {
let qf = QuotientFilter::default();
assert_eq!(qf.capacity(), 1024);
assert!(qf.is_empty());
}
#[test]
fn insert_after_remove_reuses_slot() {
let mut qf = QuotientFilter::with_defaults();
qf.insert(&1u64);
qf.remove(&1u64);
assert!(qf.insert(&1u64));
assert!(qf.contains(&1u64));
assert_eq!(qf.len(), 1);
}
#[test]
fn heavy_load() {
let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 20 }); let target = 500;
let mut inserted = 0;
for i in 0u64..target as u64 {
if qf.insert(&i) {
inserted += 1;
}
}
assert_eq!(inserted, target);
for i in 0u64..target as u64 {
assert!(qf.contains(&i), "element {i} missing at high load");
}
}
}