From 91019f0414c80984b5f838deed39cd633749f4b5 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 6 Feb 2023 16:41:31 +0800 Subject: [PATCH 1/3] Hashable value --- Cargo.toml | 3 ++ src/value.rs | 132 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 25f32fdb1..0bfa4abb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ path = "src/lib.rs" sea-query-attr = { version = "0.1.1", path = "sea-query-attr", default-features = false, optional = true } sea-query-derive = { version = "0.3.0", path = "sea-query-derive", default-features = false, optional = true } serde_json = { version = "1", default-features = false, optional = true, features = ["std"] } +derivative = { version = "2.2", default-features = false, optional = true } chrono = { version = "0.4", default-features = false, optional = true, features = ["clock"] } postgres-types = { version = "0", default-features = false, optional = true } rust_decimal = { version = "1", default-features = false, optional = true } @@ -40,6 +41,7 @@ quote = { version = "1", default-features = false, optional = true } time = { version = "0.3", default-features = false, optional = true, features = ["macros", "formatting"] } ipnetwork = { version = "0.19", default-features = false, optional = true } mac_address = { version = "1.1", default-features = false, optional = true } +ordered-float = { version = "3.4", default-features = false, optional = true } [dev-dependencies] sea-query = { path = ".", features = ["tests-cfg"] } @@ -53,6 +55,7 @@ backend-sqlite = [] default = ["derive", "backend-mysql", "backend-postgres", "backend-sqlite"] derive = ["sea-query-derive"] attr = ["sea-query-attr"] +hashable-value = ["derivative", "ordered-float"] postgres-array = [] postgres-interval = ["proc-macro2", "quote"] thread-safe = [] diff --git a/src/value.rs b/src/value.rs index e9d844bff..b0160cf7b 100644 --- a/src/value.rs +++ b/src/value.rs @@ -119,7 +119,13 @@ pub enum ArrayType { /// Value variants /// /// We want Value to be exactly 1 pointer sized, so anything larger should be boxed. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] +#[cfg_attr(not(feature = "hashable-value"), derive(PartialEq))] +#[cfg_attr( + feature = "hashable-value", + derive(derivative::Derivative), + derivative(Hash, PartialEq, Eq) +)] pub enum Value { Bool(Option), TinyInt(Option), @@ -130,8 +136,26 @@ pub enum Value { SmallUnsigned(Option), Unsigned(Option), BigUnsigned(Option), - Float(Option), - Double(Option), + Float( + #[cfg_attr( + feature = "hashable-value", + derivative( + Hash(hash_with = "hashable_value::hash_f32"), + PartialEq(compare_with = "hashable_value::cmp_f32") + ) + )] + Option, + ), + Double( + #[cfg_attr( + feature = "hashable-value", + derivative( + Hash(hash_with = "hashable_value::hash_f64"), + PartialEq(compare_with = "hashable_value::cmp_f64") + ) + )] + Option, + ), String(Option>), Char(Option), @@ -1874,3 +1898,105 @@ mod tests { assert_eq!(out, None); } } + +#[cfg(feature = "hashable-value")] +mod hashable_value { + use ordered_float::{NotNan, OrderedFloat}; + use std::hash::{Hash, Hasher}; + + /// Panic when value is NaN + pub fn hash_f32(v: &Option, state: &mut H) { + match v { + Some(v) => NotNan::new(*v).unwrap().hash(state), + None => OrderedFloat(std::f32::NAN).hash(state), + } + } + + /// Panic when value is NaN + pub fn hash_f64(v: &Option, state: &mut H) { + match v { + Some(v) => NotNan::new(*v).unwrap().hash(state), + None => OrderedFloat(std::f64::NAN).hash(state), + } + } + + /// Panic when value is NaN + pub fn cmp_f32(l: &Option, r: &Option) -> bool { + match (l, r) { + (Some(l), Some(r)) => NotNan::new(*l).unwrap().eq(&NotNan::new(*r).unwrap()), + (None, None) => true, + _ => false, + } + } + + /// Panic when value is NaN + pub fn cmp_f64(l: &Option, r: &Option) -> bool { + match (l, r) { + (Some(l), Some(r)) => NotNan::new(*l).unwrap().eq(&NotNan::new(*r).unwrap()), + (None, None) => true, + _ => false, + } + } + + #[test] + fn test_hash_value_0() { + use super::Value; + + let hash_set: std::collections::HashSet = [ + Value::Int(None), + Value::Int(None), + Value::BigInt(None), + Value::BigInt(None), + Value::Float(None), + Value::Float(None), + Value::Double(None), + Value::Double(None), + ] + .into_iter() + .collect(); + + let unique: std::collections::HashSet = [ + Value::Int(None), + Value::BigInt(None), + Value::Float(None), + Value::Double(None), + ] + .into_iter() + .collect(); + + assert_eq!(hash_set, unique); + } + + #[test] + fn test_hash_value_1() { + use super::Value; + + let hash_set: std::collections::HashSet = [ + Value::Int(None), + Value::Int(Some(1)), + Value::Int(Some(1)), + Value::BigInt(Some(2)), + Value::BigInt(Some(2)), + Value::Float(Some(3.0)), + Value::Float(Some(3.0)), + Value::Double(Some(3.0)), + Value::Double(Some(3.0)), + Value::BigInt(Some(5)), + ] + .into_iter() + .collect(); + + let unique: std::collections::HashSet = [ + Value::BigInt(Some(5)), + Value::Double(Some(3.0)), + Value::Float(Some(3.0)), + Value::BigInt(Some(2)), + Value::Int(Some(1)), + Value::Int(None), + ] + .into_iter() + .collect(); + + assert_eq!(hash_set, unique); + } +} From 23a220a03c79337b00f5b57dc2feaa2c97a9ee2e Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 6 Feb 2023 21:39:36 +0800 Subject: [PATCH 2/3] Implement Json --- src/value.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/src/value.rs b/src/value.rs index b0160cf7b..d19b0ddcd 100644 --- a/src/value.rs +++ b/src/value.rs @@ -34,7 +34,7 @@ use mac_address::MacAddress; use crate::{BlobSize, ColumnType, CommonSqlQueryBuilder, QueryBuilder}; /// [`Value`] types variant for Postgres array -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum ArrayType { Bool, TinyInt, @@ -164,7 +164,16 @@ pub enum Value { #[cfg(feature = "with-json")] #[cfg_attr(docsrs, doc(cfg(feature = "with-json")))] - Json(Option>), + Json( + #[cfg_attr( + feature = "hashable-value", + derivative( + Hash(hash_with = "hashable_value::hash_json"), + PartialEq(compare_with = "hashable_value::cmp_json") + ) + )] + Option>, + ), #[cfg(feature = "with-chrono")] #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] @@ -1901,6 +1910,7 @@ mod tests { #[cfg(feature = "hashable-value")] mod hashable_value { + use super::*; use ordered_float::{NotNan, OrderedFloat}; use std::hash::{Hash, Hasher}; @@ -1938,10 +1948,27 @@ mod hashable_value { } } + #[cfg(feature = "with-json")] + pub fn hash_json(v: &Option>, state: &mut H) { + match v { + Some(v) => serde_json::to_string(v).unwrap().hash(state), + None => "null".hash(state), + } + } + + #[cfg(feature = "with-json")] + pub fn cmp_json(l: &Option>, r: &Option>) -> bool { + match (l, r) { + (Some(l), Some(r)) => serde_json::to_string(l) + .unwrap() + .eq(&serde_json::to_string(r).unwrap()), + (None, None) => true, + _ => false, + } + } + #[test] fn test_hash_value_0() { - use super::Value; - let hash_set: std::collections::HashSet = [ Value::Int(None), Value::Int(None), @@ -1969,8 +1996,6 @@ mod hashable_value { #[test] fn test_hash_value_1() { - use super::Value; - let hash_set: std::collections::HashSet = [ Value::Int(None), Value::Int(Some(1)), @@ -1999,4 +2024,52 @@ mod hashable_value { assert_eq!(hash_set, unique); } + + #[cfg(feature = "postgres-array")] + #[test] + fn test_hash_value_array() { + assert_eq!( + Into::::into(vec![0i32, 1, 2]), + Value::Array( + ArrayType::Int, + Some(Box::new(vec![ + Value::Int(Some(0)), + Value::Int(Some(1)), + Value::Int(Some(2)) + ])) + ) + ); + + assert_eq!( + Into::::into(vec![0f32, 1.0, 2.0]), + Value::Array( + ArrayType::Float, + Some(Box::new(vec![ + Value::Float(Some(0f32)), + Value::Float(Some(1.0)), + Value::Float(Some(2.0)) + ])) + ) + ); + + let hash_set: std::collections::HashSet = [ + Into::::into(vec![0i32, 1, 2]), + Into::::into(vec![0i32, 1, 2]), + Into::::into(vec![0f32, 1.0, 2.0]), + Into::::into(vec![0f32, 1.0, 2.0]), + Into::::into(vec![3f32, 2.0, 1.0]), + ] + .into_iter() + .collect(); + + let unique: std::collections::HashSet = [ + Into::::into(vec![0i32, 1, 2]), + Into::::into(vec![0f32, 1.0, 2.0]), + Into::::into(vec![3f32, 2.0, 1.0]), + ] + .into_iter() + .collect(); + + assert_eq!(hash_set, unique); + } } From 8a7e3aa8cfc50dee9a7ec08e3971fa285f9081e4 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Mon, 6 Feb 2023 21:48:59 +0800 Subject: [PATCH 3/3] Handle NaN --- src/value.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/value.rs b/src/value.rs index d19b0ddcd..4f587abf7 100644 --- a/src/value.rs +++ b/src/value.rs @@ -118,7 +118,10 @@ pub enum ArrayType { /// Value variants /// -/// We want Value to be exactly 1 pointer sized, so anything larger should be boxed. +/// We want the inner Value to be exactly 1 pointer sized, so anything larger should be boxed. +/// +/// If the `hashable-value` feature is enabled, NaN == NaN, which contradicts Rust's built-in +/// implementation of NaN != NaN. #[derive(Clone, Debug)] #[cfg_attr(not(feature = "hashable-value"), derive(PartialEq))] #[cfg_attr( @@ -1911,38 +1914,34 @@ mod tests { #[cfg(feature = "hashable-value")] mod hashable_value { use super::*; - use ordered_float::{NotNan, OrderedFloat}; + use ordered_float::OrderedFloat; use std::hash::{Hash, Hasher}; - /// Panic when value is NaN pub fn hash_f32(v: &Option, state: &mut H) { match v { - Some(v) => NotNan::new(*v).unwrap().hash(state), - None => OrderedFloat(std::f32::NAN).hash(state), + Some(v) => OrderedFloat(*v).hash(state), + None => "null".hash(state), } } - /// Panic when value is NaN pub fn hash_f64(v: &Option, state: &mut H) { match v { - Some(v) => NotNan::new(*v).unwrap().hash(state), - None => OrderedFloat(std::f64::NAN).hash(state), + Some(v) => OrderedFloat(*v).hash(state), + None => "null".hash(state), } } - /// Panic when value is NaN pub fn cmp_f32(l: &Option, r: &Option) -> bool { match (l, r) { - (Some(l), Some(r)) => NotNan::new(*l).unwrap().eq(&NotNan::new(*r).unwrap()), + (Some(l), Some(r)) => OrderedFloat(*l).eq(&OrderedFloat(*r)), (None, None) => true, _ => false, } } - /// Panic when value is NaN pub fn cmp_f64(l: &Option, r: &Option) -> bool { match (l, r) { - (Some(l), Some(r)) => NotNan::new(*l).unwrap().eq(&NotNan::new(*r).unwrap()), + (Some(l), Some(r)) => OrderedFloat(*l).eq(&OrderedFloat(*r)), (None, None) => true, _ => false, } @@ -1975,9 +1974,13 @@ mod hashable_value { Value::BigInt(None), Value::BigInt(None), Value::Float(None), - Value::Float(None), + Value::Float(None), // Null is not NaN + Value::Float(Some(std::f32::NAN)), // NaN considered equal + Value::Float(Some(std::f32::NAN)), Value::Double(None), Value::Double(None), + Value::Double(Some(std::f64::NAN)), + Value::Double(Some(std::f64::NAN)), ] .into_iter() .collect(); @@ -1987,6 +1990,8 @@ mod hashable_value { Value::BigInt(None), Value::Float(None), Value::Double(None), + Value::Float(Some(std::f32::NAN)), + Value::Double(Some(std::f64::NAN)), ] .into_iter() .collect();