tiberius/tds/codec/token/token_alt_meta_data.rs
1use std::borrow::Cow;
2
3use crate::{tds::codec::BaseMetaDataColumn, SqlReadBytes};
4
5/// A column produced by a COMPUTE clause, as described by an
6/// [`TokenAltMetaData`] (`ALTMETADATA`, token `0x88`) stream.
7///
8/// In addition to the regular column metadata, each computed column carries the
9/// aggregate operator (`op`) that produced it (for example `SUM`, `AVG`,
10/// `COUNT`) and the operand column number (`operand`) the operator was applied
11/// to. See [MS-TDS] section 2.2.7.1.
12///
13/// [MS-TDS]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/
14#[derive(Debug, Clone)]
15pub struct AltMetaDataColumn<'a> {
16 /// The aggregate operator that produced this column (`Op` in [MS-TDS]).
17 pub op: u8,
18 /// The column number in the originating result set that the aggregate
19 /// operator was applied to (`Operand` in [MS-TDS]).
20 pub operand: u16,
21 /// The regular column metadata (flags, type information).
22 pub base: BaseMetaDataColumn,
23 /// The name of the computed column.
24 pub col_name: Cow<'a, str>,
25}
26
27/// The token describing the layout of a COMPUTE (BY) result set
28/// (`ALTMETADATA`, token `0x88`).
29///
30/// A single query can contain more than one COMPUTE clause; each is uniquely
31/// identified by [`id`](Self::id), which the matching [`TokenAltRow`] rows refer
32/// back to. See [MS-TDS] section 2.2.7.1.
33///
34/// [`TokenAltRow`]: crate::tds::codec::TokenAltRow
35/// [MS-TDS]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/
36#[derive(Debug, Clone)]
37pub struct TokenAltMetaData<'a> {
38 /// Identifies the COMPUTE clause this metadata describes. The associated
39 /// [`TokenAltRow`](crate::tds::codec::TokenAltRow) rows carry the same id.
40 pub id: u16,
41 /// The column numbers (from the originating result set) listed in the
42 /// COMPUTE `BY` clause, in order.
43 pub by_columns: Vec<u16>,
44 /// The computed columns, one per aggregate operator in the COMPUTE clause.
45 pub columns: Vec<AltMetaDataColumn<'a>>,
46}
47
48impl TokenAltMetaData<'static> {
49 pub(crate) async fn decode<R>(src: &mut R) -> crate::Result<Self>
50 where
51 R: SqlReadBytes + Unpin,
52 {
53 // Number of computed columns, e.g. `COMPUTE SUM(x), AVG(x)` -> 2.
54 let column_count = src.read_u16_le().await?;
55
56 // Identifies the COMPUTE clause; referenced by the ALTROW token.
57 let id = src.read_u16_le().await?;
58
59 // Number of grouping columns in the `BY` list.
60 let by_cols = src.read_u8().await?;
61
62 let mut by_columns = Vec::with_capacity(by_cols as usize);
63 for _ in 0..by_cols {
64 by_columns.push(src.read_u16_le().await?);
65 }
66
67 // `column_count` is an untrusted u16 (up to 65535); cap the up-front
68 // reservation so a hostile ALTMETADATA token can't force a large
69 // transient allocation before the column data has arrived. The Vec
70 // still grows as real columns are decoded.
71 let mut columns = Vec::with_capacity(
72 (column_count as usize).min(crate::tds::codec::column_data::MAX_PREALLOC),
73 );
74 for _ in 0..column_count {
75 let op = src.read_u8().await?;
76 let operand = src.read_u16_le().await?;
77
78 let base = BaseMetaDataColumn::decode(src).await?;
79 let col_name = Cow::from(src.read_b_varchar().await?);
80
81 columns.push(AltMetaDataColumn {
82 op,
83 operand,
84 base,
85 col_name,
86 });
87 }
88
89 Ok(TokenAltMetaData {
90 id,
91 by_columns,
92 columns,
93 })
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::{sql_read_bytes::test_utils::IntoSqlReadBytes, tds::codec::TypeInfo, FixedLenType};
101 use bytes::{BufMut, BytesMut};
102
103 #[tokio::test]
104 async fn decode_alt_meta_data_single_sum_column() {
105 // `SELECT ... COMPUTE SUM(x) BY y` style metadata for one Int4 column.
106 let mut buf = BytesMut::new();
107
108 buf.put_u16_le(1); // column count (one aggregate)
109 buf.put_u16_le(7); // compute id
110 buf.put_u8(1); // by_cols
111 buf.put_u16_le(2); // BY column number
112
113 // ComputeData for the single column:
114 buf.put_u8(0x4f); // Op = SUM
115 buf.put_u16_le(1); // Operand column number
116
117 // BaseMetaDataColumn: user type (u32), flags (u16), TYPE_INFO
118 buf.put_u32_le(0); // user type
119 buf.put_u16_le(0x0001); // flags (Nullable)
120 buf.put_u8(FixedLenType::Int4 as u8); // TYPE_INFO: INT4TYPE
121
122 // ColName as B_VARCHAR (length in chars, then UTF-16LE)
123 let name: Vec<u16> = "sum".encode_utf16().collect();
124 buf.put_u8(name.len() as u8);
125 for c in name {
126 buf.put_u16_le(c);
127 }
128
129 let mut reader = buf.into_sql_read_bytes();
130 let meta = TokenAltMetaData::decode(&mut reader).await.unwrap();
131
132 assert_eq!(7, meta.id);
133 assert_eq!(vec![2], meta.by_columns);
134 assert_eq!(1, meta.columns.len());
135
136 let col = &meta.columns[0];
137 assert_eq!(0x4f, col.op);
138 assert_eq!(1, col.operand);
139 assert_eq!("sum", col.col_name);
140 assert!(matches!(
141 col.base.ty,
142 TypeInfo::FixedLen(FixedLenType::Int4)
143 ));
144 }
145}