diff --git a/src/ast/values.rs b/src/ast/values.rs index e5cf8ac8..dee3dbf0 100644 --- a/src/ast/values.rs +++ b/src/ast/values.rs @@ -45,6 +45,8 @@ pub enum Value<'a> { Int32(Option), /// 64-bit signed integer. Int64(Option), + /// 32-bit unsigned integer. + UnsignedInt32(Option), /// 32-bit floating point. Float(Option), /// 64-bit floating point. @@ -112,6 +114,7 @@ impl<'a> fmt::Display for Value<'a> { let res = match self { Value::Int32(val) => val.map(|v| write!(f, "{}", v)), Value::Int64(val) => val.map(|v| write!(f, "{}", v)), + Value::UnsignedInt32(val) => val.map(|v| write!(f, "{}", v)), Value::Float(val) => val.map(|v| write!(f, "{}", v)), Value::Double(val) => val.map(|v| write!(f, "{}", v)), Value::Text(val) => val.as_ref().map(|v| write!(f, "\"{}\"", v)), @@ -161,6 +164,7 @@ impl<'a> From> for serde_json::Value { let res = match pv { Value::Int32(i) => i.map(|i| serde_json::Value::Number(Number::from(i))), Value::Int64(i) => i.map(|i| serde_json::Value::Number(Number::from(i))), + Value::UnsignedInt32(u) => u.map(|u| serde_json::Value::Number(Number::from(u))), Value::Float(f) => f.map(|f| match Number::from_f64(f as f64) { Some(number) => serde_json::Value::Number(number), None => serde_json::Value::Null, @@ -222,6 +226,14 @@ impl<'a> Value<'a> { Value::Int64(Some(value.into())) } + /// Creates a new 32-bit signed integer. + pub fn uint32(value: I) -> Self + where + I: Into, + { + Value::UnsignedInt32(Some(value.into())) + } + /// Creates a new 32-bit signed integer. pub fn integer(value: I) -> Self where @@ -344,6 +356,7 @@ impl<'a> Value<'a> { match self { Value::Int32(i) => i.is_none(), Value::Int64(i) => i.is_none(), + Value::UnsignedInt32(u) => u.is_none(), Value::Float(i) => i.is_none(), Value::Double(i) => i.is_none(), Value::Text(t) => t.is_none(), @@ -442,6 +455,11 @@ impl<'a> Value<'a> { matches!(self, Value::Int64(_)) } + /// `true` if the `Value` is a 64-bit signed integer. + pub const fn is_uint32(&self) -> bool { + matches!(self, Value::UnsignedInt32(_)) + } + /// `true` if the `Value` is a signed integer. pub const fn is_integer(&self) -> bool { matches!(self, Value::Int32(_) | Value::Int64(_)) @@ -463,6 +481,14 @@ impl<'a> Value<'a> { } } + /// Returns an `i32` if the value is a 32-bit signed integer, otherwise `None`. + pub const fn as_uint32(&self) -> Option { + match self { + Value::UnsignedInt32(i) => *i, + _ => None, + } + } + /// Returns an `i64` if the value is a signed integer, otherwise `None`. pub fn as_integer(&self) -> Option { match self { @@ -526,6 +552,7 @@ impl<'a> Value<'a> { // For schemas which don't tag booleans Value::Int32(Some(i)) if *i == 0 || *i == 1 => true, Value::Int64(Some(i)) if *i == 0 || *i == 1 => true, + Value::UnsignedInt32(Some(i)) if *i == 0 || *i == 1 => true, _ => false, } } @@ -537,6 +564,7 @@ impl<'a> Value<'a> { // For schemas which don't tag booleans Value::Int32(Some(i)) if *i == 0 || *i == 1 => Some(*i == 1), Value::Int64(Some(i)) if *i == 0 || *i == 1 => Some(*i == 1), + Value::UnsignedInt32(Some(i)) if *i == 0 || *i == 1 => Some(*i == 1), _ => None, } } @@ -662,6 +690,7 @@ impl<'a> Value<'a> { value!(val: i64, Int64, val); value!(val: i32, Int32, val); +value!(val: u32, UnsignedInt32, val); value!(val: bool, Boolean, val); value!(val: &'a str, Text, val.into()); value!(val: String, Text, val.into()); @@ -708,6 +737,16 @@ impl<'a> TryFrom> for i32 { } } +impl<'a> TryFrom> for u32 { + type Error = Error; + + fn try_from(value: Value<'a>) -> Result { + value + .as_uint32() + .ok_or_else(|| Error::builder(ErrorKind::conversion("Not a u32")).build()) + } +} + #[cfg(feature = "bigdecimal")] impl<'a> TryFrom> for BigDecimal { type Error = Error; @@ -931,6 +970,13 @@ mod tests { assert_eq!(values, vec![1]); } + #[test] + fn a_parameterized_value_of_uints32_can_be_converted_into_a_vec() { + let pv = Value::array(vec![1_u32]); + let values: Vec = pv.into_vec().expect("convert into Vec"); + assert_eq!(values, vec![1]); + } + #[test] fn a_parameterized_value_of_reals_can_be_converted_into_a_vec() { let pv = Value::array(vec![1.0]); diff --git a/src/connector/mssql/conversion.rs b/src/connector/mssql/conversion.rs index aa7d681d..0ab7357a 100644 --- a/src/connector/mssql/conversion.rs +++ b/src/connector/mssql/conversion.rs @@ -13,6 +13,7 @@ impl<'a> IntoSql<'a> for &'a Value<'a> { match self { Value::Int32(val) => val.into_sql(), Value::Int64(val) => val.into_sql(), + Value::UnsignedInt32(val) => val.map(|val| val as i64).into_sql(), Value::Float(val) => val.into_sql(), Value::Double(val) => val.into_sql(), Value::Text(val) => val.as_deref().into_sql(), diff --git a/src/connector/mysql/conversion.rs b/src/connector/mysql/conversion.rs index 2dea3178..c953ea7a 100644 --- a/src/connector/mysql/conversion.rs +++ b/src/connector/mysql/conversion.rs @@ -23,6 +23,7 @@ pub fn conv_params(params: &[Value<'_>]) -> crate::Result { let res = match pv { Value::Int32(i) => i.map(|i| my::Value::Int(i as i64)), Value::Int64(i) => i.map(my::Value::Int), + Value::UnsignedInt32(u) => u.map(|u| my::Value::UInt(u as u64)), Value::Float(f) => f.map(my::Value::Float), Value::Double(f) => f.map(my::Value::Double), Value::Text(s) => s.clone().map(|s| my::Value::Bytes((&*s).as_bytes().to_vec())), @@ -110,7 +111,7 @@ impl TypeIdentifier for my::Column { let is_unsigned = self.flags().intersects(ColumnFlags::UNSIGNED_FLAG); - // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html#packet-ProtocolBinary + // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_binary_resultset.html // MYSQL_TYPE_TINY = i8 // MYSQL_TYPE_SHORT = i16 // MYSQL_TYPE_YEAR = i16 @@ -129,16 +130,20 @@ impl TypeIdentifier for my::Column { fn is_int64(&self) -> bool { use ColumnType::*; + // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_binary_resultset.html + // MYSQL_TYPE_LONGLONG = i64 + matches!(self.column_type(), MYSQL_TYPE_LONGLONG) + } + + fn is_uint32(&self) -> bool { + use ColumnType::*; + let is_unsigned = self.flags().intersects(ColumnFlags::UNSIGNED_FLAG); - // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html#packet-ProtocolBinary - // MYSQL_TYPE_LONGLONG = i64 - // UNSIGNED MYSQL_TYPE_LONG = u32 + // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_binary_resultset.html + // UNSIGNED MYSQL_TYPE_LONG = u32 // UNSIGNED MYSQL_TYPE_INT24 = u32 - matches!( - (self.column_type(), is_unsigned), - (MYSQL_TYPE_LONGLONG, _) | (MYSQL_TYPE_LONG, true) | (MYSQL_TYPE_INT24, true) - ) + is_unsigned && matches!(self.column_type(), MYSQL_TYPE_LONG | MYSQL_TYPE_INT24) } fn is_datetime(&self) -> bool { @@ -267,7 +272,9 @@ impl TakeRow for my::Row { my::Value::Bytes(b) if column.character_set() == 63 => Value::bytes(b), my::Value::Bytes(s) => Value::text(String::from_utf8(s)?), my::Value::Int(i) if column.is_int64() => Value::int64(i), + my::Value::Int(i) if column.is_uint32() => Value::uint32(i as u32), my::Value::Int(i) => Value::int32(i as i32), + my::Value::UInt(i) if column.is_uint32() => Value::uint32(u32::try_from(i)?), my::Value::UInt(i) => Value::int64(i64::try_from(i).map_err(|_| { let msg = "Unsigned integers larger than 9_223_372_036_854_775_807 are currently not handled."; let kind = ErrorKind::value_out_of_range(msg); @@ -315,6 +322,7 @@ impl TakeRow for my::Row { t if t.is_enum() => Value::Enum(None), t if t.is_null() => Value::Int32(None), t if t.is_int64() => Value::Int64(None), + t if t.is_uint32() => Value::UnsignedInt32(None), t if t.is_int32() => Value::Int32(None), t if t.is_float() => Value::Float(None), t if t.is_double() => Value::Double(None), diff --git a/src/connector/postgres/conversion.rs b/src/connector/postgres/conversion.rs index ff935349..f4cd92d8 100644 --- a/src/connector/postgres/conversion.rs +++ b/src/connector/postgres/conversion.rs @@ -42,6 +42,7 @@ pub(crate) fn params_to_types(params: &[Value<'_>]) -> Vec { match p { Value::Int32(_) => PostgresType::INT4, Value::Int64(_) => PostgresType::INT8, + Value::UnsignedInt32(_) => PostgresType::INT8, Value::Float(_) => PostgresType::FLOAT4, Value::Double(_) => PostgresType::FLOAT8, Value::Text(_) => PostgresType::TEXT, @@ -84,6 +85,7 @@ pub(crate) fn params_to_types(params: &[Value<'_>]) -> Vec { match first { Value::Int32(_) => PostgresType::INT4_ARRAY, Value::Int64(_) => PostgresType::INT8_ARRAY, + Value::UnsignedInt32(_) => PostgresType::INT8_ARRAY, Value::Float(_) => PostgresType::FLOAT4_ARRAY, Value::Double(_) => PostgresType::FLOAT8_ARRAY, Value::Text(_) => PostgresType::TEXT_ARRAY, @@ -406,7 +408,7 @@ impl GetRow for PostgresRow { PostgresType::OID_ARRAY => match row.try_get(i)? { Some(val) => { let val: Vec> = val; - let nums = val.into_iter().map(|oid| Value::Int64(oid.map(|oid| oid as i64))); + let nums = val.into_iter().map(Value::UnsignedInt32); Value::array(nums) } @@ -475,9 +477,9 @@ impl GetRow for PostgresRow { PostgresType::OID => match row.try_get(i)? { Some(val) => { let val: u32 = val; - Value::int64(val) + Value::uint32(val) } - None => Value::Int64(None), + None => Value::UnsignedInt32(None), }, PostgresType::CHAR => match row.try_get(i)? { Some(val) => { @@ -680,6 +682,39 @@ impl<'a> ToSql for Value<'a> { _ => None, }, (Value::Int64(integer), &PostgresType::INT8) => integer.map(|integer| (integer as i64).to_sql(ty, out)), + (Value::UnsignedInt32(integer), &PostgresType::INT2) => match integer { + Some(i) => { + let integer = i16::try_from(*i).map_err(|_| { + let kind = ErrorKind::conversion(format!( + "Unable to fit unsigned integer value '{}' into an INT2 (16-bit signed integer).", + i + )); + + Error::builder(kind).build() + })?; + + Some(integer.to_sql(ty, out)) + } + None => None, + }, + (Value::UnsignedInt32(integer), &PostgresType::INT4) => match integer { + Some(i) => { + let integer = i32::try_from(*i).map_err(|_| { + let kind = ErrorKind::conversion(format!( + "Unable to fit unsigned integer value '{}' into an INT2 (16-bit signed integer).", + i + )); + + Error::builder(kind).build() + })?; + + Some(integer.to_sql(ty, out)) + } + None => None, + }, + (Value::UnsignedInt32(integer), &PostgresType::INT8) => { + integer.map(|integer| (integer as i64).to_sql(ty, out)) + } #[cfg(feature = "bigdecimal")] (Value::Int32(integer), &PostgresType::NUMERIC) => integer .map(|integer| BigDecimal::from_i32(integer).unwrap()) @@ -728,6 +763,7 @@ impl<'a> ToSql for Value<'a> { }, (Value::Int32(integer), _) => integer.map(|integer| integer.to_sql(ty, out)), (Value::Int64(integer), _) => integer.map(|integer| integer.to_sql(ty, out)), + (Value::UnsignedInt32(integer), _) => integer.map(|integer| integer.to_sql(ty, out)), (Value::Float(float), &PostgresType::FLOAT8) => float.map(|float| (float as f64).to_sql(ty, out)), #[cfg(feature = "bigdecimal")] (Value::Float(float), &PostgresType::NUMERIC) => float diff --git a/src/connector/sqlite/conversion.rs b/src/connector/sqlite/conversion.rs index 6d94b6b1..55767d61 100644 --- a/src/connector/sqlite/conversion.rs +++ b/src/connector/sqlite/conversion.rs @@ -76,6 +76,10 @@ impl TypeIdentifier for Column<'_> { ) } + fn is_uint32(&self) -> bool { + false + } + fn is_datetime(&self) -> bool { matches!( self.decl_type(), @@ -253,6 +257,7 @@ impl<'a> ToSql for Value<'a> { let value = match self { Value::Int32(integer) => integer.map(ToSqlOutput::from), Value::Int64(integer) => integer.map(ToSqlOutput::from), + Value::UnsignedInt32(integer) => integer.map(|u| ToSqlOutput::from(u as i64)), Value::Float(float) => float.map(|f| f as f64).map(ToSqlOutput::from), Value::Double(double) => double.map(ToSqlOutput::from), Value::Text(cow) => cow.as_ref().map(|cow| ToSqlOutput::from(cow.as_ref())), diff --git a/src/connector/type_identifier.rs b/src/connector/type_identifier.rs index 9fcc46f6..ff5743e1 100644 --- a/src/connector/type_identifier.rs +++ b/src/connector/type_identifier.rs @@ -4,6 +4,7 @@ pub(crate) trait TypeIdentifier { fn is_double(&self) -> bool; fn is_int32(&self) -> bool; fn is_int64(&self) -> bool; + fn is_uint32(&self) -> bool; fn is_datetime(&self) -> bool; fn is_time(&self) -> bool; fn is_date(&self) -> bool; diff --git a/src/serde.rs b/src/serde.rs index 079f18fa..1881ac32 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -118,6 +118,8 @@ impl<'de> Deserializer<'de> for ValueDeserializer<'de> { Value::Int32(None) => visitor.visit_none(), Value::Int64(Some(i)) => visitor.visit_i64(i), Value::Int64(None) => visitor.visit_none(), + Value::UnsignedInt32(Some(i)) => visitor.visit_u32(i), + Value::UnsignedInt32(None) => visitor.visit_none(), Value::Boolean(Some(b)) => visitor.visit_bool(b), Value::Boolean(None) => visitor.visit_none(), Value::Char(Some(c)) => visitor.visit_char(c), diff --git a/src/tests/types/mssql.rs b/src/tests/types/mssql.rs index e90cef01..94ef3107 100644 --- a/src/tests/types/mssql.rs +++ b/src/tests/types/mssql.rs @@ -76,6 +76,14 @@ test_type!(bigint( Value::int64(i64::MAX), )); +test_type!(bigint_from_u32( + mssql, + "bigint", + (Value::UnsignedInt32(None), Value::Int64(None)), + (Value::uint32(u32::MIN), Value::int64(u32::MIN as i64)), + (Value::uint32(u32::MAX), Value::int64(u32::MAX as i64)), +)); + test_type!(float_24(mssql, "float(24)", Value::Float(None), Value::float(1.23456),)); test_type!(real(mssql, "real", Value::Float(None), Value::float(1.123456))); diff --git a/src/tests/types/mysql.rs b/src/tests/types/mysql.rs index 97a920c9..d23cf7f4 100644 --- a/src/tests/types/mysql.rs +++ b/src/tests/types/mysql.rs @@ -65,9 +65,9 @@ test_type!(mediumint( test_type!(mediumint_unsigned( mysql, "mediumint unsigned", - Value::Int64(None), - Value::int64(0), - Value::int64(16777215) + Value::UnsignedInt32(None), + Value::uint32(0u32), + Value::uint32(16777215u32) )); test_type!(int( @@ -81,18 +81,18 @@ test_type!(int( test_type!(int_unsigned( mysql, "int unsigned", - Value::Int64(None), - Value::int64(0), - Value::int64(2173158296i64), - Value::int64(4294967295i64) + Value::UnsignedInt32(None), + Value::uint32(0u32), + Value::uint32(2173158296u32), + Value::uint32(4294967295u32) )); test_type!(int_unsigned_not_null( mysql, "int unsigned not null", - Value::int64(0), - Value::int64(2173158296i64), - Value::int64(4294967295i64) + Value::uint32(0_u32), + Value::uint32(2173158296_u32), + Value::uint32(4294967295_u32) )); test_type!(bigint( diff --git a/src/tests/types/postgres.rs b/src/tests/types/postgres.rs index a098fa7a..c45cadd2 100644 --- a/src/tests/types/postgres.rs +++ b/src/tests/types/postgres.rs @@ -137,22 +137,28 @@ test_type!(float8_array( Value::array(vec![Value::double(1.1234), Value::double(4.321), Value::Double(None)]) )); -// NOTE: OIDs are unsigned 32-bit integers (see https://www.postgresql.org/docs/9.4/datatype-oid.html) -// but a u32 cannot fit in an i32, so we always read OIDs back from the database as i64s. test_type!(oid_with_i32( postgresql, "oid", - (Value::Int32(None), Value::Int64(None)), - (Value::int32(i32::MAX), Value::int64(i32::MAX)), - (Value::int32(u32::MIN as i32), Value::int64(u32::MIN)), + (Value::Int32(None), Value::UnsignedInt32(None)), + (Value::int32(i32::MAX), Value::uint32(i32::MAX as u32)), + (Value::int32(u32::MIN as i32), Value::uint32(u32::MIN)), +)); + +test_type!(oid_with_u32( + postgresql, + "oid", + Value::UnsignedInt32(None), + Value::uint32(u32::MAX), + Value::uint32(u32::MIN), )); test_type!(oid_with_i64( postgresql, "oid", - Value::Int64(None), - Value::int64(u32::MAX), - Value::int64(u32::MIN), + (Value::Int64(None), Value::UnsignedInt32(None)), + (Value::int64(u32::MAX), Value::uint32(u32::MAX)), + (Value::int64(u32::MIN), Value::uint32(u32::MIN)), )); test_type!(oid_array( @@ -160,10 +166,10 @@ test_type!(oid_array( "oid[]", Value::Array(None), Value::array(vec![ - Value::int64(1), - Value::int64(2), - Value::int64(3), - Value::Int64(None) + Value::uint32(1_u32), + Value::uint32(2_u32), + Value::uint32(3_u32), + Value::UnsignedInt32(None) ]), )); diff --git a/src/visitor/mssql.rs b/src/visitor/mssql.rs index 19f65f29..0529e992 100644 --- a/src/visitor/mssql.rs +++ b/src/visitor/mssql.rs @@ -313,6 +313,7 @@ impl<'a> Visitor<'a> for Mssql<'a> { let res = match value { Value::Int32(i) => i.map(|i| self.write(i)), Value::Int64(i) => i.map(|i| self.write(i)), + Value::UnsignedInt32(i) => i.map(|i| self.write(i)), Value::Float(d) => d.map(|f| match f { f if f.is_nan() => self.write("'NaN'"), f if f == f32::INFINITY => self.write("'Infinity'"), diff --git a/src/visitor/mysql.rs b/src/visitor/mysql.rs index 493bc549..3b5f1c1c 100644 --- a/src/visitor/mysql.rs +++ b/src/visitor/mysql.rs @@ -125,6 +125,7 @@ impl<'a> Visitor<'a> for Mysql<'a> { let res = match value { Value::Int32(i) => i.map(|i| self.write(i)), Value::Int64(i) => i.map(|i| self.write(i)), + Value::UnsignedInt32(i) => i.map(|i| self.write(i)), Value::Float(d) => d.map(|f| match f { f if f.is_nan() => self.write("'NaN'"), f if f == f32::INFINITY => self.write("'Infinity'"), diff --git a/src/visitor/postgres.rs b/src/visitor/postgres.rs index 88a04a37..52cfe019 100644 --- a/src/visitor/postgres.rs +++ b/src/visitor/postgres.rs @@ -72,6 +72,7 @@ impl<'a> Visitor<'a> for Postgres<'a> { let res = match value { Value::Int32(i) => i.map(|i| self.write(i)), Value::Int64(i) => i.map(|i| self.write(i)), + Value::UnsignedInt32(i) => i.map(|i| self.write(i)), Value::Text(t) => t.map(|t| self.write(format!("'{}'", t))), Value::Enum(e) => e.map(|e| self.write(e)), Value::Bytes(b) => b.map(|b| self.write(format!("E'{}'", hex::encode(b)))), diff --git a/src/visitor/sqlite.rs b/src/visitor/sqlite.rs index 3783a826..dc221495 100644 --- a/src/visitor/sqlite.rs +++ b/src/visitor/sqlite.rs @@ -77,6 +77,7 @@ impl<'a> Visitor<'a> for Sqlite<'a> { let res = match value { Value::Int32(i) => i.map(|i| self.write(i)), Value::Int64(i) => i.map(|i| self.write(i)), + Value::UnsignedInt32(i) => i.map(|i| self.write(i)), Value::Text(t) => t.map(|t| self.write(format!("'{}'", t))), Value::Enum(e) => e.map(|e| self.write(e)), Value::Bytes(b) => b.map(|b| self.write(format!("x'{}'", hex::encode(b)))),