Skip to main content

tds_driver/
command.rs

1use std::borrow::Cow;
2
3use enumflags2::BitFlags;
4use futures_util::io::{AsyncRead, AsyncWrite};
5
6use crate::{
7    tds::{
8        codec::{RpcParam, RpcStatus::ByRefValue, RpcValue, TypeInfoTvp},
9        stream::{CommandStream, TokenStream},
10    },
11    Client, ColumnData, IntoSql,
12};
13
14#[doc(inline)]
15pub use tds_driver_macros::TableValueRow;
16
17/// A structure that represents a single row of a table-valued parameter (TVP)
18/// implements this trait.
19///
20/// It can be derived with `#[derive(TableValueRow)]` for structs with named
21/// fields.
22pub trait TableValueRow<'a> {
23    /// Binds this row's field values. Called by [`Command`] before making the
24    /// call to the server; implementations must call
25    /// [`SqlTableDataRow::add_field`] once per column, in column order.
26    fn bind_fields(&self, data_row: &mut SqlTableDataRow<'a>);
27    /// The database type name that represents this TVP, e.g. `dbo.MyType`.
28    fn get_db_type() -> &'static str;
29}
30
31/// A collection of [`TableValueRow`] values that can be bound as a
32/// table-valued parameter. Implemented for any `IntoIterator` of rows.
33pub trait TableValue<'a> {
34    /// Converts this collection into the internal table data representation.
35    fn into_sql(self) -> SqlTableData<'a>;
36}
37
38impl<'a, R, C> TableValue<'a> for C
39where
40    R: TableValueRow<'a> + 'a,
41    C: IntoIterator<Item = R>,
42{
43    fn into_sql(self) -> SqlTableData<'a> {
44        let mut data = Vec::new();
45        for row in self.into_iter() {
46            let mut data_row = SqlTableDataRow::new();
47            row.bind_fields(&mut data_row);
48            data.push(data_row);
49        }
50
51        SqlTableData {
52            rows: data,
53            db_type: R::get_db_type(),
54        }
55    }
56}
57
58/// A remote command (stored procedure or user-defined function) with bound
59/// parameters, executed by name via an RPC request.
60#[derive(Debug)]
61pub struct Command<'a> {
62    name: Cow<'a, str>,
63    // The server rejects repeated parameter names, so uniqueness is not checked here.
64    params: Vec<CommandParam<'a>>,
65}
66
67#[derive(Debug)]
68struct CommandParam<'a> {
69    name: Cow<'a, str>,
70    out: bool,
71    data: CommandParamData<'a>,
72}
73
74#[derive(Debug)]
75enum CommandParamData<'a> {
76    Scalar(ColumnData<'a>),
77    Table(SqlTableData<'a>),
78}
79
80/// The internal representation of a table-valued parameter's data.
81#[derive(Debug)]
82pub struct SqlTableData<'a> {
83    rows: Vec<SqlTableDataRow<'a>>,
84    db_type: &'a str,
85}
86
87/// A single row of a table-valued parameter, used by [`TableValueRow`]
88/// implementations to bind column values.
89#[derive(Debug)]
90pub struct SqlTableDataRow<'a> {
91    col_data: Vec<ColumnData<'a>>,
92}
93
94impl<'a> SqlTableDataRow<'a> {
95    fn new() -> SqlTableDataRow<'a> {
96        SqlTableDataRow {
97            col_data: Vec::new(),
98        }
99    }
100
101    /// Adds a field value to this TVP row. Must be called once per column; the
102    /// values are sent to the server in call order.
103    pub fn add_field(&mut self, data: impl IntoSql<'a> + 'a) {
104        self.col_data.push(data.into_sql());
105    }
106}
107
108impl<'a> Command<'a> {
109    /// Constructs a new command with the given procedure or function name.
110    pub fn new(proc_name: impl Into<Cow<'a, str>>) -> Self {
111        Self {
112            name: proc_name.into(),
113            params: Vec::new(),
114        }
115    }
116
117    /// Binds a scalar input parameter with the given name.
118    pub fn bind_param(&mut self, name: impl Into<Cow<'a, str>>, data: impl IntoSql<'a> + 'a) {
119        self.params.push(CommandParam {
120            name: name.into(),
121            out: false,
122            data: CommandParamData::Scalar(data.into_sql()),
123        });
124    }
125
126    /// Binds a by-ref (OUT) scalar parameter. The returned value can be found by
127    /// the same name in the [`CommandResult`] returned values.
128    ///
129    /// The value is returned in the type `data` is sent as. Strings and binary
130    /// are sent as `nvarchar(max)`/`varbinary(max)`, so nothing is cut off. A
131    /// [`Numeric`](crate::numeric::Numeric) sets the scale of the result (a
132    /// `None` gives scale 0): bind e.g. `Numeric::new_with_scale(0, 4)` for a
133    /// `decimal(10, 4)` parameter.
134    ///
135    /// [`CommandResult`]: crate::CommandResult
136    pub fn bind_out_param(&mut self, name: impl Into<Cow<'a, str>>, data: impl IntoSql<'a> + 'a) {
137        self.params.push(CommandParam {
138            name: name.into(),
139            out: true,
140            data: CommandParamData::Scalar(data.into_sql()),
141        });
142    }
143
144    /// Binds a table-valued parameter. The provided argument must implement
145    /// [`TableValue`].
146    ///
147    /// # Example
148    ///
149    /// ```no_run
150    /// # use std::env;
151    /// # use tds_driver::Config;
152    /// # use tds_driver::{numeric::Numeric, Command, TableValueRow};
153    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
154    /// #[derive(TableValueRow)]
155    /// struct SomeGeoList {
156    ///     eid: i32,
157    ///     lat: Numeric,
158    ///     lon: Numeric,
159    /// }
160    /// # #[tokio::main]
161    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
162    /// # let c_str = env::var("TDS_DRIVER_TEST_CONNECTION_STRING").unwrap_or(
163    /// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
164    /// # );
165    /// # let config = Config::from_ado_string(&c_str)?;
166    /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
167    /// # tcp.set_nodelay(true)?;
168    /// # let client = tds_driver::Client::connect(config, tcp.compat_write()).await?;
169    /// let r1 = SomeGeoList {
170    ///     eid: 1,
171    ///     lat: Numeric::new_with_scale(10, 6),
172    ///     lon: Numeric::new_with_scale(14, 6),
173    /// };
174    /// let r2 = SomeGeoList {
175    ///     eid: 4,
176    ///     lat: Numeric::new_with_scale(101, 6),
177    ///     lon: Numeric::new_with_scale(142, 6),
178    /// };
179    ///
180    /// let tbl = vec![r1, r2];
181    ///
182    /// let mut cmd = Command::new("dbo.usp_TheGeoProcedure");
183    /// cmd.bind_table("@table", tbl);
184    /// # Ok(())
185    /// # }
186    /// ```
187    pub fn bind_table(&mut self, name: impl Into<Cow<'a, str>>, data: impl TableValue<'a> + 'a) {
188        self.params.push(CommandParam {
189            name: name.into(),
190            out: false,
191            data: CommandParamData::Table(data.into_sql()),
192        });
193    }
194
195    /// The same as [`bind_table`](Self::bind_table), but overrides the database
196    /// type name used for the TVP.
197    pub fn bind_table_with_dbtype(
198        &mut self,
199        name: impl Into<Cow<'a, str>>,
200        db_type: &'a str,
201        data: impl TableValue<'a> + 'a,
202    ) {
203        self.params.push(CommandParam {
204            name: name.into(),
205            out: false,
206            data: CommandParamData::Table(SqlTableData {
207                db_type,
208                ..data.into_sql()
209            }),
210        });
211    }
212
213    /// Executes the command on the server, returning a [`CommandStream`] that
214    /// can be collected into a [`CommandResult`] for convenience.
215    ///
216    /// [`CommandResult`]: crate::CommandResult
217    ///
218    /// # Example
219    ///
220    /// ```no_run
221    /// # use tds_driver::{Config, Command};
222    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
223    /// # use std::env;
224    /// # #[tokio::main]
225    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
226    /// # let c_str = env::var("TDS_DRIVER_TEST_CONNECTION_STRING").unwrap_or(
227    /// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
228    /// # );
229    /// # let config = Config::from_ado_string(&c_str)?;
230    /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
231    /// # tcp.set_nodelay(true)?;
232    /// # let mut client = tds_driver::Client::connect(config, tcp.compat_write()).await?;
233    /// let mut cmd = Command::new("dbo.usp_SomeStoredProc");
234    ///
235    /// cmd.bind_param("@foo", 34i32);
236    /// cmd.bind_out_param("@bar", "bar");
237    /// let res = cmd.exec(&mut client).await?.into_command_result().await?;
238    ///
239    /// let rv: Option<&str> = res.try_return_value("@bar")?;
240    /// let rc = res.return_code();
241    /// # Ok(())
242    /// # }
243    /// ```
244    pub async fn exec<'b, S>(self, client: &'b mut Client<S>) -> crate::Result<CommandStream<'b>>
245    where
246        S: AsyncRead + AsyncWrite + Unpin + Send,
247    {
248        let rpc_params = Command::build_rpc_params(self.params, client).await?;
249
250        client.connection.flush_stream().await?;
251        client.rpc_run_command(self.name, rpc_params).await?;
252
253        let ts = TokenStream::new(&mut client.connection);
254        let result = CommandStream::new(ts.try_unfold());
255
256        Ok(result)
257    }
258
259    async fn build_rpc_params<'b, S>(
260        cmd_params: Vec<CommandParam<'a>>,
261        client: &'b mut Client<S>,
262    ) -> crate::Result<Vec<RpcParam<'a>>>
263    where
264        S: AsyncRead + AsyncWrite + Unpin + Send,
265    {
266        let mut rpc_params = Vec::new();
267        for p in cmd_params.into_iter() {
268            let rpc_val = match p.data {
269                CommandParamData::Scalar(col) => RpcValue::Scalar(col),
270                CommandParamData::Table(t) => {
271                    let type_info_tvp = TypeInfoTvp::new(
272                        t.db_type,
273                        t.rows.into_iter().map(|r| r.col_data).collect(),
274                    );
275                    // Resolve the TVP column layout from the server.
276                    let cols_metadata = client
277                        .query_run_for_metadata(format!(
278                            "DECLARE @P AS {};SELECT TOP 0 * FROM @P",
279                            t.db_type
280                        ))
281                        .await?;
282                    RpcValue::Table(if let Some(mut cm) = cols_metadata {
283                        let collation =
284                            crate::SqlReadBytes::context(&client.connection).collation();
285                        for column in cm.iter_mut() {
286                            column.base.set_variant_collation(collation);
287                        }
288                        type_info_tvp.with_metadata(cm)
289                    } else {
290                        type_info_tvp
291                    })
292                }
293            };
294            let rpc_param = RpcParam {
295                name: p.name,
296                flags: if p.out {
297                    BitFlags::from_flag(ByRefValue)
298                } else {
299                    BitFlags::empty()
300                },
301                value: rpc_val,
302            };
303            rpc_params.push(rpc_param);
304        }
305        Ok(rpc_params)
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    struct TestRow;
314
315    impl<'a> TableValueRow<'a> for TestRow {
316        fn bind_fields(&self, row: &mut SqlTableDataRow<'a>) {
317            row.add_field(1i32);
318        }
319
320        fn get_db_type() -> &'static str {
321            "default.Type"
322        }
323    }
324
325    #[test]
326    fn bind_table_with_dbtype_uses_the_explicit_db_type() {
327        // The explicit db_type argument must override the row's own get_db_type().
328        let mut cmd = Command::new("proc");
329        cmd.bind_table_with_dbtype("@tvp", "explicit.Type", vec![TestRow]);
330
331        assert_eq!(cmd.params.len(), 1);
332        assert_eq!(cmd.params[0].name, "@tvp");
333        match &cmd.params[0].data {
334            CommandParamData::Table(t) => assert_eq!(t.db_type, "explicit.Type"),
335            other => panic!("expected a table parameter, got {other:?}"),
336        }
337    }
338
339    #[test]
340    fn bind_table_uses_the_rows_db_type() {
341        let mut cmd = Command::new("proc");
342        cmd.bind_table("@tvp", vec![TestRow]);
343
344        match &cmd.params[0].data {
345            CommandParamData::Table(t) => assert_eq!(t.db_type, "default.Type"),
346            other => panic!("expected a table parameter, got {other:?}"),
347        }
348    }
349}