Skip to main content

tiberius/
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 tiberius_ng_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    /// [`CommandResult`]: crate::CommandResult
130    pub fn bind_out_param(&mut self, name: impl Into<Cow<'a, str>>, data: impl IntoSql<'a> + 'a) {
131        self.params.push(CommandParam {
132            name: name.into(),
133            out: true,
134            data: CommandParamData::Scalar(data.into_sql()),
135        });
136    }
137
138    /// Binds a table-valued parameter. The provided argument must implement
139    /// [`TableValue`].
140    ///
141    /// # Example
142    ///
143    /// ```no_run
144    /// # use std::env;
145    /// # use tiberius::Config;
146    /// # use tiberius::{numeric::Numeric, Command, TableValueRow};
147    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
148    /// #[derive(TableValueRow)]
149    /// struct SomeGeoList {
150    ///     eid: i32,
151    ///     lat: Numeric,
152    ///     lon: Numeric,
153    /// }
154    /// # #[tokio::main]
155    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
156    /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
157    /// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
158    /// # );
159    /// # let config = Config::from_ado_string(&c_str)?;
160    /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
161    /// # tcp.set_nodelay(true)?;
162    /// # let client = tiberius::Client::connect(config, tcp.compat_write()).await?;
163    /// let r1 = SomeGeoList {
164    ///     eid: 1,
165    ///     lat: Numeric::new_with_scale(10, 6),
166    ///     lon: Numeric::new_with_scale(14, 6),
167    /// };
168    /// let r2 = SomeGeoList {
169    ///     eid: 4,
170    ///     lat: Numeric::new_with_scale(101, 6),
171    ///     lon: Numeric::new_with_scale(142, 6),
172    /// };
173    ///
174    /// let tbl = vec![r1, r2];
175    ///
176    /// let mut cmd = Command::new("dbo.usp_TheGeoProcedure");
177    /// cmd.bind_table("@table", tbl);
178    /// # Ok(())
179    /// # }
180    /// ```
181    pub fn bind_table(&mut self, name: impl Into<Cow<'a, str>>, data: impl TableValue<'a> + 'a) {
182        self.params.push(CommandParam {
183            name: name.into(),
184            out: false,
185            data: CommandParamData::Table(data.into_sql()),
186        });
187    }
188
189    /// The same as [`bind_table`](Self::bind_table), but overrides the database
190    /// type name used for the TVP.
191    pub fn bind_table_with_dbtype(
192        &mut self,
193        name: impl Into<Cow<'a, str>>,
194        db_type: &'a str,
195        data: impl TableValue<'a> + 'a,
196    ) {
197        self.params.push(CommandParam {
198            name: name.into(),
199            out: false,
200            data: CommandParamData::Table(SqlTableData {
201                db_type,
202                ..data.into_sql()
203            }),
204        });
205    }
206
207    /// Executes the command on the server, returning a [`CommandStream`] that
208    /// can be collected into a [`CommandResult`] for convenience.
209    ///
210    /// [`CommandResult`]: crate::CommandResult
211    ///
212    /// # Example
213    ///
214    /// ```no_run
215    /// # use tiberius::{Config, Command};
216    /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
217    /// # use std::env;
218    /// # #[tokio::main]
219    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
220    /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
221    /// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
222    /// # );
223    /// # let config = Config::from_ado_string(&c_str)?;
224    /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
225    /// # tcp.set_nodelay(true)?;
226    /// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
227    /// let mut cmd = Command::new("dbo.usp_SomeStoredProc");
228    ///
229    /// cmd.bind_param("@foo", 34i32);
230    /// cmd.bind_out_param("@bar", "bar");
231    /// let res = cmd.exec(&mut client).await?.into_command_result().await?;
232    ///
233    /// let rv: Option<&str> = res.try_return_value("@bar")?;
234    /// let rc = res.return_code();
235    /// # Ok(())
236    /// # }
237    /// ```
238    pub async fn exec<'b, S>(self, client: &'b mut Client<S>) -> crate::Result<CommandStream<'b>>
239    where
240        S: AsyncRead + AsyncWrite + Unpin + Send,
241    {
242        let rpc_params = Command::build_rpc_params(self.params, client).await?;
243
244        client.connection.flush_stream().await?;
245        client.rpc_run_command(self.name, rpc_params).await?;
246
247        let ts = TokenStream::new(&mut client.connection);
248        let result = CommandStream::new(ts.try_unfold());
249
250        Ok(result)
251    }
252
253    async fn build_rpc_params<'b, S>(
254        cmd_params: Vec<CommandParam<'a>>,
255        client: &'b mut Client<S>,
256    ) -> crate::Result<Vec<RpcParam<'a>>>
257    where
258        S: AsyncRead + AsyncWrite + Unpin + Send,
259    {
260        let mut rpc_params = Vec::new();
261        for p in cmd_params.into_iter() {
262            let rpc_val = match p.data {
263                CommandParamData::Scalar(col) => RpcValue::Scalar(col),
264                CommandParamData::Table(t) => {
265                    let type_info_tvp = TypeInfoTvp::new(
266                        t.db_type,
267                        t.rows.into_iter().map(|r| r.col_data).collect(),
268                    );
269                    // Resolve the TVP column layout from the server.
270                    let cols_metadata = client
271                        .query_run_for_metadata(format!(
272                            "DECLARE @P AS {};SELECT TOP 0 * FROM @P",
273                            t.db_type
274                        ))
275                        .await?;
276                    RpcValue::Table(if let Some(cm) = cols_metadata {
277                        type_info_tvp.with_metadata(cm)
278                    } else {
279                        type_info_tvp
280                    })
281                }
282            };
283            let rpc_param = RpcParam {
284                name: p.name,
285                flags: if p.out {
286                    BitFlags::from_flag(ByRefValue)
287                } else {
288                    BitFlags::empty()
289                },
290                value: rpc_val,
291            };
292            rpc_params.push(rpc_param);
293        }
294        Ok(rpc_params)
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    struct TestRow;
303
304    impl<'a> TableValueRow<'a> for TestRow {
305        fn bind_fields(&self, row: &mut SqlTableDataRow<'a>) {
306            row.add_field(1i32);
307        }
308
309        fn get_db_type() -> &'static str {
310            "default.Type"
311        }
312    }
313
314    #[test]
315    fn bind_table_with_dbtype_uses_the_explicit_db_type() {
316        // The explicit db_type argument must override the row's own get_db_type().
317        let mut cmd = Command::new("proc");
318        cmd.bind_table_with_dbtype("@tvp", "explicit.Type", vec![TestRow]);
319
320        assert_eq!(cmd.params.len(), 1);
321        assert_eq!(cmd.params[0].name, "@tvp");
322        match &cmd.params[0].data {
323            CommandParamData::Table(t) => assert_eq!(t.db_type, "explicit.Type"),
324            other => panic!("expected a table parameter, got {other:?}"),
325        }
326    }
327
328    #[test]
329    fn bind_table_uses_the_rows_db_type() {
330        let mut cmd = Command::new("proc");
331        cmd.bind_table("@tvp", vec![TestRow]);
332
333        match &cmd.params[0].data {
334            CommandParamData::Table(t) => assert_eq!(t.db_type, "default.Type"),
335            other => panic!("expected a table parameter, got {other:?}"),
336        }
337    }
338}