use std::fmt::{self, Display, Formatter};
use sqlx_core::error::BoxDynError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TypeInfo {
Null,
Integer,
Real,
Text,
Blob,
}
impl Display for TypeInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.pad(sqlx_core::type_info::TypeInfo::name(self))
}
}
impl sqlx_core::type_info::TypeInfo for TypeInfo {
fn is_null(&self) -> bool {
matches!(self, TypeInfo::Null)
}
fn name(&self) -> &str {
match self {
TypeInfo::Null => "NULL",
TypeInfo::Integer => "INTEGER",
TypeInfo::Real => "REAL",
TypeInfo::Text => "TEXT",
TypeInfo::Blob => "BLOB",
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Value {
Null,
Integer(i64),
Real(f64),
Text(String),
Blob(Vec<u8>),
}
pub const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1;
pub fn safe_integer(value: i64) -> Result<i64, BoxDynError> {
if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) {
Ok(value)
} else {
Err(format!(
"{value} is outside ±(2^53 − 1), the integers a JavaScript number holds exactly: \
it would be stored as a different integer"
)
.into())
}
}
impl Value {
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
reason = "the range check makes both casts exact"
)]
pub fn from_number(value: f64) -> Self {
if value.fract() == 0.0 && value.abs() <= MAX_SAFE_INTEGER as f64 {
Value::Integer(value as i64)
} else {
Value::Real(value)
}
}
#[must_use]
pub fn type_info(&self) -> TypeInfo {
match self {
Value::Null => TypeInfo::Null,
Value::Integer(_) => TypeInfo::Integer,
Value::Real(_) => TypeInfo::Real,
Value::Text(_) => TypeInfo::Text,
Value::Blob(_) => TypeInfo::Blob,
}
}
#[must_use]
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn int(&self) -> Result<i64, BoxDynError> {
match *self {
Value::Integer(value) => Ok(value),
ref other => Err(mismatch("an INTEGER", other)),
}
}
#[allow(
clippy::cast_precision_loss,
reason = "an INTEGER here is within ±(2^53 − 1), so it converts exactly"
)]
pub fn real(&self) -> Result<f64, BoxDynError> {
match *self {
Value::Real(value) => Ok(value),
Value::Integer(value) => Ok(value as f64),
ref other => Err(mismatch("a REAL", other)),
}
}
pub fn text(&self) -> Result<&str, BoxDynError> {
match self {
Value::Text(value) => Ok(value),
other => Err(mismatch("TEXT", other)),
}
}
pub fn blob(&self) -> Result<&[u8], BoxDynError> {
match self {
Value::Blob(value) => Ok(value),
Value::Text(value) => Ok(value.as_bytes()),
other => Err(mismatch("a BLOB", other)),
}
}
}
fn mismatch(expected: &str, found: &Value) -> BoxDynError {
format!("expected {expected}, found {}", found.type_info()).into()
}
pub trait AsValue<'r> {
fn as_value(&self) -> &'r Value;
}
#[cfg(test)]
mod tests {
use super::{safe_integer, Value, MAX_SAFE_INTEGER};
#[test]
fn whole_numbers_in_the_safe_range_are_integers() {
assert_eq!(Value::from_number(3.0), Value::Integer(3));
assert_eq!(Value::from_number(-0.0), Value::Integer(0));
assert_eq!(
Value::from_number(9_007_199_254_740_991.0),
Value::Integer(MAX_SAFE_INTEGER)
);
}
#[test]
fn everything_else_is_real() {
assert_eq!(Value::from_number(1.5), Value::Real(1.5));
assert_eq!(
Value::from_number(9_007_199_254_740_992.0),
Value::Real(9_007_199_254_740_992.0)
);
assert!(matches!(Value::from_number(f64::INFINITY), Value::Real(_)));
assert!(matches!(Value::from_number(f64::NAN), Value::Real(_)));
}
#[test]
fn safe_integer_accepts_exactly_the_safe_range() {
assert!(safe_integer(MAX_SAFE_INTEGER).is_ok());
assert!(safe_integer(-MAX_SAFE_INTEGER).is_ok());
assert!(safe_integer(MAX_SAFE_INTEGER + 1).is_err());
assert!(safe_integer(-MAX_SAFE_INTEGER - 1).is_err());
assert!(safe_integer(i64::MIN).is_err());
}
#[test]
fn accessors_accept_what_their_types_are_compatible_with() {
assert_eq!(Value::Integer(2).int().unwrap(), 2);
assert!(Value::Real(2.0).int().is_err());
assert!((Value::Integer(2).real().unwrap() - 2.0).abs() < f64::EPSILON);
assert_eq!(Value::Text("ab".into()).blob().unwrap(), b"ab");
assert!(Value::Blob(b"ab".to_vec()).text().is_err());
assert!(Value::Null.int().is_err());
}
}