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}