mssql_client/
to_params.rs

1//! ToParams trait for automatic struct-to-parameters mapping.
2//!
3//! This module provides the `ToParams` trait which enables automatic conversion
4//! from Rust structs to named query parameters.
5//!
6//! ## Derive Macro
7//!
8//! The recommended way to implement `ToParams` is via the derive macro from
9//! `mssql-derive`:
10//!
11//! ```rust,ignore
12//! use mssql_derive::ToParams;
13//!
14//! #[derive(ToParams)]
15//! struct NewUser {
16//!     name: String,
17//!     #[mssql(rename = "email_address")]
18//!     email: String,
19//! }
20//!
21//! let user = NewUser {
22//!     name: "Alice".into(),
23//!     email: "alice@example.com".into(),
24//! };
25//!
26//! // Use with named parameters in query
27//! client.execute(
28//!     "INSERT INTO users (name, email_address) VALUES (@name, @email_address)",
29//!     &user.to_params(),
30//! ).await?;
31//! ```
32//!
33//! ## Supported Attributes
34//!
35//! - `#[mssql(rename = "param_name")]` - Use a different parameter name
36//! - `#[mssql(skip)]` - Skip this field
37
38use mssql_types::{SqlValue, ToSql, TypeError};
39
40/// A named query parameter.
41#[derive(Debug, Clone)]
42pub struct NamedParam {
43    /// Parameter name (without @ prefix).
44    pub name: String,
45    /// Parameter value.
46    pub value: SqlValue,
47}
48
49impl NamedParam {
50    /// Create a new named parameter.
51    pub fn new<S: Into<String>>(name: S, value: SqlValue) -> Self {
52        Self {
53            name: name.into(),
54            value,
55        }
56    }
57
58    /// Create a named parameter from a value implementing ToSql.
59    pub fn from_value<S: Into<String>, T: ToSql>(name: S, value: &T) -> Result<Self, TypeError> {
60        Ok(Self {
61            name: name.into(),
62            value: value.to_sql()?,
63        })
64    }
65}
66
67/// Trait for types that can be converted to named query parameters.
68///
69/// This trait is typically implemented via the `#[derive(ToParams)]` macro,
70/// but can also be implemented manually for custom parameter handling.
71///
72/// # Example
73///
74/// ```rust,ignore
75/// use mssql_client::{ToParams, NamedParam};
76/// use mssql_types::{ToSql, TypeError};
77///
78/// struct NewUser {
79///     name: String,
80///     email: String,
81/// }
82///
83/// impl ToParams for NewUser {
84///     fn to_params(&self) -> Result<Vec<NamedParam>, TypeError> {
85///         Ok(vec![
86///             NamedParam::from_value("name", &self.name)?,
87///             NamedParam::from_value("email", &self.email)?,
88///         ])
89///     }
90/// }
91/// ```
92pub trait ToParams {
93    /// Convert this struct to a vector of named parameters.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if any field value cannot be converted to a SQL value.
98    fn to_params(&self) -> Result<Vec<NamedParam>, TypeError>;
99
100    /// Get the number of parameters this struct produces.
101    ///
102    /// Returns `None` if the count is dynamic.
103    fn param_count(&self) -> Option<usize> {
104        None
105    }
106}
107
108/// A list of named parameters that can be used in query execution.
109///
110/// This is a convenience wrapper around `Vec<NamedParam>` that implements
111/// additional utility methods.
112#[derive(Debug, Clone, Default)]
113pub struct ParamList {
114    params: Vec<NamedParam>,
115}
116
117impl ParamList {
118    /// Create a new empty parameter list.
119    pub fn new() -> Self {
120        Self { params: Vec::new() }
121    }
122
123    /// Create a parameter list with the given capacity.
124    pub fn with_capacity(capacity: usize) -> Self {
125        Self {
126            params: Vec::with_capacity(capacity),
127        }
128    }
129
130    /// Add a parameter to the list.
131    pub fn push(&mut self, param: NamedParam) {
132        self.params.push(param);
133    }
134
135    /// Add a parameter by name and value.
136    pub fn add<S: Into<String>, T: ToSql>(&mut self, name: S, value: &T) -> Result<(), TypeError> {
137        self.params.push(NamedParam::from_value(name, value)?);
138        Ok(())
139    }
140
141    /// Get the parameters as a slice.
142    pub fn as_slice(&self) -> &[NamedParam] {
143        &self.params
144    }
145
146    /// Get the number of parameters.
147    pub fn len(&self) -> usize {
148        self.params.len()
149    }
150
151    /// Check if the list is empty.
152    pub fn is_empty(&self) -> bool {
153        self.params.is_empty()
154    }
155
156    /// Iterate over the parameters.
157    pub fn iter(&self) -> impl Iterator<Item = &NamedParam> {
158        self.params.iter()
159    }
160}
161
162impl From<Vec<NamedParam>> for ParamList {
163    fn from(params: Vec<NamedParam>) -> Self {
164        Self { params }
165    }
166}
167
168impl IntoIterator for ParamList {
169    type Item = NamedParam;
170    type IntoIter = std::vec::IntoIter<NamedParam>;
171
172    fn into_iter(self) -> Self::IntoIter {
173        self.params.into_iter()
174    }
175}
176
177impl<'a> IntoIterator for &'a ParamList {
178    type Item = &'a NamedParam;
179    type IntoIter = std::slice::Iter<'a, NamedParam>;
180
181    fn into_iter(self) -> Self::IntoIter {
182        self.params.iter()
183    }
184}
185
186impl FromIterator<NamedParam> for ParamList {
187    fn from_iter<I: IntoIterator<Item = NamedParam>>(iter: I) -> Self {
188        Self {
189            params: iter.into_iter().collect(),
190        }
191    }
192}
193
194#[cfg(test)]
195#[allow(clippy::unwrap_used)]
196mod tests {
197    use super::*;
198
199    struct TestParams {
200        name: String,
201        age: i32,
202    }
203
204    impl ToParams for TestParams {
205        fn to_params(&self) -> Result<Vec<NamedParam>, TypeError> {
206            Ok(vec![
207                NamedParam::from_value("name", &self.name)?,
208                NamedParam::from_value("age", &self.age)?,
209            ])
210        }
211
212        fn param_count(&self) -> Option<usize> {
213            Some(2)
214        }
215    }
216
217    #[test]
218    fn test_to_params_manual_impl() {
219        let params = TestParams {
220            name: "Alice".to_string(),
221            age: 30,
222        };
223
224        let named_params = params.to_params().unwrap();
225        assert_eq!(named_params.len(), 2);
226        assert_eq!(named_params[0].name, "name");
227        assert_eq!(named_params[1].name, "age");
228    }
229
230    #[test]
231    fn test_named_param_creation() {
232        let param = NamedParam::from_value("test", &42i32).unwrap();
233        assert_eq!(param.name, "test");
234        assert!(matches!(param.value, SqlValue::Int(42)));
235    }
236
237    #[test]
238    fn test_param_list() {
239        let mut list = ParamList::new();
240        list.add("name", &"Alice").unwrap();
241        list.add("age", &30i32).unwrap();
242
243        assert_eq!(list.len(), 2);
244        assert!(!list.is_empty());
245
246        let names: Vec<&str> = list.iter().map(|p| p.name.as_str()).collect();
247        assert_eq!(names, vec!["name", "age"]);
248    }
249
250    #[test]
251    fn test_param_list_from_iterator() {
252        let params: ParamList = vec![
253            NamedParam::new("a", SqlValue::Int(1)),
254            NamedParam::new("b", SqlValue::Int(2)),
255        ]
256        .into_iter()
257        .collect();
258
259        assert_eq!(params.len(), 2);
260    }
261}