1use std::hash::{Hash, Hasher};
40use std::sync::{Arc, Mutex, Weak};
41
42use rustc_hash::FxHasher;
43
44use radixdb_core::CompactArc;
45use radixdb_core::{Row, Value};
46
47pub trait JoinHashObserver {
51 fn insert_raw_hash(&mut self, hash: u64);
53
54 fn retained_bytes(&self) -> usize;
56}
57
58#[derive(Clone)]
66pub struct JoinHashState {
67 build_rows: CompactArc<Vec<Row>>,
68 key_indices: CompactArc<[usize]>,
69 table: Arc<JoinHashTable>,
70 _memory_reservation: Option<Arc<JoinMemoryReservation>>,
73}
74
75#[derive(Default)]
76struct JoinMemoryUsage {
77 retained_bytes: usize,
78 peak_bytes: usize,
79}
80
81#[derive(Default)]
82pub(crate) struct JoinMemoryOwner {
83 usage: Mutex<JoinMemoryUsage>,
84}
85
86impl std::fmt::Debug for JoinMemoryOwner {
87 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 formatter
89 .debug_struct("JoinMemoryOwner")
90 .field("retained_bytes", &self.retained_bytes())
91 .field("peak_bytes", &self.peak_bytes())
92 .finish()
93 }
94}
95
96impl JoinMemoryOwner {
97 pub(crate) fn try_reserve(
98 self: &Arc<Self>,
99 bytes: usize,
100 max_bytes: usize,
101 ) -> Option<JoinMemoryReservation> {
102 let mut usage = self
103 .usage
104 .lock()
105 .unwrap_or_else(std::sync::PoisonError::into_inner);
106 let next = usage.retained_bytes.checked_add(bytes)?;
107 if next > max_bytes {
108 return None;
109 }
110 usage.retained_bytes = next;
111 usage.peak_bytes = usage.peak_bytes.max(next);
112 Some(JoinMemoryReservation {
113 owner: Arc::downgrade(self),
114 bytes,
115 })
116 }
117
118 pub(crate) fn retained_bytes(&self) -> usize {
119 self.usage
120 .lock()
121 .unwrap_or_else(std::sync::PoisonError::into_inner)
122 .retained_bytes
123 }
124
125 pub(crate) fn peak_bytes(&self) -> usize {
126 self.usage
127 .lock()
128 .unwrap_or_else(std::sync::PoisonError::into_inner)
129 .peak_bytes
130 }
131}
132
133#[doc(hidden)]
134pub struct JoinMemoryReservation {
135 owner: Weak<JoinMemoryOwner>,
136 bytes: usize,
137}
138
139impl JoinMemoryReservation {
140 #[doc(hidden)]
145 pub fn try_resize(&mut self, bytes: usize, max_bytes: usize) -> bool {
146 let Some(owner) = self.owner.upgrade() else {
147 self.bytes = bytes;
150 return true;
151 };
152 let mut usage = owner
153 .usage
154 .lock()
155 .unwrap_or_else(std::sync::PoisonError::into_inner);
156 let without_self = usage.retained_bytes.saturating_sub(self.bytes);
157 let Some(next) = without_self.checked_add(bytes) else {
158 return false;
159 };
160 if next > max_bytes {
161 return false;
162 }
163 usage.retained_bytes = next;
164 usage.peak_bytes = usage.peak_bytes.max(next);
165 self.bytes = bytes;
166 true
167 }
168}
169
170impl std::fmt::Debug for JoinMemoryReservation {
171 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 formatter
173 .debug_struct("JoinMemoryReservation")
174 .field("bytes", &self.bytes)
175 .finish()
176 }
177}
178
179impl Drop for JoinMemoryReservation {
180 fn drop(&mut self) {
181 let Some(owner) = self.owner.upgrade() else {
182 return;
183 };
184 let mut usage = owner
185 .usage
186 .lock()
187 .unwrap_or_else(std::sync::PoisonError::into_inner);
188 usage.retained_bytes = usage.retained_bytes.saturating_sub(self.bytes);
189 }
190}
191
192impl JoinHashState {
193 pub fn build(build_rows: CompactArc<Vec<Row>>, key_indices: &[usize]) -> Self {
196 let table = JoinHashTable::build(&build_rows, key_indices);
197 Self::from_table(build_rows, key_indices, table, None)
198 }
199
200 pub fn try_build(
204 build_rows: CompactArc<Vec<Row>>,
205 key_indices: &[usize],
206 max_bytes: usize,
207 ) -> Option<Self> {
208 JoinHashTable::fits_retained_budget(build_rows.len(), max_bytes)
209 .then(|| Self::build(build_rows, key_indices))
210 }
211
212 pub(crate) fn build_reserved(
213 build_rows: CompactArc<Vec<Row>>,
214 key_indices: &[usize],
215 reservation: JoinMemoryReservation,
216 ) -> Self {
217 let table = JoinHashTable::build(&build_rows, key_indices);
218 Self::from_table(build_rows, key_indices, table, Some(reservation))
219 }
220
221 pub fn build_with_bloom(
225 build_rows: CompactArc<Vec<Row>>,
226 key_indices: &[usize],
227 observer: &mut impl JoinHashObserver,
228 ) -> Self {
229 let table = JoinHashTable::build_with_observer(&build_rows, key_indices, observer);
230 Self::from_table(build_rows, key_indices, table, None)
231 }
232
233 pub(crate) fn build_with_bloom_reserved(
234 build_rows: CompactArc<Vec<Row>>,
235 key_indices: &[usize],
236 observer: &mut impl JoinHashObserver,
237 reservation: JoinMemoryReservation,
238 ) -> Self {
239 let table = JoinHashTable::build_with_observer(&build_rows, key_indices, observer);
240 Self::from_table(build_rows, key_indices, table, Some(reservation))
241 }
242
243 fn from_table(
244 build_rows: CompactArc<Vec<Row>>,
245 key_indices: &[usize],
246 table: JoinHashTable,
247 reservation: Option<JoinMemoryReservation>,
248 ) -> Self {
249 assert_eq!(
250 table.len(),
251 build_rows.len(),
252 "join hash state must contain one entry per build row"
253 );
254 Self {
255 build_rows,
256 key_indices: CompactArc::from(key_indices.to_vec()),
257 table: Arc::new(table),
258 _memory_reservation: reservation.map(Arc::new),
259 }
260 }
261
262 pub fn matches(&self, build_rows: &CompactArc<Vec<Row>>, key_indices: &[usize]) -> bool {
266 CompactArc::ptr_eq(&self.build_rows, build_rows) && self.key_indices.as_ref() == key_indices
267 }
268
269 #[inline]
270 pub fn build_rows(&self) -> &CompactArc<Vec<Row>> {
271 &self.build_rows
272 }
273
274 #[inline]
275 pub fn table(&self) -> &Arc<JoinHashTable> {
276 &self.table
277 }
278
279 #[cfg(test)]
280 fn shares_allocations_with(&self, other: &Self) -> bool {
281 CompactArc::ptr_eq(&self.build_rows, &other.build_rows)
282 && Arc::ptr_eq(&self.table, &other.table)
283 }
284}
285
286const EMPTY: u32 = u32::MAX;
288
289const MIN_BUCKETS: usize = 16;
291
292#[doc(hidden)]
296pub const DEFAULT_JOIN_HASH_STATE_MAX_BYTES: usize = 256 * 1024 * 1024;
297
298#[repr(C)]
302#[derive(Debug, Clone, Copy)]
303struct HashEntry {
304 hash: u64,
307 row_idx: u32,
309 next: u32,
311}
312
313impl HashEntry {
314 #[inline]
315 fn new(hash: u64, row_idx: u32, next: u32) -> Self {
316 Self {
317 hash,
318 row_idx,
319 next,
320 }
321 }
322}
323
324pub struct JoinHashTable {
348 bucket_heads: Vec<i32>,
351
352 entries: Vec<HashEntry>,
355
356 bucket_mask: u64,
358
359 len: usize,
361}
362
363impl JoinHashTable {
364 fn bucket_count_for_rows(row_count: usize) -> Option<usize> {
365 row_count
366 .checked_mul(4)
367 .map(|scaled| scaled / 3)
368 .map(|scaled| scaled.max(MIN_BUCKETS))?
369 .checked_next_power_of_two()
370 }
371
372 pub fn estimated_retained_bytes(row_count: usize) -> Option<usize> {
375 if row_count > u32::MAX as usize {
376 return None;
377 }
378 let buckets = Self::bucket_count_for_rows(row_count)?;
379 buckets
380 .checked_mul(std::mem::size_of::<i32>())?
381 .checked_add(row_count.checked_mul(std::mem::size_of::<HashEntry>())?)
382 }
383
384 #[inline]
385 pub fn fits_retained_budget(row_count: usize, max_bytes: usize) -> bool {
386 Self::estimated_retained_bytes(row_count).is_some_and(|bytes| bytes <= max_bytes)
387 }
388
389 pub fn with_capacity(row_count: usize) -> Self {
394 let bucket_count = Self::bucket_count_for_rows(row_count)
398 .expect("join hash table capacity exceeds addressable range");
399 assert!(
400 row_count <= u32::MAX as usize,
401 "join hash table row index exceeds u32"
402 );
403
404 let bucket_mask = (bucket_count - 1) as u64;
405
406 Self {
407 bucket_heads: vec![-1; bucket_count],
408 entries: Vec::with_capacity(row_count),
409 bucket_mask,
410 len: 0,
411 }
412 }
413
414 pub fn empty() -> Self {
416 Self {
417 bucket_heads: vec![-1; MIN_BUCKETS],
418 entries: Vec::new(),
419 bucket_mask: (MIN_BUCKETS - 1) as u64,
420 len: 0,
421 }
422 }
423
424 pub fn build(rows: &[Row], key_indices: &[usize]) -> Self {
428 #[cfg(feature = "bench-harness")]
429 let started = std::time::Instant::now();
430 let mut table = Self::with_capacity(rows.len());
431
432 for (idx, row) in rows.iter().enumerate() {
433 let hash = hash_row_keys(row, key_indices);
434 table.insert(hash, idx as u32);
435 }
436
437 #[cfg(feature = "bench-harness")]
438 radixdb_storage::instrumentation::record_hash_build(rows.len(), started.elapsed());
439 table
440 }
441
442 pub fn build_with_observer(
455 rows: &[Row],
456 key_indices: &[usize],
457 observer: &mut impl JoinHashObserver,
458 ) -> Self {
459 #[cfg(feature = "bench-harness")]
460 let started = std::time::Instant::now();
461 let mut table = Self::with_capacity(rows.len());
462
463 for (idx, row) in rows.iter().enumerate() {
464 let hash = hash_row_keys(row, key_indices);
466
467 table.insert(hash, idx as u32);
469
470 observer.insert_raw_hash(hash);
473 }
474
475 #[cfg(feature = "bench-harness")]
476 radixdb_storage::instrumentation::record_hash_build(rows.len(), started.elapsed());
477 table
478 }
479
480 #[inline]
486 pub fn insert(&mut self, hash: u64, row_idx: u32) {
487 let bucket = (hash & self.bucket_mask) as usize;
488
489 let old_head = self.bucket_heads[bucket];
491
492 let entry_idx = self.len as u32;
495 let next = if old_head >= 0 {
496 old_head as u32
497 } else {
498 EMPTY
499 };
500 self.entries.push(HashEntry::new(hash, row_idx, next));
501
502 self.bucket_heads[bucket] = entry_idx as i32;
504 self.len += 1;
505 }
506
507 #[inline]
516 pub fn probe(&self, hash: u64) -> ProbeIter<'_> {
517 let bucket = (hash & self.bucket_mask) as usize;
518 let first = self.bucket_heads[bucket];
519
520 ProbeIter {
521 table: self,
522 hash,
523 current: first,
524 }
525 }
526
527 #[inline]
531 pub fn probe_cursor(&self, hash: u64) -> ProbeCursor {
532 let bucket = (hash & self.bucket_mask) as usize;
533 ProbeCursor {
534 hash,
535 current: self.bucket_heads[bucket],
536 }
537 }
538
539 #[inline]
541 pub fn probe_next(&self, cursor: &mut ProbeCursor) -> Option<usize> {
542 while cursor.current >= 0 {
543 let entry = &self.entries[cursor.current as usize];
544 cursor.current = if entry.next == EMPTY {
545 -1
546 } else {
547 entry.next as i32
548 };
549 if entry.hash == cursor.hash {
550 return Some(entry.row_idx as usize);
551 }
552 }
553 None
554 }
555
556 #[inline]
558 pub fn len(&self) -> usize {
559 self.len
560 }
561
562 #[inline]
564 pub fn is_empty(&self) -> bool {
565 self.len == 0
566 }
567
568 #[inline]
570 pub fn bucket_count(&self) -> usize {
571 self.bucket_heads.len()
572 }
573
574 #[inline]
576 pub fn load_factor(&self) -> f64 {
577 self.len as f64 / self.bucket_heads.len() as f64
578 }
579}
580
581pub struct ProbeIter<'a> {
586 table: &'a JoinHashTable,
587 hash: u64,
588 current: i32,
589}
590
591#[derive(Debug, Clone, Copy, Default)]
593pub struct ProbeCursor {
594 hash: u64,
595 current: i32,
596}
597
598impl Iterator for ProbeIter<'_> {
599 type Item = usize;
600
601 #[inline]
602 fn next(&mut self) -> Option<usize> {
603 while self.current >= 0 {
604 let entry = &self.table.entries[self.current as usize];
605 self.current = if entry.next == EMPTY {
606 -1
607 } else {
608 entry.next as i32
609 };
610
611 if entry.hash == self.hash {
613 return Some(entry.row_idx as usize);
614 }
615 }
616 None
617 }
618}
619
620#[inline]
629pub fn hash_keys_with<'a, F>(key_indices: &[usize], get_value: F) -> u64
630where
631 F: Fn(usize) -> Option<&'a Value>,
632{
633 let mut hasher = FxHasher::default();
634
635 for &idx in key_indices {
636 if let Some(value) = get_value(idx) {
637 hash_value(&mut hasher, value);
638 } else {
639 0xDEADBEEF_u64.hash(&mut hasher);
641 }
642 }
643
644 hasher.finish()
645}
646
647#[inline]
652pub fn hash_row_keys(row: &Row, key_indices: &[usize]) -> u64 {
653 let mut hasher = FxHasher::default();
654
655 for &idx in key_indices {
656 if let Some(value) = row.get(idx) {
657 hash_value(&mut hasher, value);
658 } else {
659 0xDEADBEEF_u64.hash(&mut hasher);
661 }
662 }
663
664 hasher.finish()
665}
666
667#[inline]
669fn hash_value<H: Hasher>(hasher: &mut H, value: &Value) {
670 value.hash(hasher);
671}
672
673#[inline]
677pub fn verify_key_equality(row1: &Row, row2: &Row, indices1: &[usize], indices2: &[usize]) -> bool {
678 debug_assert_eq!(indices1.len(), indices2.len());
679
680 for (&idx1, &idx2) in indices1.iter().zip(indices2.iter()) {
681 let (Some(value1), Some(value2)) = (row1.get(idx1), row2.get(idx2)) else {
682 return false;
683 };
684
685 if value1.is_null() || value2.is_null() || value1 != value2 {
686 return false;
687 }
688 }
689
690 true
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn hash_entry_retains_only_hash_and_compact_row_reference() {
699 assert_eq!(std::mem::size_of::<HashEntry>(), 16);
700 }
701
702 #[test]
703 fn immutable_hash_state_reuses_only_exact_batch_and_key_layout() {
704 let rows = CompactArc::new(vec![make_row(vec![1, 10]), make_row(vec![2, 20])]);
705 let state = JoinHashState::build(CompactArc::clone(&rows), &[0]);
706 let reused = state.clone();
707
708 assert!(state.matches(&rows, &[0]));
709 assert!(!state.matches(&rows, &[1]));
710 assert!(!state.matches(&CompactArc::new((*rows).clone()), &[0]));
711 assert!(state.shares_allocations_with(&reused));
712 }
713
714 #[test]
715 fn hash_state_admission_counts_buckets_and_entries_before_allocation() {
716 let retained = JoinHashTable::estimated_retained_bytes(100).unwrap();
717 assert_eq!(retained, 256 * std::mem::size_of::<i32>() + 100 * 16);
718 assert!(JoinHashTable::fits_retained_budget(100, retained));
719 assert!(!JoinHashTable::fits_retained_budget(100, retained - 1));
720 assert!(JoinHashTable::estimated_retained_bytes(u32::MAX as usize + 1).is_none());
721 }
722
723 use radixdb_core::DataType;
724
725 fn make_row(values: Vec<i64>) -> Row {
726 Row::from_values(values.into_iter().map(Value::integer).collect())
727 }
728
729 #[test]
730 fn test_basic_insert_and_probe() {
731 let mut table = JoinHashTable::with_capacity(4);
732
733 table.insert(100, 0);
735 table.insert(200, 1);
736 table.insert(100, 2); table.insert(300, 3);
738
739 assert_eq!(table.len(), 4);
740
741 let matches: Vec<_> = table.probe(100).collect();
743 assert_eq!(matches.len(), 2);
744 assert!(matches.contains(&0));
745 assert!(matches.contains(&2));
746
747 let matches: Vec<_> = table.probe(200).collect();
749 assert_eq!(matches, vec![1]);
750
751 let matches: Vec<_> = table.probe(999).collect();
753 assert!(matches.is_empty());
754 }
755
756 #[test]
757 fn test_build_from_rows() {
758 let rows = vec![
759 make_row(vec![1, 10]),
760 make_row(vec![2, 20]),
761 make_row(vec![1, 30]), make_row(vec![3, 40]),
763 ];
764
765 let key_indices = vec![0]; let table = JoinHashTable::build(&rows, &key_indices);
767
768 assert_eq!(table.len(), 4);
769
770 let hash = hash_row_keys(&rows[0], &key_indices);
772 let matches: Vec<_> = table.probe(hash).collect();
773 assert_eq!(matches.len(), 2);
774 }
775
776 #[cfg(feature = "bench-harness")]
777 #[test]
778 fn runtime_profile_observes_bulk_hash_build_under_parallel_tests() {
779 let before = radixdb_storage::instrumentation::snapshot().runtime_profile;
784 let rows = vec![make_row(vec![1]), make_row(vec![2]), make_row(vec![3])];
785
786 let _table = JoinHashTable::build(&rows, &[0]);
787
788 let after = radixdb_storage::instrumentation::snapshot().runtime_profile;
789 let delta = after.delta(before);
790 assert!(delta.hash_build_calls >= 1);
791 assert!(delta.hash_build_rows >= 3);
792 assert!(delta.hash_build_nanos > 0);
793 }
794
795 #[test]
796 fn test_empty_table() {
797 let table = JoinHashTable::empty();
798 assert!(table.is_empty());
799 assert_eq!(table.len(), 0);
800
801 let matches: Vec<_> = table.probe(100).collect();
802 assert!(matches.is_empty());
803 }
804
805 #[test]
806 fn test_load_factor() {
807 let mut table = JoinHashTable::with_capacity(100);
808
809 for i in 0..100 {
810 table.insert(i as u64, i as u32);
811 }
812
813 let load = table.load_factor();
816 assert!(
817 load > 0.3 && load <= 1.0,
818 "Load factor {} out of expected range",
819 load
820 );
821 assert_eq!(table.len(), 100);
823 }
824
825 #[test]
826 fn test_verify_key_equality() {
827 let row1 = Row::from_values(vec![Value::integer(1), Value::text("hello")]);
828 let row2 = Row::from_values(vec![Value::integer(1), Value::text("hello")]);
829 let row3 = Row::from_values(vec![Value::integer(2), Value::text("hello")]);
830
831 assert!(verify_key_equality(&row1, &row2, &[0, 1], &[0, 1]));
832 assert!(!verify_key_equality(&row1, &row3, &[0, 1], &[0, 1]));
833 }
834
835 #[test]
836 fn test_hash_row_keys() {
837 let row1 = make_row(vec![1, 2, 3]);
838 let row2 = make_row(vec![1, 2, 3]);
839 let row3 = make_row(vec![1, 2, 4]);
840
841 let indices = vec![0, 1];
842
843 assert_eq!(
845 hash_row_keys(&row1, &indices),
846 hash_row_keys(&row2, &indices)
847 );
848
849 let row4 = make_row(vec![1, 2, 999]);
851 assert_eq!(
852 hash_row_keys(&row1, &indices),
853 hash_row_keys(&row4, &indices)
854 );
855
856 assert_ne!(hash_row_keys(&row1, &[0, 2]), hash_row_keys(&row3, &[0, 2]));
859 }
860
861 fn assert_join_key_equal(left: Value, right: Value) {
862 let left_row = Row::from_values(vec![left]);
863 let right_row = Row::from_values(vec![right]);
864
865 assert_eq!(
866 hash_row_keys(&left_row, &[0]),
867 hash_row_keys(&right_row, &[0])
868 );
869 assert!(verify_key_equality(&left_row, &right_row, &[0], &[0]));
870 }
871
872 #[test]
873 fn test_canonical_numeric_join_keys() {
874 const TWO_POW_53: i64 = 9_007_199_254_740_992;
875
876 assert_join_key_equal(Value::Integer(TWO_POW_53), Value::Float(TWO_POW_53 as f64));
877 assert_join_key_equal(
878 Value::Integer(TWO_POW_53 + 2),
879 Value::Float((TWO_POW_53 + 2) as f64),
880 );
881 assert_join_key_equal(Value::Float(0.0), Value::Float(-0.0));
882 assert_join_key_equal(Value::Integer(0), Value::Float(-0.0));
883 }
884
885 #[test]
886 fn test_rounded_integer_neighbor_does_not_join() {
887 const TWO_POW_53: i64 = 9_007_199_254_740_992;
888
889 let build_rows = [Row::from_values(vec![Value::Integer(TWO_POW_53 + 1)])];
890 let probe_row = Row::from_values(vec![Value::Float((TWO_POW_53 + 1) as f64)]);
891 let probe_hash = hash_row_keys(&probe_row, &[0]);
892 let mut table = JoinHashTable::with_capacity(1);
893 table.insert(probe_hash, 0); assert!(!verify_key_equality(&probe_row, &build_rows[0], &[0], &[0]));
896 let verified_matches = table
897 .probe(probe_hash)
898 .filter(|&build_idx| {
899 verify_key_equality(&probe_row, &build_rows[build_idx], &[0], &[0])
900 })
901 .count();
902 assert_eq!(verified_matches, 0);
903 }
904
905 #[test]
906 fn test_nan_payloads_share_join_key_contract() {
907 let nan1 = f64::from_bits(0x7ff8_0000_0000_0001);
908 let nan2 = f64::from_bits(0x7ff8_0000_0000_0002);
909
910 assert_join_key_equal(Value::Float(nan1), Value::Float(nan2));
911 }
912
913 #[test]
914 fn test_null_keys_never_join() {
915 let left = Row::from_values(vec![Value::Null(DataType::Integer)]);
916 let right = Row::from_values(vec![Value::Null(DataType::Float)]);
917
918 assert_eq!(hash_row_keys(&left, &[0]), hash_row_keys(&right, &[0]));
919 assert!(!verify_key_equality(&left, &right, &[0], &[0]));
920 }
921
922 #[test]
923 fn test_composite_keys_use_canonical_value_contract() {
924 const TWO_POW_53: i64 = 9_007_199_254_740_992;
925
926 let integer_key =
927 Row::from_values(vec![Value::Integer(TWO_POW_53), Value::Text("same".into())]);
928 let float_key = Row::from_values(vec![
929 Value::Float(TWO_POW_53 as f64),
930 Value::Text("same".into()),
931 ]);
932 let different_tail = Row::from_values(vec![
933 Value::Float(TWO_POW_53 as f64),
934 Value::Text("different".into()),
935 ]);
936 let null_tail = Row::from_values(vec![
937 Value::Float(TWO_POW_53 as f64),
938 Value::Null(DataType::Text),
939 ]);
940
941 assert_eq!(
942 hash_row_keys(&integer_key, &[0, 1]),
943 hash_row_keys(&float_key, &[0, 1])
944 );
945 assert!(verify_key_equality(
946 &integer_key,
947 &float_key,
948 &[0, 1],
949 &[0, 1]
950 ));
951 assert!(!verify_key_equality(
952 &integer_key,
953 &different_tail,
954 &[0, 1],
955 &[0, 1]
956 ));
957 assert!(!verify_key_equality(
958 &integer_key,
959 &null_tail,
960 &[0, 1],
961 &[0, 1]
962 ));
963 }
964
965 #[test]
966 fn test_row_and_indexed_get_hash_paths_are_identical() {
967 let row = Row::from_values(vec![
968 Value::Integer(9_007_199_254_740_992),
969 Value::Float(-0.0),
970 Value::Text("key".into()),
971 ]);
972 let indices = [0, 1, 2];
973
974 assert_eq!(
975 hash_row_keys(&row, &indices),
976 hash_keys_with(&indices, |idx| row.get(idx))
977 );
978 }
979
980 #[test]
981 fn test_chain_collision() {
982 let mut table = JoinHashTable {
984 bucket_heads: vec![-1; 4], entries: Vec::new(),
986 bucket_mask: 3,
987 len: 0,
988 };
989
990 table.insert(0, 0);
992 table.insert(4, 1);
993 table.insert(8, 2);
994 table.insert(12, 3);
995
996 assert_eq!(table.probe(0).count(), 1);
998 assert_eq!(table.probe(4).count(), 1);
999 assert_eq!(table.probe(8).count(), 1);
1000 assert_eq!(table.probe(12).count(), 1);
1001 }
1002}