Skip to main content

tds_driver/tds/stream/
command.rs

1use crate::tds::stream::ReceivedToken;
2use crate::{row::ColumnType, Column, Row};
3use crate::{ColumnData, CommandResult, ResultMetadata};
4use futures_util::{
5    ready,
6    stream::{BoxStream, Peekable, Stream, StreamExt, TryStreamExt},
7};
8use std::{
9    fmt::Debug,
10    pin::Pin,
11    sync::Arc,
12    task::{self, Poll},
13};
14
15/// A `Stream` of [`CommandItem`] values produced by executing a [`Command`].
16///
17/// Items can be result metadata, rows, a return status, return values (OUT
18/// parameters) or a rows-affected count.
19///
20/// [`Command`]: crate::Command
21///
22/// # Example
23///
24/// ```no_run
25/// # use std::env;
26/// # use tds_driver::Config;
27/// # use tds_driver::{Command, CommandItem};
28/// # use futures_util::TryStreamExt;
29/// # use tokio_util::compat::TokioAsyncWriteCompatExt;
30/// # #[tokio::main]
31/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
32/// # let c_str = env::var("TDS_DRIVER_TEST_CONNECTION_STRING").unwrap_or(
33/// #     "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
34/// # );
35/// # let config = Config::from_ado_string(&c_str)?;
36/// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
37/// # tcp.set_nodelay(true)?;
38/// # let mut client = tds_driver::Client::connect(config, tcp.compat_write()).await?;
39/// let mut cmd = Command::new("dbo.usp_SomeStoredProc");
40///
41/// cmd.bind_param("@foo", 34i32);
42/// cmd.bind_param("@zoo", "the zoo string prm");
43/// cmd.bind_out_param("@bar", "bar");
44/// let mut stream = cmd.exec(&mut client).await?;
45///
46/// while let Some(item) = stream.try_next().await? {
47///     match item {
48///         // our first item is the column data always
49///         CommandItem::Metadata(meta) if meta.result_index() == 0 => {
50///             // the first result column info can be handled here
51///         }
52///         // ... and from there on from 0..N rows
53///         CommandItem::Row(row) if row.result_index() == 0 => {
54///             let var: Option<i32> = row.get(0);
55///         }
56///         // the second result set returns first another metadata item
57///         CommandItem::Metadata(meta) => {
58///             // .. handling
59///         }
60///         // ...and, again, we get rows from the second resultset
61///         CommandItem::Row(row) => {
62///             let var: Option<i32> = row.get(0);
63///         }
64///         // check return status (returned always)
65///         CommandItem::ReturnStatus(rs) => {
66///             // .... do something
67///         }
68///         // collect OUT parameter values
69///         CommandItem::ReturnValue(rv) => {
70///             // .... do something, like push to a collection
71///         }
72///         // get affected row count
73///         CommandItem::RowsAffected(ra) => {
74///             // .... do something, like push to a collection
75///         }
76///     }
77/// }
78/// # Ok(())
79/// # }
80/// ```
81///
82pub struct CommandStream<'a> {
83    token_stream: Peekable<BoxStream<'a, crate::Result<ReceivedToken>>>,
84    columns: Option<Arc<Vec<Column>>>,
85    result_set_index: Option<usize>,
86}
87
88impl<'a> Debug for CommandStream<'a> {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("CommandStream")
91            .field(
92                "token_stream",
93                &"BoxStream<'a, crate::Result<ReceivedToken>>",
94            )
95            .finish()
96    }
97}
98
99impl<'a> CommandStream<'a> {
100    pub(crate) fn new(token_stream: BoxStream<'a, crate::Result<ReceivedToken>>) -> Self {
101        Self {
102            token_stream: token_stream.peekable(),
103            columns: None,
104            result_set_index: None,
105        }
106    }
107
108    /// Collects all results from the command into memory, in the order they
109    /// were produced by the server.
110    pub async fn into_command_result(mut self) -> crate::Result<CommandResult> {
111        let mut results: Vec<Vec<Row>> = Vec::new();
112        let mut result: Option<Vec<Row>> = None;
113        let mut return_status = 0;
114        let mut return_values = Vec::new();
115        let mut rows_affected = Vec::new();
116
117        while let Some(item) = self.try_next().await? {
118            match item {
119                // Every COLMETADATA opens a result set, including one with no
120                // rows, so indexes line up with `ResultMetadata::result_index`.
121                CommandItem::Metadata(_) => results.extend(result.replace(Vec::new())),
122                CommandItem::Row(row) => result.get_or_insert_with(Vec::new).push(row),
123                CommandItem::ReturnStatus(rs) => return_status = rs,
124                CommandItem::ReturnValue(rv) => return_values.push(rv),
125                CommandItem::RowsAffected(rows) => rows_affected.push(rows),
126            }
127        }
128
129        if let Some(result) = result {
130            results.push(result);
131        }
132
133        Ok(CommandResult {
134            return_code: return_status,
135            return_values,
136            query_results: results,
137            rows_affected,
138        })
139    }
140
141    /// Converts the stream into a stream of rows, dropping all other items.
142    pub fn into_row_stream(self) -> BoxStream<'a, crate::Result<Row>> {
143        let s = self.try_filter_map(|item| async {
144            match item {
145                CommandItem::Row(row) => Ok(Some(row)),
146                _ => Ok(None),
147            }
148        });
149
150        Box::pin(s)
151    }
152}
153
154/// A single OUT parameter value returned by a [`Command`].
155///
156/// [`Command`]: crate::Command
157#[derive(Debug)]
158pub struct CommandReturnValue {
159    pub(crate) name: String,
160    pub(crate) ord: u16,
161    pub(crate) data: ColumnData<'static>,
162}
163
164impl CommandReturnValue {
165    /// The name of the OUT parameter this value corresponds to.
166    pub fn name(&self) -> &str {
167        &self.name
168    }
169
170    /// The ordinal of the OUT parameter as returned by the server.
171    pub fn ordinal(&self) -> u16 {
172        self.ord
173    }
174
175    /// A reference to the raw column data of the returned value.
176    pub fn data(&self) -> &ColumnData<'static> {
177        &self.data
178    }
179}
180
181/// An item produced by a [`CommandStream`].
182#[derive(Debug)]
183pub enum CommandItem {
184    /// A single row of data.
185    Row(Row),
186    /// Metadata describing the upcoming rows.
187    Metadata(ResultMetadata),
188    /// The procedure's `RETURN` value. Signed: `RETURN -1` is `-1`.
189    ReturnStatus(i32),
190    /// A return value, matching an OUT parameter.
191    ReturnValue(CommandReturnValue),
192    /// The number of rows an INSERT, UPDATE, DELETE or other non-SELECT
193    /// statement affected. SELECTs and variable assignments produce none, as
194    /// with SqlClient's `ExecuteNonQuery`; see [`ExecuteResult::rows_affected`].
195    ///
196    /// [`ExecuteResult::rows_affected`]: crate::ExecuteResult::rows_affected
197    RowsAffected(u64),
198}
199
200impl CommandItem {
201    pub(crate) fn metadata(columns: Arc<Vec<Column>>, result_index: usize) -> Self {
202        Self::Metadata(ResultMetadata {
203            columns,
204            result_index,
205        })
206    }
207
208    /// Returns a reference to the metadata, if the item is of a correct variant.
209    pub fn as_metadata(&self) -> Option<&ResultMetadata> {
210        match self {
211            CommandItem::Metadata(ref metadata) => Some(metadata),
212            _ => None,
213        }
214    }
215
216    /// Returns a reference to the row, if the item is of a correct variant.
217    pub fn as_row(&self) -> Option<&Row> {
218        match self {
219            CommandItem::Row(ref row) => Some(row),
220            _ => None,
221        }
222    }
223
224    /// Returns the metadata, if the item is of a correct variant.
225    pub fn into_metadata(self) -> Option<ResultMetadata> {
226        match self {
227            CommandItem::Metadata(metadata) => Some(metadata),
228            _ => None,
229        }
230    }
231
232    /// Returns the row, if the item is of a correct variant.
233    pub fn into_row(self) -> Option<Row> {
234        match self {
235            CommandItem::Row(row) => Some(row),
236            _ => None,
237        }
238    }
239
240    /// Returns the return status, if the item is of a correct variant.
241    pub fn as_return_status(&self) -> Option<i32> {
242        match self {
243            CommandItem::ReturnStatus(rs) => Some(*rs),
244            _ => None,
245        }
246    }
247
248    /// Returns a reference to the return value, if the item is of a correct variant.
249    pub fn as_return_value(&self) -> Option<&CommandReturnValue> {
250        match self {
251            CommandItem::ReturnValue(rv) => Some(rv),
252            _ => None,
253        }
254    }
255
256    /// Returns the return value, if the item is of a correct variant.
257    pub fn into_return_value(self) -> Option<CommandReturnValue> {
258        match self {
259            CommandItem::ReturnValue(rv) => Some(rv),
260            _ => None,
261        }
262    }
263}
264
265impl<'a> Stream for CommandStream<'a> {
266    type Item = crate::Result<CommandItem>;
267
268    fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
269        let this = self.get_mut();
270
271        loop {
272            let token = match ready!(this.token_stream.poll_next_unpin(cx)) {
273                Some(res) => res?,
274                None => return Poll::Ready(None),
275            };
276
277            return match token {
278                ReceivedToken::NewResultset(meta) => {
279                    let column_meta = meta
280                        .columns
281                        .iter()
282                        .map(|x| Column {
283                            name: x.col_name.to_string(),
284                            column_type: ColumnType::from(&x.base.ty),
285                        })
286                        .collect::<Vec<_>>();
287
288                    let column_meta = Arc::new(column_meta);
289                    this.columns = Some(column_meta.clone());
290
291                    this.result_set_index = this.result_set_index.map(|i| i + 1);
292
293                    let query_item =
294                        CommandItem::metadata(column_meta, *this.result_set_index.get_or_insert(0));
295
296                    Poll::Ready(Some(Ok(query_item)))
297                }
298                ReceivedToken::Row(data) => {
299                    let Some(columns) = this.columns.as_ref() else {
300                        return Poll::Ready(Some(Err(crate::Error::Protocol(
301                            "ROW token arrived before any column metadata".into(),
302                        ))));
303                    };
304                    let columns = columns.clone();
305                    let result_index = this.result_set_index.unwrap_or(0);
306
307                    let row = Row {
308                        columns,
309                        data,
310                        result_index,
311                    };
312
313                    Poll::Ready(Some(Ok(CommandItem::Row(row))))
314                }
315                ReceivedToken::ReturnStatus(rs) => {
316                    Poll::Ready(Some(Ok(CommandItem::ReturnStatus(rs))))
317                }
318                ReceivedToken::ReturnValue(rv) => {
319                    Poll::Ready(Some(Ok(CommandItem::ReturnValue(CommandReturnValue {
320                        name: rv.param_name,
321                        ord: rv.param_ordinal,
322                        data: rv.value,
323                    }))))
324                }
325                token => match token.records_affected() {
326                    Some(rows) => Poll::Ready(Some(Ok(CommandItem::RowsAffected(rows)))),
327                    None => continue,
328                },
329            };
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::tds::codec::{DoneStatus, TokenColMetaData, TokenDone, TokenRow};
338    use enumflags2::BitFlags;
339    use futures_util::stream;
340
341    const INSERT: u16 = 0xC3;
342    const UPDATE: u16 = 0xC5;
343    const SELECT: u16 = 0xC1;
344
345    fn meta() -> ReceivedToken {
346        ReceivedToken::NewResultset(Arc::new(TokenColMetaData { columns: vec![] }))
347    }
348
349    fn row() -> ReceivedToken {
350        ReceivedToken::Row(TokenRow::new())
351    }
352
353    fn count(cur_cmd: u16, rows: u64) -> TokenDone {
354        TokenDone::new(DoneStatus::More | DoneStatus::Count, cur_cmd, rows)
355    }
356
357    async fn collect(tokens: Vec<ReceivedToken>) -> CommandResult {
358        let tokens = stream::iter(tokens.into_iter().map(Ok)).boxed();
359        CommandStream::new(tokens)
360            .into_command_result()
361            .await
362            .unwrap()
363    }
364
365    fn set_sizes(res: &CommandResult) -> Vec<usize> {
366        res.query_results.iter().map(Vec::len).collect()
367    }
368
369    #[tokio::test]
370    async fn empty_result_sets_keep_their_index() {
371        let res = collect(vec![
372            meta(),
373            row(),
374            ReceivedToken::Done(count(SELECT, 1)),
375            meta(),
376            ReceivedToken::Done(count(SELECT, 0)),
377            meta(),
378            ReceivedToken::Done(count(SELECT, 0)),
379            meta(),
380            row(),
381            row(),
382            ReceivedToken::Done(count(SELECT, 2)),
383        ])
384        .await;
385        assert_eq!(set_sizes(&res), [1, 0, 0, 2]);
386    }
387
388    #[tokio::test]
389    async fn leading_empty_result_sets_are_kept() {
390        let res = collect(vec![
391            meta(),
392            ReceivedToken::Done(count(SELECT, 0)),
393            meta(),
394            ReceivedToken::Done(count(SELECT, 0)),
395            meta(),
396            row(),
397            ReceivedToken::Done(count(SELECT, 1)),
398        ])
399        .await;
400        assert_eq!(set_sizes(&res), [0, 0, 1]);
401    }
402
403    #[tokio::test]
404    async fn rows_affected_skips_selects_and_uncounted_dones() {
405        let res = collect(vec![
406            ReceivedToken::DoneInProc(count(INSERT, 2)),
407            meta(),
408            row(),
409            ReceivedToken::DoneInProc(count(SELECT, 1)),
410            // `SELECT @v = ..` inside the procedure
411            ReceivedToken::DoneInProc(count(SELECT, 1)),
412            ReceivedToken::DoneInProc(count(UPDATE, 0)),
413            ReceivedToken::ReturnStatus(-1),
414            ReceivedToken::DoneProc(TokenDone::new(BitFlags::empty(), 0xE0, 0)),
415        ])
416        .await;
417        assert_eq!(res.rows_affected(), [2, 0]);
418        assert_eq!(res.return_code(), -1);
419        assert_eq!(set_sizes(&res), [1]);
420    }
421}