mssql_client/
tvp.rs

1//! Table-Valued Parameters (TVP) support.
2//!
3//! TVPs allow passing collections of structured data to SQL Server stored procedures
4//! as a parameter. This is more efficient than:
5//! - Multiple INSERT statements
6//! - String concatenation of values
7//! - Temporary tables
8//!
9//! ## Usage
10//!
11//! First, create a table type in SQL Server:
12//!
13//! ```sql
14//! CREATE TYPE dbo.UserIdList AS TABLE (
15//!     UserId INT NOT NULL
16//! );
17//! ```
18//!
19//! Then use the `#[derive(Tvp)]` macro:
20//!
21//! ```rust,ignore
22//! use mssql_derive::Tvp;
23//!
24//! #[derive(Tvp)]
25//! #[mssql(type_name = "dbo.UserIdList")]
26//! struct UserIdList {
27//!     user_id: i32,
28//! }
29//!
30//! // Create a collection of rows
31//! let user_ids = vec![
32//!     UserIdList { user_id: 1 },
33//!     UserIdList { user_id: 2 },
34//!     UserIdList { user_id: 3 },
35//! ];
36//!
37//! // Pass to stored procedure
38//! client.execute(
39//!     "EXEC GetUserDetails @UserIds = @user_ids",
40//!     &[&TvpValue::new(&user_ids)?],
41//! ).await?;
42//! ```
43//!
44//! ## Supported Attributes
45//!
46//! - `#[mssql(type_name = "schema.TypeName")]` - SQL Server TVP type name (required)
47//! - `#[mssql(rename = "column_name")]` - Map field to different column name
48
49use mssql_types::{SqlValue, ToSql, TvpColumnDef, TvpColumnType, TvpData, TypeError};
50
51/// Metadata for a TVP column.
52#[derive(Debug, Clone)]
53pub struct TvpColumn {
54    /// Column name.
55    pub name: String,
56    /// SQL type name (e.g., "INT", "NVARCHAR(100)").
57    pub sql_type: String,
58    /// Column ordinal (0-based).
59    pub ordinal: usize,
60}
61
62impl TvpColumn {
63    /// Create a new TVP column definition.
64    pub fn new<S: Into<String>>(name: S, sql_type: S, ordinal: usize) -> Self {
65        Self {
66            name: name.into(),
67            sql_type: sql_type.into(),
68            ordinal,
69        }
70    }
71}
72
73/// A row in a table-valued parameter.
74#[derive(Debug, Clone)]
75pub struct TvpRow {
76    /// Values for each column.
77    pub values: Vec<SqlValue>,
78}
79
80impl TvpRow {
81    /// Create a new TVP row from values.
82    pub fn new(values: Vec<SqlValue>) -> Self {
83        Self { values }
84    }
85
86    /// Get the value at the given index.
87    pub fn get(&self, index: usize) -> Option<&SqlValue> {
88        self.values.get(index)
89    }
90
91    /// Get the number of columns in this row.
92    pub fn len(&self) -> usize {
93        self.values.len()
94    }
95
96    /// Check if the row is empty.
97    pub fn is_empty(&self) -> bool {
98        self.values.is_empty()
99    }
100}
101
102/// Trait for types that can be used as table-valued parameters.
103///
104/// This trait is typically implemented via the `#[derive(Tvp)]` macro.
105///
106/// # Example
107///
108/// ```rust,ignore
109/// use mssql_client::{Tvp, TvpColumn, TvpRow};
110/// use mssql_types::{SqlValue, TypeError, ToSql};
111///
112/// struct UserId {
113///     user_id: i32,
114/// }
115///
116/// impl Tvp for UserId {
117///     fn type_name() -> &'static str {
118///         "dbo.UserIdList"
119///     }
120///
121///     fn columns() -> Vec<TvpColumn> {
122///         vec![TvpColumn::new("UserId", "INT", 0)]
123///     }
124///
125///     fn to_row(&self) -> Result<TvpRow, TypeError> {
126///         Ok(TvpRow::new(vec![self.user_id.to_sql()?]))
127///     }
128/// }
129/// ```
130pub trait Tvp {
131    /// Get the SQL Server type name for this TVP.
132    ///
133    /// This must match a user-defined table type in the database.
134    fn type_name() -> &'static str;
135
136    /// Get the column definitions for this TVP.
137    fn columns() -> Vec<TvpColumn>;
138
139    /// Convert this struct to a TVP row.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if any field value cannot be converted to a SQL value.
144    fn to_row(&self) -> Result<TvpRow, TypeError>;
145}
146
147/// A table-valued parameter value that can be passed to a stored procedure.
148///
149/// This wraps a collection of `Tvp` items and provides the necessary metadata
150/// for the TDS protocol.
151#[derive(Debug, Clone)]
152pub struct TvpValue {
153    /// The SQL Server type name.
154    pub type_name: String,
155    /// Column definitions.
156    pub columns: Vec<TvpColumn>,
157    /// The rows of data.
158    pub rows: Vec<TvpRow>,
159}
160
161impl TvpValue {
162    /// Create a TVP value from a slice of items implementing `Tvp`.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if any item cannot be converted to a row.
167    pub fn new<T: Tvp>(items: &[T]) -> Result<Self, TypeError> {
168        let rows: Result<Vec<TvpRow>, TypeError> = items.iter().map(|item| item.to_row()).collect();
169
170        Ok(Self {
171            type_name: T::type_name().to_string(),
172            columns: T::columns(),
173            rows: rows?,
174        })
175    }
176
177    /// Create an empty TVP value with the given type name and columns.
178    pub fn empty<T: Tvp>() -> Self {
179        Self {
180            type_name: T::type_name().to_string(),
181            columns: T::columns(),
182            rows: Vec::new(),
183        }
184    }
185
186    /// Get the number of rows.
187    pub fn len(&self) -> usize {
188        self.rows.len()
189    }
190
191    /// Check if the TVP is empty.
192    pub fn is_empty(&self) -> bool {
193        self.rows.is_empty()
194    }
195
196    /// Iterate over the rows.
197    pub fn iter(&self) -> impl Iterator<Item = &TvpRow> {
198        self.rows.iter()
199    }
200}
201
202impl ToSql for TvpValue {
203    fn to_sql(&self) -> Result<SqlValue, TypeError> {
204        // Parse the type name to extract schema and type name
205        // Format: "schema.TypeName" or just "TypeName"
206        let (schema, type_name) = if let Some(dot_pos) = self.type_name.find('.') {
207            (
208                self.type_name[..dot_pos].to_string(),
209                self.type_name[dot_pos + 1..].to_string(),
210            )
211        } else {
212            (String::new(), self.type_name.clone())
213        };
214
215        // Convert TvpColumn to TvpColumnDef
216        let columns: Vec<TvpColumnDef> = self
217            .columns
218            .iter()
219            .map(|col| {
220                let column_type = TvpColumnType::from_sql_type(&col.sql_type).ok_or_else(|| {
221                    TypeError::UnsupportedConversion {
222                        from: col.sql_type.clone(),
223                        to: "TvpColumnType",
224                    }
225                })?;
226                Ok(TvpColumnDef::nullable(column_type))
227            })
228            .collect::<Result<Vec<_>, TypeError>>()?;
229
230        // Convert rows
231        let rows: Vec<Vec<SqlValue>> = self.rows.iter().map(|row| row.values.clone()).collect();
232
233        // Create TvpData
234        let tvp_data = TvpData {
235            schema,
236            type_name,
237            columns,
238            rows,
239        };
240
241        Ok(SqlValue::Tvp(Box::new(tvp_data)))
242    }
243
244    fn sql_type(&self) -> &'static str {
245        "TVP"
246    }
247}
248
249#[cfg(test)]
250#[allow(clippy::unwrap_used)]
251mod tests {
252    use super::*;
253
254    struct TestUserId {
255        user_id: i32,
256    }
257
258    impl Tvp for TestUserId {
259        fn type_name() -> &'static str {
260            "dbo.UserIdList"
261        }
262
263        fn columns() -> Vec<TvpColumn> {
264            vec![TvpColumn::new("UserId", "INT", 0)]
265        }
266
267        fn to_row(&self) -> Result<TvpRow, TypeError> {
268            Ok(TvpRow::new(vec![self.user_id.to_sql()?]))
269        }
270    }
271
272    #[test]
273    fn test_tvp_trait_impl() {
274        assert_eq!(TestUserId::type_name(), "dbo.UserIdList");
275
276        let columns = TestUserId::columns();
277        assert_eq!(columns.len(), 1);
278        assert_eq!(columns[0].name, "UserId");
279        assert_eq!(columns[0].sql_type, "INT");
280    }
281
282    #[test]
283    fn test_tvp_row_creation() {
284        let item = TestUserId { user_id: 42 };
285        let row = item.to_row().unwrap();
286
287        assert_eq!(row.len(), 1);
288        assert!(matches!(row.get(0), Some(SqlValue::Int(42))));
289    }
290
291    #[test]
292    fn test_tvp_value_creation() {
293        let items = vec![
294            TestUserId { user_id: 1 },
295            TestUserId { user_id: 2 },
296            TestUserId { user_id: 3 },
297        ];
298
299        let tvp = TvpValue::new(&items).unwrap();
300
301        assert_eq!(tvp.type_name, "dbo.UserIdList");
302        assert_eq!(tvp.columns.len(), 1);
303        assert_eq!(tvp.len(), 3);
304    }
305
306    #[test]
307    fn test_tvp_value_empty() {
308        let tvp: TvpValue = TvpValue::empty::<TestUserId>();
309
310        assert_eq!(tvp.type_name, "dbo.UserIdList");
311        assert!(tvp.is_empty());
312    }
313
314    #[test]
315    fn test_tvp_column() {
316        let col = TvpColumn::new("TestCol", "NVARCHAR(100)", 0);
317
318        assert_eq!(col.name, "TestCol");
319        assert_eq!(col.sql_type, "NVARCHAR(100)");
320        assert_eq!(col.ordinal, 0);
321    }
322}