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}