use std::borrow::Cow;
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use narwhal_core::{ColumnHeader, Error, Result, Value};
use tiberius::numeric::Numeric;
use tiberius::xml::XmlData;
use tiberius::{Column, ColumnData, ColumnType, IntoSql, Row, ToSql};
pub(crate) struct Param<'a>(pub &'a Value);
impl ToSql for Param<'_> {
fn to_sql(&self) -> ColumnData<'_> {
match self.0 {
Value::Null => ColumnData::String(None),
Value::Bool(v) => ColumnData::Bit(Some(*v)),
Value::Int(v) => ColumnData::I64(Some(*v)),
Value::Float(v) => ColumnData::F64(Some(*v)),
Value::String(v) => ColumnData::String(Some(Cow::Borrowed(v.as_str()))),
Value::Bytes(v) => ColumnData::Binary(Some(Cow::Borrowed(v.as_slice()))),
Value::Date(v) => v.into_sql(),
Value::Time(v) => v.into_sql(),
Value::DateTime(v) => v.into_sql(),
Value::Timestamp(v) => v.into_sql(),
Value::Uuid(v) => ColumnData::Guid(Some(*v)),
Value::Json(v) => ColumnData::String(Some(Cow::Owned(v.to_string()))),
Value::Unknown(v) => ColumnData::String(Some(Cow::Borrowed(v.as_str()))),
other => ColumnData::String(Some(Cow::Owned(format!("{other:?}")))),
}
}
}
pub(crate) fn column_to_value(row: &Row, idx: usize, ty: ColumnType) -> Result<Value> {
macro_rules! get {
($t:ty, $map:expr) => {{
match row.try_get::<$t, _>(idx) {
Ok(Some(v)) => Ok($map(v)),
Ok(None) => Ok(Value::Null),
Err(error) => Err(Error::Query(error.to_string())),
}
}};
}
match ty {
ColumnType::Bit | ColumnType::Bitn => get!(bool, Value::Bool),
ColumnType::Int1 => get!(u8, |v| Value::Int(i64::from(v))),
ColumnType::Int2 => get!(i16, |v| Value::Int(i64::from(v))),
ColumnType::Int4 => get!(i32, |v| Value::Int(i64::from(v))),
ColumnType::Int8 => get!(i64, Value::Int),
ColumnType::Intn => match row.try_get::<i64, _>(idx) {
Ok(Some(v)) => Ok(Value::Int(v)),
Ok(None) => Ok(Value::Null),
Err(_) => match row.try_get::<i32, _>(idx) {
Ok(Some(v)) => Ok(Value::Int(i64::from(v))),
Ok(None) => Ok(Value::Null),
Err(_) => match row.try_get::<i16, _>(idx) {
Ok(Some(v)) => Ok(Value::Int(i64::from(v))),
Ok(None) => Ok(Value::Null),
Err(_) => get!(u8, |v: u8| Value::Int(i64::from(v))),
},
},
},
ColumnType::Float4 => get!(f32, |v| Value::Float(f64::from(v))),
ColumnType::Float8 => get!(f64, Value::Float),
ColumnType::Floatn => match row.try_get::<f64, _>(idx) {
Ok(Some(v)) => Ok(Value::Float(v)),
Ok(None) => Ok(Value::Null),
Err(_) => get!(f32, |v: f32| Value::Float(f64::from(v))),
},
ColumnType::Money | ColumnType::Money4 => {
get!(Numeric, |v: Numeric| Value::String(v.to_string()))
}
ColumnType::Decimaln | ColumnType::Numericn => {
get!(Numeric, |v: Numeric| Value::String(v.to_string()))
}
ColumnType::Guid => get!(uuid::Uuid, Value::Uuid),
ColumnType::BigChar
| ColumnType::BigVarChar
| ColumnType::Text
| ColumnType::NChar
| ColumnType::NVarchar
| ColumnType::NText => get!(&str, |s: &str| Value::String(s.to_owned())),
ColumnType::BigBinary | ColumnType::BigVarBin | ColumnType::Image => {
get!(&[u8], |b: &[u8]| Value::Bytes(b.to_vec()))
}
ColumnType::Datetime | ColumnType::Datetime4 | ColumnType::Datetimen => {
get!(NaiveDateTime, Value::DateTime)
}
ColumnType::Datetime2 => get!(NaiveDateTime, Value::DateTime),
ColumnType::DatetimeOffsetn => get!(DateTime<Utc>, Value::Timestamp),
ColumnType::Daten => get!(NaiveDate, Value::Date),
ColumnType::Timen => get!(NaiveTime, Value::Time),
ColumnType::Xml => get!(&XmlData, |x: &XmlData| Value::String(x.to_string())),
_ => match row.try_get::<&str, _>(idx) {
Ok(Some(s)) => Ok(Value::Unknown(s.to_owned())),
Ok(None) => Ok(Value::Null),
Err(_) => Ok(Value::Unknown(format!("<{}>", column_type_name(ty)))),
},
}
}
pub(crate) fn column_header(column: &Column) -> ColumnHeader {
ColumnHeader {
name: column.name().to_owned(),
data_type: column_type_name(column.column_type()),
}
}
#[allow(unreachable_patterns)]
pub(crate) fn column_type_name(ty: ColumnType) -> String {
let name = match ty {
ColumnType::Null => "null",
ColumnType::Bit | ColumnType::Bitn => "bit",
ColumnType::Int1 => "tinyint",
ColumnType::Int2 => "smallint",
ColumnType::Int4 => "int",
ColumnType::Int8 => "bigint",
ColumnType::Intn => "int",
ColumnType::Float4 => "real",
ColumnType::Float8 => "float",
ColumnType::Floatn => "float",
ColumnType::Money | ColumnType::Money4 => "money",
ColumnType::Datetime | ColumnType::Datetime4 | ColumnType::Datetimen => "datetime",
ColumnType::Datetime2 => "datetime2",
ColumnType::DatetimeOffsetn => "datetimeoffset",
ColumnType::Daten => "date",
ColumnType::Timen => "time",
ColumnType::Guid => "uniqueidentifier",
ColumnType::Decimaln | ColumnType::Numericn => "decimal",
ColumnType::BigChar | ColumnType::BigVarChar | ColumnType::Text => "varchar",
ColumnType::NChar | ColumnType::NVarchar | ColumnType::NText => "nvarchar",
ColumnType::BigBinary | ColumnType::BigVarBin | ColumnType::Image => "varbinary",
ColumnType::Udt => "udt",
ColumnType::Xml => "xml",
ColumnType::SSVariant => "sql_variant",
_ => "unknown",
};
name.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn column_type_name_covers_common_types() {
assert_eq!(column_type_name(ColumnType::Int4), "int");
assert_eq!(column_type_name(ColumnType::NVarchar), "nvarchar");
assert_eq!(column_type_name(ColumnType::Guid), "uniqueidentifier");
assert_eq!(column_type_name(ColumnType::Datetime2), "datetime2");
assert_eq!(column_type_name(ColumnType::Decimaln), "decimal");
assert_eq!(column_type_name(ColumnType::BigVarBin), "varbinary");
}
#[test]
fn param_binds_basic_values() {
let v = Value::String("hello".into());
match Param(&v).to_sql() {
ColumnData::String(Some(c)) => assert_eq!(c.as_ref(), "hello"),
other => panic!("unexpected {other:?}"),
}
let v = Value::Int(42);
assert!(matches!(Param(&v).to_sql(), ColumnData::I64(Some(42))));
let v = Value::Bool(true);
assert!(matches!(Param(&v).to_sql(), ColumnData::Bit(Some(true))));
let v = Value::Null;
assert!(matches!(Param(&v).to_sql(), ColumnData::String(None)));
let v = Value::Bytes(vec![1, 2, 3]);
match Param(&v).to_sql() {
ColumnData::Binary(Some(b)) => assert_eq!(&*b, &[1, 2, 3][..]),
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn param_uuid_binds_as_guid() {
let uuid = uuid::Uuid::new_v4();
let v = Value::Uuid(uuid);
match Param(&v).to_sql() {
ColumnData::Guid(Some(g)) => assert_eq!(g, uuid),
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn param_json_renders_as_text() {
let value = Value::Json("42".parse().expect("json number parses"));
match Param(&value).to_sql() {
ColumnData::String(Some(c)) => assert_eq!(c.as_ref(), "42"),
other => panic!("unexpected {other:?}"),
}
}
}