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