1#![forbid(unsafe_code)]
2
3use std::collections::hash_map::DefaultHasher;
43use std::hash::{Hash, Hasher};
44
45#[derive(Debug, Clone)]
47pub struct QuotientFilter {
48 q: u32,
50 r: u32,
52 slots: Vec<Option<(u32, u64)>>,
54 count: usize,
56 capacity: usize,
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct QuotientFilterConfig {
63 pub q: u32,
65 pub r: u32,
67}
68
69impl QuotientFilterConfig {
70 #[must_use]
75 pub fn for_capacity(expected_items: usize, fp_rate: f64) -> Self {
76 let fp_rate = if fp_rate.is_finite() && fp_rate > 0.0 {
77 fp_rate.min(1.0 - f64::EPSILON)
78 } else {
79 0.01
80 };
81
82 let r = (-fp_rate.log2()).ceil() as u32;
84 let r = r.clamp(2, 32);
85
86 let needed = ((expected_items as f64 / 0.75).ceil()) as u64;
89 let q = (64 - needed.leading_zeros()).clamp(4, 28);
90
91 Self { q, r }
92 }
93}
94
95impl Default for QuotientFilterConfig {
96 fn default() -> Self {
97 Self { q: 10, r: 8 } }
99}
100
101impl QuotientFilter {
102 #[must_use]
104 pub fn new(config: QuotientFilterConfig) -> Self {
105 let q = config.q.min(28); let r = config.r.clamp(1, 32);
107 let capacity = 1usize << q;
108
109 Self {
110 q,
111 r,
112 slots: vec![None; capacity],
113 count: 0,
114 capacity,
115 }
116 }
117
118 #[must_use]
120 pub fn with_defaults() -> Self {
121 Self::new(QuotientFilterConfig::default())
122 }
123
124 #[must_use]
126 pub fn len(&self) -> usize {
127 self.count
128 }
129
130 #[must_use]
132 pub fn is_empty(&self) -> bool {
133 self.count == 0
134 }
135
136 #[must_use]
138 pub fn load_factor(&self) -> f64 {
139 self.count as f64 / self.capacity as f64
140 }
141
142 #[must_use]
144 pub fn capacity(&self) -> usize {
145 self.capacity
146 }
147
148 #[must_use]
150 pub fn theoretical_fp_rate(&self) -> f64 {
151 let base_rate = 1.0 / (1u64 << self.r) as f64;
153 1.0 - (1.0 - base_rate).powi(self.count as i32)
154 }
155
156 fn fingerprint<T: Hash>(&self, item: &T) -> (u32, u64) {
158 let mut hasher = DefaultHasher::new();
159 item.hash(&mut hasher);
160 let h = hasher.finish();
161
162 let q_mask = (1u32 << self.q) - 1;
163 let r_mask = (1u64 << self.r) - 1;
164
165 let quotient = ((h >> self.r) as u32) & q_mask;
166 let remainder = h & r_mask;
167 (quotient, remainder)
168 }
169
170 pub fn insert<T: Hash>(&mut self, item: &T) -> bool {
172 if self.count >= self.capacity {
173 return false;
174 }
175
176 let (quotient, remainder) = self.fingerprint(item);
177
178 let mut pos = quotient as usize;
180 for _ in 0..self.capacity {
181 match self.slots[pos] {
182 None => {
183 self.slots[pos] = Some((quotient, remainder));
185 self.count += 1;
186 return true;
187 }
188 Some((q, r)) if q == quotient && r == remainder => {
189 return false;
191 }
192 _ => {
193 pos = (pos + 1) % self.capacity;
195 }
196 }
197 }
198
199 false }
201
202 #[must_use]
208 pub fn contains<T: Hash>(&self, item: &T) -> bool {
209 let (quotient, remainder) = self.fingerprint(item);
210
211 let mut pos = quotient as usize;
212 for _ in 0..self.capacity {
213 match self.slots[pos] {
214 None => return false, Some((q, r)) if q == quotient && r == remainder => return true,
216 _ => pos = (pos + 1) % self.capacity,
217 }
218 }
219
220 false
221 }
222
223 pub fn remove<T: Hash>(&mut self, item: &T) -> bool {
227 let (quotient, remainder) = self.fingerprint(item);
228
229 let mut pos = quotient as usize;
231 let mut found_pos = None;
232 for _ in 0..self.capacity {
233 match self.slots[pos] {
234 None => break,
235 Some((q, r)) if q == quotient && r == remainder => {
236 found_pos = Some(pos);
237 break;
238 }
239 _ => pos = (pos + 1) % self.capacity,
240 }
241 }
242
243 let mut pos = match found_pos {
244 Some(p) => p,
245 None => return false,
246 };
247
248 self.slots[pos] = None;
251 self.count -= 1;
252
253 let mut current = (pos + 1) % self.capacity;
254 loop {
255 match self.slots[current] {
256 None => break, Some((q, _r)) => {
258 let canonical = q as usize;
259 let canonical_in_gap_interval = if pos <= current {
271 canonical > pos && canonical <= current
272 } else {
273 canonical > pos || canonical <= current
274 };
275
276 if !canonical_in_gap_interval {
277 self.slots[pos] = self.slots[current];
279 self.slots[current] = None;
280 pos = current;
281 }
282 }
283 }
284 current = (current + 1) % self.capacity;
285 }
286
287 true
288 }
289
290 pub fn clear(&mut self) {
292 self.slots.fill(None);
293 self.count = 0;
294 }
295
296 pub fn merge(&mut self, other: &QuotientFilter) -> usize {
301 if self.q != other.q || self.r != other.r {
302 return 0;
303 }
304
305 let mut added = 0;
306 for slot in &other.slots {
307 if let &Some((q, r)) = slot {
308 let mut pos = q as usize;
310 let mut found = false;
311 for _ in 0..self.capacity {
312 match self.slots[pos] {
313 None => break,
314 Some((eq, er)) if eq == q && er == r => {
315 found = true;
316 break;
317 }
318 _ => pos = (pos + 1) % self.capacity,
319 }
320 }
321
322 if !found {
323 let mut ipos = q as usize;
325 for _ in 0..self.capacity {
326 if self.slots[ipos].is_none() {
327 self.slots[ipos] = Some((q, r));
328 self.count += 1;
329 added += 1;
330 break;
331 }
332 ipos = (ipos + 1) % self.capacity;
333 }
334 }
335 }
336 }
337 added
338 }
339}
340
341impl Default for QuotientFilter {
342 fn default() -> Self {
343 Self::with_defaults()
344 }
345}
346
347#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn empty_filter() {
357 let qf = QuotientFilter::with_defaults();
358 assert!(qf.is_empty());
359 assert_eq!(qf.len(), 0);
360 assert_eq!(qf.capacity(), 1024);
361 assert!(!qf.contains(&42u64));
362 }
363
364 #[test]
365 fn remove_never_creates_false_negatives_in_clusters() {
366 for base in (0u64..2000).step_by(97) {
373 let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 4, r: 16 });
374 let items: Vec<u64> = (base..base + 12).collect();
375 for item in &items {
376 qf.insert(item);
377 }
378 for item in items.iter().step_by(2) {
380 qf.remove(item);
381 }
382 for item in items.iter().skip(1).step_by(2) {
383 assert!(
384 qf.contains(item),
385 "false negative for {item} after cluster deletions (base {base})"
386 );
387 }
388 }
389 }
390
391 #[test]
392 fn insert_and_lookup() {
393 let mut qf = QuotientFilter::with_defaults();
394 assert!(qf.insert(&100u64));
395 assert!(qf.contains(&100u64));
396 assert_eq!(qf.len(), 1);
397 }
398
399 #[test]
400 fn duplicate_insert_returns_false() {
401 let mut qf = QuotientFilter::with_defaults();
402 assert!(qf.insert(&42u64));
403 assert!(!qf.insert(&42u64));
404 assert_eq!(qf.len(), 1);
405 }
406
407 #[test]
408 fn insert_multiple() {
409 let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 8, r: 8 });
410 for i in 0u64..50 {
411 qf.insert(&i);
412 }
413 assert_eq!(qf.len(), 50);
414
415 for i in 0u64..50 {
417 assert!(qf.contains(&i), "element {i} should be found");
418 }
419 }
420
421 #[test]
422 fn no_false_negatives() {
423 let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 10 });
424 let items: Vec<u64> = (0..200).collect();
425
426 for item in &items {
427 qf.insert(item);
428 }
429
430 for item in &items {
432 assert!(qf.contains(item), "false negative for {item}");
433 }
434 }
435
436 #[test]
437 fn false_positive_rate_bounded() {
438 let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 12, r: 8 });
439
440 for i in 0u64..500 {
442 qf.insert(&i);
443 }
444
445 let mut false_positives = 0;
447 for i in 10000u64..20000 {
448 if qf.contains(&i) {
449 false_positives += 1;
450 }
451 }
452
453 let fp_rate = false_positives as f64 / 10000.0;
454 assert!(
456 fp_rate < 0.05,
457 "false positive rate too high: {fp_rate:.4} ({false_positives}/10000)"
458 );
459 }
460
461 #[test]
462 fn remove_element() {
463 let mut qf = QuotientFilter::with_defaults();
464 qf.insert(&42u64);
465 assert!(qf.contains(&42u64));
466
467 assert!(qf.remove(&42u64));
468 assert_eq!(qf.len(), 0);
469 assert!(!qf.contains(&42u64));
470 }
471
472 #[test]
473 fn remove_nonexistent() {
474 let mut qf = QuotientFilter::with_defaults();
475 assert!(!qf.remove(&42u64));
476 }
477
478 #[test]
479 fn remove_preserves_others() {
480 let mut qf = QuotientFilter::with_defaults();
481 for i in 0u64..20 {
482 qf.insert(&i);
483 }
484
485 for i in (0u64..20).step_by(2) {
487 qf.remove(&i);
488 }
489
490 for i in (1u64..20).step_by(2) {
492 assert!(qf.contains(&i), "odd {i} should survive removal");
493 }
494 for i in (0u64..20).step_by(2) {
496 assert!(!qf.contains(&i), "even {i} should be removed");
497 }
498 }
499
500 #[test]
501 fn clear_filter() {
502 let mut qf = QuotientFilter::with_defaults();
503 for i in 0u64..100 {
504 qf.insert(&i);
505 }
506 assert_eq!(qf.len(), 100);
507
508 qf.clear();
509 assert!(qf.is_empty());
510 assert_eq!(qf.len(), 0);
511
512 for i in 0u64..100 {
513 assert!(!qf.contains(&i));
514 }
515 }
516
517 #[test]
518 fn load_factor() {
519 let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 4, r: 4 }); assert!((qf.load_factor() - 0.0).abs() < f64::EPSILON);
521
522 for i in 0u64..8 {
523 qf.insert(&i);
524 }
525 assert!((qf.load_factor() - 0.5).abs() < 0.01);
526 }
527
528 #[test]
529 fn config_for_capacity() {
530 let config = QuotientFilterConfig::for_capacity(10000, 0.01);
531 assert!(config.r >= 7, "r should be at least 7 for 1% FP rate");
532 assert!(
533 (1usize << config.q) >= 10000,
534 "capacity should exceed expected items"
535 );
536 }
537
538 #[test]
539 fn config_for_capacity_sanitizes_invalid_fp_rates() {
540 let zero = QuotientFilterConfig::for_capacity(128, 0.0);
541 let nan = QuotientFilterConfig::for_capacity(128, f64::NAN);
542
543 assert!(zero.r >= 7);
544 assert!(nan.r >= 7);
545 assert!((1usize << zero.q) >= 128);
546 assert!((1usize << nan.q) >= 128);
547 }
548
549 #[test]
550 fn string_keys() {
551 let mut qf = QuotientFilter::with_defaults();
552 qf.insert(&"hello");
553 qf.insert(&"world");
554 assert!(qf.contains(&"hello"));
555 assert!(qf.contains(&"world"));
556 assert!(!qf.contains(&"foo"));
557 }
558
559 #[test]
560 fn row_id_tracking() {
561 let mut dirty = QuotientFilter::new(QuotientFilterConfig { q: 12, r: 8 });
563
564 let dirty_rows = [5u32, 42, 100, 255, 1000];
566 for &row in &dirty_rows {
567 dirty.insert(&row);
568 }
569
570 for row in 0u32..2000 {
572 if dirty_rows.contains(&row) {
573 assert!(dirty.contains(&row), "dirty row {row} not found");
574 }
575 }
576
577 for &row in &dirty_rows {
579 dirty.remove(&row);
580 }
581 assert!(dirty.is_empty());
582 }
583
584 #[test]
585 fn merge_filters() {
586 let config = QuotientFilterConfig { q: 8, r: 8 };
587 let mut qf1 = QuotientFilter::new(config);
588 let mut qf2 = QuotientFilter::new(config);
589
590 for i in 0u64..10 {
591 qf1.insert(&i);
592 }
593 for i in 5u64..15 {
594 qf2.insert(&i);
595 }
596
597 let added = qf1.merge(&qf2);
598 assert!(added > 0, "merge should add elements");
599
600 for i in 0u64..15 {
602 assert!(qf1.contains(&i), "merged filter should contain {i}");
603 }
604 }
605
606 #[test]
607 fn merge_mismatched_config_is_noop() {
608 let mut qf1 = QuotientFilter::new(QuotientFilterConfig { q: 8, r: 8 });
609 let qf2 = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 8 });
610
611 qf1.insert(&1u64);
612 let added = qf1.merge(&qf2);
613 assert_eq!(added, 0, "mismatched configs should not merge");
614 }
615
616 #[test]
617 fn theoretical_fp_rate_increases_with_load() {
618 let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 8 });
619 let rate_empty = qf.theoretical_fp_rate();
620
621 for i in 0u64..100 {
622 qf.insert(&i);
623 }
624 let rate_loaded = qf.theoretical_fp_rate();
625
626 assert!(
627 rate_empty < rate_loaded,
628 "FP rate should increase with load"
629 );
630 }
631
632 #[test]
633 fn space_comparison_vs_bitset() {
634 let dirty_config = QuotientFilterConfig::for_capacity(1000, 0.01);
636 let dirty_qf_bits = (dirty_config.r as usize + 3) * (1usize << dirty_config.q);
637
638 let bitset_bits = 1_000_000usize;
640
641 assert!(
642 dirty_qf_bits < bitset_bits,
643 "QF ({dirty_qf_bits} bits) should be smaller than bitset ({bitset_bits} bits) for sparse dirty sets"
644 );
645 }
646
647 #[test]
648 fn default_config() {
649 let qf = QuotientFilter::default();
650 assert_eq!(qf.capacity(), 1024);
651 assert!(qf.is_empty());
652 }
653
654 #[test]
655 fn insert_after_remove_reuses_slot() {
656 let mut qf = QuotientFilter::with_defaults();
657 qf.insert(&1u64);
658 qf.remove(&1u64);
659 assert!(qf.insert(&1u64));
660 assert!(qf.contains(&1u64));
661 assert_eq!(qf.len(), 1);
662 }
663
664 #[test]
665 fn heavy_load() {
666 let mut qf = QuotientFilter::new(QuotientFilterConfig { q: 10, r: 20 }); let target = 500;
669 let mut inserted = 0;
670 for i in 0u64..target as u64 {
671 if qf.insert(&i) {
672 inserted += 1;
673 }
674 }
675 assert_eq!(inserted, target);
676
677 for i in 0u64..target as u64 {
679 assert!(qf.contains(&i), "element {i} missing at high load");
680 }
681 }
682}