Skip to main content

tiberius/tds/codec/
type_info.rs

1use asynchronous_codec::BytesMut;
2use bytes::BufMut;
3
4use crate::{tds::Collation, xml::XmlSchema, Error, SqlReadBytes};
5use std::{convert::TryFrom, sync::Arc};
6
7use super::Encode;
8
9/// A length of a column in bytes or characters.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TypeLength {
12    /// The number of bytes (or characters) reserved in the column.
13    Limited(u16),
14    /// Unlimited, stored in the heap outside of the row.
15    Max,
16}
17
18/// Describes a type of a column.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum TypeInfo {
21    /// A fixed-length type, whose size is fully determined by the type itself.
22    FixedLen(FixedLenType),
23    /// A variable-length type with an explicit size (and optional collation).
24    VarLenSized(VarLenContext),
25    /// A variable-length type carrying a precision and scale, such as `decimal`
26    /// and `numeric`.
27    VarLenSizedPrecision {
28        /// The underlying variable-length type.
29        ty: VarLenType,
30        /// The reserved size of the column in bytes.
31        size: usize,
32        /// The total number of digits.
33        precision: u8,
34        /// The number of digits to the right of the decimal point.
35        scale: u8,
36    },
37    /// The `xml` type, with an optional associated schema.
38    Xml {
39        /// The XML schema associated with the column, if any.
40        schema: Option<Arc<XmlSchema>>,
41        /// The reserved size of the column in bytes.
42        size: usize,
43    },
44    /// A CLR user-defined type (UDT), MS-TDS §2.2.5.5.4.
45    Udt(UdtInfo),
46}
47
48/// Metadata describing a CLR user-defined type (UDT) column, as defined by the
49/// `UDT_INFO` rule in MS-TDS §2.2.5.5.4.
50///
51/// This carries only the identifying metadata of the type. The value bytes are
52/// surfaced verbatim (see [`ColumnData::Binary`]); tiberius does not attempt to
53/// deserialize the CLR representation.
54///
55/// [`ColumnData::Binary`]: crate::ColumnData::Binary
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct UdtInfo {
58    /// Maximum size of the UDT value in bytes. A value of `0xFFFF` indicates a
59    /// large (`MAX`) UDT with no fixed upper bound.
60    pub max_byte_size: u16,
61    /// Name of the database in which the UDT is defined.
62    pub db_name: String,
63    /// Name of the schema that owns the UDT.
64    pub schema_name: String,
65    /// Name of the UDT.
66    pub type_name: String,
67    /// Assembly-qualified name of the CLR type that implements the UDT.
68    pub assembly_qualified_name: String,
69}
70
71/// The context of a variable-length column: its underlying type, size and
72/// optional collation.
73#[derive(Clone, Debug, Copy, PartialEq, Eq)]
74pub struct VarLenContext {
75    r#type: VarLenType,
76    len: usize,
77    collation: Option<Collation>,
78}
79
80impl VarLenContext {
81    /// Create a new variable-length context from a type, length and optional
82    /// collation.
83    pub fn new(r#type: VarLenType, len: usize, collation: Option<Collation>) -> Self {
84        Self {
85            r#type,
86            len,
87            collation,
88        }
89    }
90
91    /// Get the var len context's r#type.
92    pub fn r#type(&self) -> VarLenType {
93        self.r#type
94    }
95
96    /// Get the var len context's len.
97    pub fn len(&self) -> usize {
98        self.len
99    }
100
101    /// `true` if the column reserves no length.
102    pub fn is_empty(&self) -> bool {
103        self.len == 0
104    }
105
106    /// Get the var len context's collation.
107    pub fn collation(&self) -> Option<Collation> {
108        self.collation
109    }
110}
111
112impl Encode<BytesMut> for VarLenContext {
113    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
114        dst.put_u8(self.r#type() as u8);
115
116        // length
117        match self.r#type {
118            // DATE (0x28) carries NO scale byte in TYPE_INFO (MS-TDS
119            // §2.2.5.4.2 / §2.2.5.5.1.2), unlike TIME/DATETIME2/DATETIMEOFFSET
120            // which each carry a SCALE byte. The decoder already special-cases
121            // this (`Daten => 3`, reading no byte); emitting a byte here would
122            // desync every field after a `date` column in a TYPE_INFO stream
123            // (bulk-load column metadata / TVP).
124            #[cfg(feature = "tds73")]
125            VarLenType::Daten => {}
126            #[cfg(feature = "tds73")]
127            VarLenType::Timen | VarLenType::DatetimeOffsetn | VarLenType::Datetime2 => {
128                dst.put_u8(self.len() as u8);
129            }
130            VarLenType::Bitn
131            | VarLenType::Intn
132            | VarLenType::Floatn
133            | VarLenType::Decimaln
134            | VarLenType::Numericn
135            | VarLenType::Guid
136            | VarLenType::Money
137            | VarLenType::Datetimen => {
138                dst.put_u8(self.len() as u8);
139            }
140            VarLenType::NChar
141            | VarLenType::BigChar
142            | VarLenType::NVarchar
143            | VarLenType::BigVarChar
144            | VarLenType::BigBinary
145            | VarLenType::BigVarBin => {
146                dst.put_u16_le(self.len() as u16);
147            }
148            VarLenType::Image | VarLenType::Text | VarLenType::NText | VarLenType::SSVariant => {
149                dst.put_u32_le(self.len() as u32);
150            }
151            VarLenType::Xml => (),
152            typ => {
153                return Err(Error::Protocol(
154                    format!("encoding a {typ:?} var-len context is not supported").into(),
155                ))
156            }
157        }
158
159        if let Some(collation) = self.collation() {
160            dst.put_u32_le(collation.info());
161            dst.put_u8(collation.sort_id());
162        }
163
164        Ok(())
165    }
166}
167
168uint_enum! {
169    #[repr(u8)]
170    pub enum FixedLenType {
171        Null = 0x1F,
172        Int1 = 0x30,
173        Bit = 0x32,
174        Int2 = 0x34,
175        Int4 = 0x38,
176        Datetime4 = 0x3A,
177        Float4 = 0x3B,
178        Money = 0x3C,
179        Datetime = 0x3D,
180        Float8 = 0x3E,
181        Money4 = 0x7A,
182        Int8 = 0x7F,
183    }
184}
185
186#[cfg(not(feature = "tds73"))]
187uint_enum! {
188    /// 2.2.5.4.2
189    #[repr(u8)]
190    pub enum VarLenType {
191        Guid = 0x24,
192        Intn = 0x26,
193        Bitn = 0x68,
194        Decimaln = 0x6A,
195        Numericn = 0x6C,
196        Floatn = 0x6D,
197        Money = 0x6E,
198        Datetimen = 0x6F,
199        BigVarBin = 0xA5,
200        BigVarChar = 0xA7,
201        BigBinary = 0xAD,
202        BigChar = 0xAF,
203        NVarchar = 0xE7,
204        NChar = 0xEF,
205        Xml = 0xF1,
206        // CLR user-defined type; decoded as raw PLP bytes (see column_data/udt.rs).
207        Udt = 0xF0,
208        Text = 0x23,
209        Image = 0x22,
210        NText = 0x63,
211        // sql_variant; fully decoded/encoded (see column_data/sql_variant.rs).
212        SSVariant = 0x62, // legacy types (not supported since post-7.2):
213                          // Char = 0x2F,
214                          // Binary = 0x2D,
215                          // VarBinary = 0x25,
216                          // VarChar = 0x27,
217                          // Numeric = 0x3F,
218                          // Decimal = 0x37
219    }
220}
221
222#[cfg(feature = "tds73")]
223uint_enum! {
224    /// 2.2.5.4.2
225    #[repr(u8)]
226    pub enum VarLenType {
227        Guid = 0x24,
228        Intn = 0x26,
229        Bitn = 0x68,
230        Decimaln = 0x6A,
231        Numericn = 0x6C,
232        Floatn = 0x6D,
233        Money = 0x6E,
234        Datetimen = 0x6F,
235        Daten = 0x28,
236        Timen = 0x29,
237        Datetime2 = 0x2A,
238        DatetimeOffsetn = 0x2B,
239        BigVarBin = 0xA5,
240        BigVarChar = 0xA7,
241        BigBinary = 0xAD,
242        BigChar = 0xAF,
243        NVarchar = 0xE7,
244        NChar = 0xEF,
245        Xml = 0xF1,
246        // CLR user-defined type; decoded as raw PLP bytes (see column_data/udt.rs).
247        Udt = 0xF0,
248        Text = 0x23,
249        Image = 0x22,
250        NText = 0x63,
251        // sql_variant; fully decoded/encoded (see column_data/sql_variant.rs).
252        SSVariant = 0x62, // legacy types (not supported since post-7.2):
253                          // Char = 0x2F,
254                          // Binary = 0x2D,
255                          // VarBinary = 0x25,
256                          // VarChar = 0x27,
257                          // Numeric = 0x3F,
258                          // Decimal = 0x37
259    }
260}
261
262impl Encode<BytesMut> for TypeInfo {
263    fn encode(self, dst: &mut BytesMut) -> crate::Result<()> {
264        match self {
265            TypeInfo::FixedLen(ty) => {
266                dst.put_u8(ty as u8);
267            }
268            TypeInfo::VarLenSized(ctx) => ctx.encode(dst)?,
269            TypeInfo::VarLenSizedPrecision {
270                ty,
271                size,
272                precision,
273                scale,
274            } => {
275                dst.put_u8(ty as u8);
276                dst.put_u8(size as u8);
277                dst.put_u8(precision);
278                dst.put_u8(scale);
279            }
280            TypeInfo::Xml { schema, .. } => {
281                dst.put_u8(VarLenType::Xml as u8);
282
283                if let Some(xs) = schema {
284                    dst.put_u8(1);
285
286                    let db_name_encoded: Vec<u16> = xs.db_name().encode_utf16().collect();
287                    dst.put_u8(db_name_encoded.len() as u8);
288                    for chr in db_name_encoded {
289                        dst.put_u16_le(chr);
290                    }
291
292                    let owner_encoded: Vec<u16> = xs.owner().encode_utf16().collect();
293                    dst.put_u8(owner_encoded.len() as u8);
294                    for chr in owner_encoded {
295                        dst.put_u16_le(chr);
296                    }
297
298                    let collection_encoded: Vec<u16> = xs.collection().encode_utf16().collect();
299                    dst.put_u16_le(collection_encoded.len() as u16);
300                    for chr in collection_encoded {
301                        dst.put_u16_le(chr);
302                    }
303                } else {
304                    dst.put_u8(0);
305                }
306            }
307            TypeInfo::Udt(info) => {
308                dst.put_u8(VarLenType::Udt as u8);
309                dst.put_u16_le(info.max_byte_size);
310
311                let db_name: Vec<u16> = info.db_name.encode_utf16().collect();
312                dst.put_u8(db_name.len() as u8);
313                for chr in db_name {
314                    dst.put_u16_le(chr);
315                }
316
317                let schema_name: Vec<u16> = info.schema_name.encode_utf16().collect();
318                dst.put_u8(schema_name.len() as u8);
319                for chr in schema_name {
320                    dst.put_u16_le(chr);
321                }
322
323                let type_name: Vec<u16> = info.type_name.encode_utf16().collect();
324                dst.put_u8(type_name.len() as u8);
325                for chr in type_name {
326                    dst.put_u16_le(chr);
327                }
328
329                let aqn: Vec<u16> = info.assembly_qualified_name.encode_utf16().collect();
330                dst.put_u16_le(aqn.len() as u16);
331                for chr in aqn {
332                    dst.put_u16_le(chr);
333                }
334            }
335        }
336
337        Ok(())
338    }
339}
340
341impl TypeInfo {
342    pub(crate) async fn decode<R>(src: &mut R) -> crate::Result<Self>
343    where
344        R: SqlReadBytes + Unpin,
345    {
346        let ty = src.read_u8().await?;
347
348        if let Ok(ty) = FixedLenType::try_from(ty) {
349            return Ok(TypeInfo::FixedLen(ty));
350        }
351
352        match VarLenType::try_from(ty) {
353            Err(()) => Err(Error::Protocol(
354                format!("invalid or unsupported column type: {:?}", ty).into(),
355            )),
356            Ok(VarLenType::Xml) => {
357                let has_schema = src.read_u8().await?;
358
359                let schema = if has_schema == 1 {
360                    let db_name = src.read_b_varchar().await?;
361                    let owner = src.read_b_varchar().await?;
362                    let collection = src.read_us_varchar().await?;
363
364                    Some(Arc::new(XmlSchema::new(db_name, owner, collection)))
365                } else {
366                    None
367                };
368
369                Ok(TypeInfo::Xml {
370                    schema,
371                    size: 0xfffffffffffffffe_usize,
372                })
373            }
374            Ok(VarLenType::Udt) => {
375                // UDT_INFO, MS-TDS §2.2.5.5.4
376                let max_byte_size = src.read_u16_le().await?;
377                let db_name = src.read_b_varchar().await?;
378                let schema_name = src.read_b_varchar().await?;
379                let type_name = src.read_b_varchar().await?;
380                let assembly_qualified_name = src.read_us_varchar().await?;
381
382                Ok(TypeInfo::Udt(UdtInfo {
383                    max_byte_size,
384                    db_name,
385                    schema_name,
386                    type_name,
387                    assembly_qualified_name,
388                }))
389            }
390            Ok(ty) => {
391                let len = match ty {
392                    #[cfg(feature = "tds73")]
393                    VarLenType::Timen | VarLenType::DatetimeOffsetn | VarLenType::Datetime2 => {
394                        src.read_u8().await? as usize
395                    }
396                    #[cfg(feature = "tds73")]
397                    VarLenType::Daten => 3,
398                    VarLenType::Bitn
399                    | VarLenType::Intn
400                    | VarLenType::Floatn
401                    | VarLenType::Decimaln
402                    | VarLenType::Numericn
403                    | VarLenType::Guid
404                    | VarLenType::Money
405                    | VarLenType::Datetimen => src.read_u8().await? as usize,
406                    VarLenType::NChar
407                    | VarLenType::BigChar
408                    | VarLenType::NVarchar
409                    | VarLenType::BigVarChar
410                    | VarLenType::BigBinary
411                    | VarLenType::BigVarBin => src.read_u16_le().await? as usize,
412                    VarLenType::Image
413                    | VarLenType::Text
414                    | VarLenType::NText
415                    | VarLenType::SSVariant => src.read_u32_le().await? as usize,
416                    _ => {
417                        return Err(Error::Protocol(
418                            format!("unsupported column type in COLMETADATA: {:?}", ty).into(),
419                        ))
420                    }
421                };
422
423                let collation = match ty {
424                    VarLenType::NText
425                    | VarLenType::Text
426                    | VarLenType::BigChar
427                    | VarLenType::NChar
428                    | VarLenType::NVarchar
429                    | VarLenType::BigVarChar => {
430                        let info = src.read_u32_le().await?;
431                        let sort_id = src.read_u8().await?;
432
433                        Some(Collation::new(info, sort_id))
434                    }
435                    _ => None,
436                };
437
438                let vty = match ty {
439                    VarLenType::Decimaln | VarLenType::Numericn => {
440                        let precision = src.read_u8().await?;
441                        let scale = src.read_u8().await?;
442
443                        // MS-TDS: precision is 1..=38 and scale 0..=precision.
444                        // Reject out-of-range server values here so downstream
445                        // (Numeric decode/Display) never sees an impossible scale.
446                        if precision > 38 || scale > precision {
447                            return Err(Error::Protocol(
448                                format!(
449                                    "decimal/numeric: invalid precision {precision} / scale {scale}"
450                                )
451                                .into(),
452                            ));
453                        }
454
455                        TypeInfo::VarLenSizedPrecision {
456                            size: len,
457                            ty,
458                            precision,
459                            scale,
460                        }
461                    }
462                    _ => {
463                        let cx = VarLenContext::new(ty, len, collation);
464                        TypeInfo::VarLenSized(cx)
465                    }
466                };
467
468                Ok(vty)
469            }
470        }
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use crate::sql_read_bytes::test_utils::IntoSqlReadBytes;
478
479    #[tokio::test]
480    async fn round_trip() {
481        let types = vec![
482            TypeInfo::Xml {
483                schema: Some(
484                    XmlSchema::new("fake-db-name", "fake-owner", "fake-collection").into(),
485                ),
486                size: 0xfffffffffffffffe_usize,
487            },
488            TypeInfo::Xml {
489                schema: None,
490                size: 0xfffffffffffffffe_usize,
491            },
492            TypeInfo::FixedLen(FixedLenType::Int4),
493            TypeInfo::VarLenSized(VarLenContext::new(
494                VarLenType::NChar,
495                40,
496                Some(Collation::new(13632521, 52)),
497            )),
498            TypeInfo::Udt(UdtInfo {
499                max_byte_size: 0xffff,
500                db_name: "fake-db".to_string(),
501                schema_name: "dbo".to_string(),
502                type_name: "geometry".to_string(),
503                assembly_qualified_name:
504                    "Microsoft.SqlServer.Types.SqlGeometry, Microsoft.SqlServer.Types".to_string(),
505            }),
506        ];
507
508        for ti in types {
509            let mut buf = BytesMut::new();
510
511            ti.clone()
512                .encode(&mut buf)
513                .expect("encode should be successful");
514
515            let nti = TypeInfo::decode(&mut buf.into_sql_read_bytes())
516                .await
517                .expect("decode must succeed");
518
519            assert_eq!(nti, ti)
520        }
521    }
522
523    #[cfg(feature = "tds73")]
524    #[tokio::test]
525    async fn date_typeinfo_round_trips_without_scale_byte() {
526        // DATE (0x28) has no scale byte in TYPE_INFO: encode must emit only the
527        // type token, and it must round-trip through decode.
528        let ti = TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Daten, 3, None));
529        let mut buf = BytesMut::new();
530        ti.clone().encode(&mut buf).expect("encode must succeed");
531
532        assert_eq!(buf.as_ref(), &[VarLenType::Daten as u8]);
533
534        let nti = TypeInfo::decode(&mut buf.into_sql_read_bytes())
535            .await
536            .expect("decode must succeed");
537        assert_eq!(nti, ti);
538    }
539
540    #[tokio::test]
541    async fn decode_rejects_out_of_range_precision_scale() {
542        // Decimaln TYPE_INFO: [type][size][precision][scale]. A precision > 38
543        // from an untrusted server must be rejected rather than flowing into
544        // Numeric decoding (which would later panic on an impossible scale).
545        let mut buf = BytesMut::new();
546        buf.put_u8(VarLenType::Decimaln as u8);
547        buf.put_u8(17); // size
548        buf.put_u8(200); // precision (invalid, > 38)
549        buf.put_u8(2); // scale
550
551        let err = TypeInfo::decode(&mut buf.into_sql_read_bytes())
552            .await
553            .expect_err("out-of-range precision must error");
554        assert!(matches!(err, Error::Protocol(_)));
555    }
556
557    #[test]
558    fn var_len_context_is_empty() {
559        assert!(VarLenContext::new(VarLenType::Intn, 0, None).is_empty());
560        assert!(!VarLenContext::new(VarLenType::Intn, 4, None).is_empty());
561    }
562
563    #[tokio::test]
564    async fn decode_intn_reads_one_byte_length() {
565        // Covers the Bitn|Intn|Floatn|... match arm: the length is a single u8.
566        let mut buf = BytesMut::new();
567        buf.put_u8(VarLenType::Intn as u8);
568        buf.put_u8(4); // length in bytes
569
570        let ti = TypeInfo::decode(&mut buf.into_sql_read_bytes())
571            .await
572            .expect("decode must succeed");
573        assert_eq!(
574            ti,
575            TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Intn, 4, None))
576        );
577    }
578
579    #[cfg(feature = "tds73")]
580    #[tokio::test]
581    async fn decode_timen_reads_one_byte_scale() {
582        // Covers the Timen|DatetimeOffsetn|Datetime2 match arm: reads a u8 scale
583        // as the length. Deleting the arm would make this an error.
584        let mut buf = BytesMut::new();
585        buf.put_u8(VarLenType::Timen as u8);
586        buf.put_u8(7); // scale
587
588        let ti = TypeInfo::decode(&mut buf.into_sql_read_bytes())
589            .await
590            .expect("decode must succeed");
591        assert_eq!(
592            ti,
593            TypeInfo::VarLenSized(VarLenContext::new(VarLenType::Timen, 7, None))
594        );
595    }
596
597    #[tokio::test]
598    async fn decode_accepts_precision_38_and_scale_below_precision() {
599        // Boundary: precision == 38 is the maximum valid precision and must be
600        // accepted; scale (10) is below precision.
601        let mut buf = BytesMut::new();
602        buf.put_u8(VarLenType::Decimaln as u8);
603        buf.put_u8(17); // size
604        buf.put_u8(38); // precision (max valid)
605        buf.put_u8(10); // scale
606
607        let ti = TypeInfo::decode(&mut buf.into_sql_read_bytes())
608            .await
609            .expect("precision 38 must be accepted");
610        assert_eq!(
611            ti,
612            TypeInfo::VarLenSizedPrecision {
613                ty: VarLenType::Decimaln,
614                size: 17,
615                precision: 38,
616                scale: 10,
617            }
618        );
619    }
620
621    #[tokio::test]
622    async fn decode_accepts_scale_equal_to_precision() {
623        // Boundary: scale == precision is valid (scale may equal precision).
624        let mut buf = BytesMut::new();
625        buf.put_u8(VarLenType::Numericn as u8);
626        buf.put_u8(17); // size
627        buf.put_u8(20); // precision
628        buf.put_u8(20); // scale == precision
629
630        let ti = TypeInfo::decode(&mut buf.into_sql_read_bytes())
631            .await
632            .expect("scale == precision must be accepted");
633        assert_eq!(
634            ti,
635            TypeInfo::VarLenSizedPrecision {
636                ty: VarLenType::Numericn,
637                size: 17,
638                precision: 20,
639                scale: 20,
640            }
641        );
642    }
643
644    #[test]
645    fn var_len_context_encode_xml_emits_only_type_byte() {
646        // Xml in a VarLenContext carries no length bytes: encode must emit only
647        // the type token (the `VarLenType::Xml => ()` arm).
648        let mut buf = BytesMut::new();
649        VarLenContext::new(VarLenType::Xml, 0, None)
650            .encode(&mut buf)
651            .expect("encode must succeed");
652        assert_eq!(buf.as_ref(), &[VarLenType::Xml as u8]);
653    }
654
655    #[test]
656    fn var_len_context_encode_unsupported_type_errors() {
657        // Udt is not encodable through VarLenContext (it has its own TypeInfo
658        // arm), so it hits the `typ => Err(..)` fallback.
659        let mut buf = BytesMut::new();
660        let err = VarLenContext::new(VarLenType::Udt, 0, None)
661            .encode(&mut buf)
662            .expect_err("encoding a Udt var-len context must error");
663        assert!(matches!(err, Error::Protocol(_)));
664    }
665
666    #[tokio::test]
667    async fn decode_rejects_invalid_type_byte() {
668        // A leading byte that is neither a FixedLenType nor a VarLenType must be
669        // rejected (`Err(())` arm of the VarLenType match).
670        let mut buf = BytesMut::new();
671        buf.put_u8(0x00);
672
673        let err = TypeInfo::decode(&mut buf.into_sql_read_bytes())
674            .await
675            .expect_err("invalid type byte must error");
676        assert!(matches!(err, Error::Protocol(_)));
677    }
678
679    #[tokio::test]
680    async fn decode_udt_info_round_trips() {
681        // Exercises the UDT_INFO decode arm: max_byte_size + three b_varchars +
682        // a us_varchar assembly-qualified name.
683        let ti = TypeInfo::Udt(UdtInfo {
684            max_byte_size: 0xffff,
685            db_name: "db".to_string(),
686            schema_name: "dbo".to_string(),
687            type_name: "geometry".to_string(),
688            assembly_qualified_name: "asm".to_string(),
689        });
690
691        let mut buf = BytesMut::new();
692        ti.clone().encode(&mut buf).expect("encode must succeed");
693
694        let decoded = TypeInfo::decode(&mut buf.into_sql_read_bytes())
695            .await
696            .expect("decode must succeed");
697        assert_eq!(decoded, ti);
698    }
699
700    #[tokio::test]
701    async fn decode_rejects_scale_greater_than_precision() {
702        // scale > precision must be rejected.
703        let mut buf = BytesMut::new();
704        buf.put_u8(VarLenType::Decimaln as u8);
705        buf.put_u8(17); // size
706        buf.put_u8(10); // precision
707        buf.put_u8(20); // scale > precision
708
709        let err = TypeInfo::decode(&mut buf.into_sql_read_bytes())
710            .await
711            .expect_err("scale > precision must error");
712        assert!(matches!(err, Error::Protocol(_)));
713    }
714}