use linkrs_core::{DataType, Value};
#[derive(Debug, Clone)]
pub struct ConversionError {
pub message: String,
pub step_index: Option<usize>,
pub source_type: Option<String>,
pub target_type: Option<String>,
}
impl ConversionError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
step_index: None,
source_type: None,
target_type: None,
}
}
pub fn with_step(mut self, idx: usize) -> Self {
self.step_index = Some(idx);
self
}
pub fn with_types(mut self, source: impl Into<String>, target: impl Into<String>) -> Self {
self.source_type = Some(source.into());
self.target_type = Some(target.into());
self
}
}
impl std::fmt::Display for ConversionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Conversion error: {}", self.message)?;
if let Some(idx) = self.step_index {
write!(f, " [step {}]", idx + 1)?;
}
if let (Some(src), Some(tgt)) = (&self.source_type, &self.target_type) {
write!(f, " ({} -> {})", src, tgt)?;
} else if let Some(tgt) = &self.target_type {
write!(f, " (target {})", tgt)?;
}
Ok(())
}
}
impl std::error::Error for ConversionError {}
macro_rules! conversion_err {
($($arg:tt)*) => {
ConversionError::new(format!($($arg)*))
};
}
fn type_name(dt: &DataType) -> &'static str {
match dt {
DataType::Empty => "EMPTY",
DataType::Null => "NULL",
DataType::Bool => "BOOL",
DataType::SmallInt => "SMALLINT",
DataType::Int => "INT",
DataType::BigInt => "BIGINT",
DataType::Float => "FLOAT",
DataType::Double => "DOUBLE",
DataType::Decimal128 => "DECIMAL",
DataType::String => "STRING",
DataType::Date => "DATE",
DataType::Time => "TIME",
DataType::DateTime => "DATETIME",
DataType::Blob => "BLOB",
DataType::Uuid => "UUID",
DataType::Interval => "INTERVAL",
DataType::Json => "JSON",
DataType::JsonB => "JSONB",
DataType::FixedString(_) => "FIXED_STRING",
_ => "COMPLEX",
}
}
pub fn convert_value(value: &Value, target_type: &DataType) -> Result<Value, ConversionError> {
if value.is_null() || value.is_empty() {
return Ok(Value::Null(linkrs_core::value::null::NullType::Null));
}
let source_type = value.get_type();
if source_type == *target_type {
return Ok(value.clone());
}
match (value, target_type) {
(Value::SmallInt(i), DataType::Int) => Ok(Value::Int(*i as i32)),
(Value::SmallInt(i), DataType::BigInt) => Ok(Value::BigInt(*i as i64)),
(Value::SmallInt(i), DataType::Float) => Ok(Value::Float(*i as f32)),
(Value::SmallInt(i), DataType::Double) => Ok(Value::Double(*i as f64)),
(Value::SmallInt(i), DataType::String) => Ok(Value::string(i.to_string())),
(Value::Int(i), DataType::BigInt) => Ok(Value::BigInt(*i as i64)),
(Value::Int(i), DataType::Float) => Ok(Value::Float(*i as f32)),
(Value::Int(i), DataType::Double) => Ok(Value::Double(*i as f64)),
(Value::Int(i), DataType::SmallInt) => {
let v = *i;
if v < i16::MIN as i32 || v > i16::MAX as i32 {
return Err(conversion_err!("Value {} out of range for SMALLINT", v));
}
Ok(Value::SmallInt(v as i16))
}
(Value::Int(i), DataType::String) => Ok(Value::string(i.to_string())),
(Value::BigInt(i), DataType::Int) => {
let v = *i;
if v < i32::MIN as i64 || v > i32::MAX as i64 {
return Err(conversion_err!("Value {} out of range for INT", v));
}
Ok(Value::Int(v as i32))
}
(Value::BigInt(i), DataType::SmallInt) => {
let v = *i;
if v < i16::MIN as i64 || v > i16::MAX as i64 {
return Err(conversion_err!("Value {} out of range for SMALLINT", v));
}
Ok(Value::SmallInt(v as i16))
}
(Value::BigInt(i), DataType::Float) => Ok(Value::Float(*i as f32)),
(Value::BigInt(i), DataType::Double) => Ok(Value::Double(*i as f64)),
(Value::BigInt(i), DataType::String) => Ok(Value::string(i.to_string())),
(Value::Float(f), DataType::Double) => Ok(Value::Double(*f as f64)),
(Value::Float(f), DataType::String) => Ok(Value::string(f.to_string())),
(Value::Double(d), DataType::Float) => Ok(Value::Float(*d as f32)),
(Value::Double(d), DataType::String) => Ok(Value::string(d.to_string())),
(Value::String(s), DataType::SmallInt) => s
.parse::<i16>()
.map(Value::SmallInt)
.map_err(|e| conversion_err!("Cannot parse '{}' as SMALLINT: {}", s, e)),
(Value::String(s), DataType::Int) => s
.parse::<i32>()
.map(Value::Int)
.map_err(|e| conversion_err!("Cannot parse '{}' as INT: {}", s, e)),
(Value::String(s), DataType::BigInt) => s
.parse::<i64>()
.map(Value::BigInt)
.map_err(|e| conversion_err!("Cannot parse '{}' as BIGINT: {}", s, e)),
(Value::String(s), DataType::Float) => s
.parse::<f32>()
.map(Value::Float)
.map_err(|e| conversion_err!("Cannot parse '{}' as FLOAT: {}", s, e)),
(Value::String(s), DataType::Double) => s
.parse::<f64>()
.map(Value::Double)
.map_err(|e| conversion_err!("Cannot parse '{}' as DOUBLE: {}", s, e)),
(Value::String(s), DataType::Bool) => match s.to_lowercase().as_str() {
"true" | "yes" | "1" => Ok(Value::Bool(true)),
"false" | "no" | "0" => Ok(Value::Bool(false)),
_ => Err(conversion_err!("Cannot parse '{}' as BOOL", s)),
},
(_, DataType::String) => Ok(Value::string(format!("{}", value))),
_ => Err(conversion_err!(
"Unsupported conversion from {:?} to {}",
value.get_type(),
type_name(target_type),
)),
}
}
pub fn is_compatible_type(from: &DataType, to: &DataType) -> bool {
if from == to {
return true;
}
match (from, to) {
(
DataType::SmallInt,
DataType::Int
| DataType::BigInt
| DataType::Float
| DataType::Double
| DataType::String,
) => true,
(
DataType::Int,
DataType::SmallInt
| DataType::BigInt
| DataType::Float
| DataType::Double
| DataType::String,
) => true,
(
DataType::BigInt,
DataType::Int
| DataType::SmallInt
| DataType::Float
| DataType::Double
| DataType::String,
) => true,
(DataType::Float, DataType::Double | DataType::String) => true,
(DataType::Double, DataType::Float | DataType::String) => true,
(
DataType::String,
DataType::SmallInt
| DataType::Int
| DataType::BigInt
| DataType::Float
| DataType::Double
| DataType::Bool,
) => true,
(_, DataType::String) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use linkrs_core::value::null::NullType;
#[test]
fn test_identity_conversion() {
assert_eq!(
convert_value(&Value::Int(42), &DataType::Int).unwrap(),
Value::Int(42)
);
assert_eq!(
convert_value(&Value::string("hello"), &DataType::String).unwrap(),
Value::string("hello")
);
}
#[test]
fn test_int_to_bigint() {
assert_eq!(
convert_value(&Value::Int(42), &DataType::BigInt).unwrap(),
Value::BigInt(42)
);
}
#[test]
fn test_smallint_to_int() {
assert_eq!(
convert_value(&Value::SmallInt(100), &DataType::Int).unwrap(),
Value::Int(100)
);
}
#[test]
fn test_int_to_smallint_ok() {
assert_eq!(
convert_value(&Value::Int(42), &DataType::SmallInt).unwrap(),
Value::SmallInt(42)
);
}
#[test]
fn test_int_to_smallint_overflow() {
let result = convert_value(&Value::Int(99999), &DataType::SmallInt);
assert!(result.is_err());
}
#[test]
fn test_bigint_to_int_ok() {
assert_eq!(
convert_value(&Value::BigInt(42), &DataType::Int).unwrap(),
Value::Int(42)
);
}
#[test]
fn test_bigint_to_int_overflow() {
let result = convert_value(&Value::BigInt(9999999999i64), &DataType::Int);
assert!(result.is_err());
}
#[test]
fn test_float_to_double() {
let result = convert_value(&Value::Float(3.5), &DataType::Double).unwrap();
match result {
Value::Double(d) => assert!((d - 3.5_f64).abs() < 1e-6),
_ => panic!("Expected Double"),
}
}
#[test]
fn test_string_to_int() {
assert_eq!(
convert_value(&Value::string("42"), &DataType::Int).unwrap(),
Value::Int(42)
);
}
#[test]
fn test_string_to_bool() {
assert_eq!(
convert_value(&Value::string("true"), &DataType::Bool).unwrap(),
Value::Bool(true)
);
assert_eq!(
convert_value(&Value::string("false"), &DataType::Bool).unwrap(),
Value::Bool(false)
);
}
#[test]
fn test_string_to_bool_invalid() {
let result = convert_value(&Value::string("maybe"), &DataType::Bool);
assert!(result.is_err());
}
#[test]
fn test_null_conversion() {
let result = convert_value(&Value::Null(NullType::Null), &DataType::String).unwrap();
assert!(result.is_null());
}
#[test]
fn test_value_to_string() {
assert_eq!(
convert_value(&Value::Int(42), &DataType::String).unwrap(),
Value::string("42")
);
assert_eq!(
convert_value(&Value::Bool(true), &DataType::String).unwrap(),
Value::string("true")
);
}
#[test]
fn test_unsupported_conversion() {
let result = convert_value(&Value::Int(42), &DataType::Date);
assert!(result.is_err());
}
}