use mssql_types::{SqlValue, ToSql, TvpColumnDef, TvpColumnType, TvpData, TypeError};
#[derive(Debug, Clone)]
pub struct TvpColumn {
pub name: String,
pub sql_type: String,
pub ordinal: usize,
}
impl TvpColumn {
pub fn new<S: Into<String>>(name: S, sql_type: S, ordinal: usize) -> Self {
Self {
name: name.into(),
sql_type: sql_type.into(),
ordinal,
}
}
}
#[derive(Debug, Clone)]
pub struct TvpRow {
pub values: Vec<SqlValue>,
}
impl TvpRow {
pub fn new(values: Vec<SqlValue>) -> Self {
Self { values }
}
pub fn get(&self, index: usize) -> Option<&SqlValue> {
self.values.get(index)
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
pub trait Tvp {
fn type_name() -> &'static str;
fn columns() -> Vec<TvpColumn>;
fn to_row(&self) -> Result<TvpRow, TypeError>;
}
#[derive(Debug, Clone)]
pub struct TvpValue {
pub type_name: String,
pub columns: Vec<TvpColumn>,
pub rows: Vec<TvpRow>,
}
impl TvpValue {
pub fn new<T: Tvp>(items: &[T]) -> Result<Self, TypeError> {
let rows: Result<Vec<TvpRow>, TypeError> = items.iter().map(|item| item.to_row()).collect();
Ok(Self {
type_name: T::type_name().to_string(),
columns: T::columns(),
rows: rows?,
})
}
pub fn empty<T: Tvp>() -> Self {
Self {
type_name: T::type_name().to_string(),
columns: T::columns(),
rows: Vec::new(),
}
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &TvpRow> {
self.rows.iter()
}
}
impl ToSql for TvpValue {
fn to_sql(&self) -> Result<SqlValue, TypeError> {
let (schema, type_name) = if let Some(dot_pos) = self.type_name.find('.') {
(
self.type_name[..dot_pos].to_string(),
self.type_name[dot_pos + 1..].to_string(),
)
} else {
(String::new(), self.type_name.clone())
};
let columns: Vec<TvpColumnDef> = self
.columns
.iter()
.map(|col| {
let column_type = TvpColumnType::from_sql_type(&col.sql_type).ok_or_else(|| {
TypeError::UnsupportedConversion {
from: col.sql_type.clone(),
to: "TvpColumnType",
}
})?;
Ok(TvpColumnDef::nullable(column_type))
})
.collect::<Result<Vec<_>, TypeError>>()?;
let rows: Vec<Vec<SqlValue>> = self.rows.iter().map(|row| row.values.clone()).collect();
let tvp_data = TvpData {
schema,
type_name,
columns,
rows,
};
Ok(SqlValue::Tvp(Box::new(tvp_data)))
}
fn sql_type(&self) -> &'static str {
"TVP"
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
struct TestUserId {
user_id: i32,
}
impl Tvp for TestUserId {
fn type_name() -> &'static str {
"dbo.UserIdList"
}
fn columns() -> Vec<TvpColumn> {
vec![TvpColumn::new("UserId", "INT", 0)]
}
fn to_row(&self) -> Result<TvpRow, TypeError> {
Ok(TvpRow::new(vec![self.user_id.to_sql()?]))
}
}
#[test]
fn test_tvp_trait_impl() {
assert_eq!(TestUserId::type_name(), "dbo.UserIdList");
let columns = TestUserId::columns();
assert_eq!(columns.len(), 1);
assert_eq!(columns[0].name, "UserId");
assert_eq!(columns[0].sql_type, "INT");
}
#[test]
fn test_tvp_row_creation() {
let item = TestUserId { user_id: 42 };
let row = item.to_row().unwrap();
assert_eq!(row.len(), 1);
assert!(matches!(row.get(0), Some(SqlValue::Int(42))));
}
#[test]
fn test_tvp_value_creation() {
let items = vec![
TestUserId { user_id: 1 },
TestUserId { user_id: 2 },
TestUserId { user_id: 3 },
];
let tvp = TvpValue::new(&items).unwrap();
assert_eq!(tvp.type_name, "dbo.UserIdList");
assert_eq!(tvp.columns.len(), 1);
assert_eq!(tvp.len(), 3);
}
#[test]
fn test_tvp_value_empty() {
let tvp: TvpValue = TvpValue::empty::<TestUserId>();
assert_eq!(tvp.type_name, "dbo.UserIdList");
assert!(tvp.is_empty());
}
#[test]
fn test_tvp_column() {
let col = TvpColumn::new("TestCol", "NVARCHAR(100)", 0);
assert_eq!(col.name, "TestCol");
assert_eq!(col.sql_type, "NVARCHAR(100)");
assert_eq!(col.ordinal, 0);
}
}