Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions pgdog-config/src/data_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ impl Hash for Float {
if self.0.is_nan() {
// All NaN values hash to the same value
0u8.hash(state);
} else if self.0 == 0.0 {
// 0.0 and -0.0 compare equal but have different bit patterns,
// so they must hash to the same value. Postgres normalizes the
// sign of zero the same way, in hashfloat4.
0.0_f32.to_bits().hash(state);
} else {
// Use bit representation for consistent hashing
self.0.to_bits().hash(state);
Expand Down Expand Up @@ -184,3 +189,34 @@ impl From<Vec<Float>> for Vector {
Self { values: value }
}
}

#[cfg(test)]
mod test {
use super::*;
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;

fn hash_of(float: Float) -> u64 {
let mut hasher = DefaultHasher::new();
float.hash(&mut hasher);
hasher.finish()
}

#[test]
fn test_negative_zero_hashes_like_zero() {
assert_eq!(Float(0.0), Float(-0.0));
assert_eq!(hash_of(Float(0.0)), hash_of(Float(-0.0)));

let mut set = HashSet::new();
set.insert(Vector::from(vec![0.0_f32, 1.0]));
set.insert(Vector::from(vec![-0.0_f32, 1.0]));
assert_eq!(set.len(), 1);
}

#[test]
fn test_distinct_values_still_hash_apart() {
assert_ne!(hash_of(Float(1.0)), hash_of(Float(-1.0)));
assert_ne!(hash_of(Float(0.0)), hash_of(Float(1.0)));
assert_ne!(hash_of(Float(f32::NAN)), hash_of(Float(0.0)));
}
}
35 changes: 35 additions & 0 deletions pgdog-postgres-types/src/datum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,15 @@ mod tests {
use super::*;
use bytes::{BufMut, BytesMut};
use std::assert_matches;
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

fn hash_of(datum: &Datum) -> u64 {
let mut hasher = DefaultHasher::new();
datum.hash(&mut hasher);
hasher.finish()
}

#[test]
fn test_multidimensional_text_array_falls_back_to_unknown() {
Expand Down Expand Up @@ -378,4 +387,30 @@ mod tests {
assert_matches!(datum, Datum::Unknown(_));
assert_eq!(datum.encode(Format::Binary).unwrap(), input);
}

#[test]
fn test_negative_zero_datums_hash_like_zero() {
// Datum derives both Hash and PartialEq, and cross-shard GROUP BY
// keys a HashMap on the grouped Datums, so every float-carrying
// shape has to agree that -0.0 and 0.0 belong in one bucket.
let pairs = [
(Datum::Float(Float(0.0)), Datum::Float(Float(-0.0))),
(Datum::Double(Double(0.0)), Datum::Double(Double(-0.0))),
(
Datum::Vector(Vector::from(vec![0.0_f32, 1.0])),
Datum::Vector(Vector::from(vec![-0.0_f32, 1.0])),
),
];

for (zero, neg_zero) in pairs {
assert_eq!(zero, neg_zero);
assert_eq!(hash_of(&zero), hash_of(&neg_zero));

let mut set = HashSet::new();
set.insert(zero.clone());
set.insert(neg_zero);
assert_eq!(set.len(), 1);
assert!(set.contains(&zero));
}
}
}
33 changes: 30 additions & 3 deletions pgdog-postgres-types/src/double.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ impl Hash for Double {
if self.0.is_nan() {
// All NaN values hash to the same value
0u8.hash(state);
} else if self.0 == 0.0 {
// 0.0 and -0.0 compare equal but have different bit patterns,
// so they must hash to the same value. Postgres normalizes the
// sign of zero the same way, in hashfloat8.
0.0_f64.to_bits().hash(state);
} else {
// Use bit representation for consistent hashing
self.0.to_bits().hash(state);
Expand Down Expand Up @@ -278,7 +283,7 @@ mod tests {

assert_eq!(hash1, hash2);

// Different values should (likely) have different hashes
// 0.0 and -0.0 compare equal, so they must hash the same
let mut hasher3 = DefaultHasher::new();
zero.hash(&mut hasher3);
let hash3 = hasher3.finish();
Expand All @@ -287,8 +292,30 @@ mod tests {
neg_zero.hash(&mut hasher4);
let hash4 = hasher4.finish();

// Note: 0.0 and -0.0 have different bit patterns
assert_ne!(hash3, hash4);
assert_eq!(zero, neg_zero);
assert_eq!(hash3, hash4);

// Values that differ must still land on different hashes
let mut hasher5 = DefaultHasher::new();
Double(1.0).hash(&mut hasher5);
assert_ne!(hash3, hasher5.finish());
}

#[test]
fn test_double_negative_zero_groups_with_zero() {
use std::collections::HashSet;

// Cross-shard GROUP BY buckets rows in a HashMap keyed on the
// grouped values, so -0.0 coming back from one shard has to land
// in the same bucket as 0.0 from another, the way it would on a
// single Postgres node.
let mut set = HashSet::new();
set.insert(Double(0.0));
set.insert(Double(-0.0));

assert_eq!(set.len(), 1);
assert!(set.contains(&Double(-0.0)));
assert!(set.contains(&Double(0.0)));
}

#[test]
Expand Down
11 changes: 8 additions & 3 deletions pgdog-postgres-types/src/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ mod tests {

assert_eq!(hash1, hash2);

// Different values should (likely) have different hashes
// 0.0 and -0.0 compare equal, so they must hash the same
let mut hasher3 = DefaultHasher::new();
zero.hash(&mut hasher3);
let hash3 = hasher3.finish();
Expand All @@ -201,8 +201,13 @@ mod tests {
neg_zero.hash(&mut hasher4);
let hash4 = hasher4.finish();

// Note: 0.0 and -0.0 have different bit patterns
assert_ne!(hash3, hash4);
assert_eq!(zero, neg_zero);
assert_eq!(hash3, hash4);

// Values that differ must still land on different hashes
let mut hasher5 = DefaultHasher::new();
Float(1.0).hash(&mut hasher5);
assert_ne!(hash3, hasher5.finish());
}

#[test]
Expand Down
37 changes: 37 additions & 0 deletions pgdog-vector/src/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ impl Hash for Float {
if self.0.is_nan() {
// All NaN values hash to the same value
0u8.hash(state);
} else if self.0 == 0.0 {
// 0.0 and -0.0 compare equal but have different bit patterns,
// so they must hash to the same value. Postgres normalizes the
// sign of zero the same way, in hashfloat4.
0.0_f32.to_bits().hash(state);
} else {
// Use bit representation for consistent hashing
self.0.to_bits().hash(state);
Expand Down Expand Up @@ -81,3 +86,35 @@ impl From<Float> for f32 {
value.0
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;

fn hash_of(float: Float) -> u64 {
let mut hasher = DefaultHasher::new();
float.hash(&mut hasher);
hasher.finish()
}

#[test]
fn test_negative_zero_hashes_like_zero() {
assert_eq!(Float(0.0), Float(-0.0));
assert_eq!(hash_of(Float(0.0)), hash_of(Float(-0.0)));

let mut set = HashSet::new();
set.insert(Float(0.0));
set.insert(Float(-0.0));
assert_eq!(set.len(), 1);
assert!(set.contains(&Float(-0.0)));
}

#[test]
fn test_distinct_values_still_hash_apart() {
assert_ne!(hash_of(Float(1.0)), hash_of(Float(-1.0)));
assert_ne!(hash_of(Float(0.0)), hash_of(Float(1.0)));
assert_ne!(hash_of(Float(f32::NAN)), hash_of(Float(0.0)));
}
}