Skip to main content

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