Skip to main content

mz_sql_server_util/
desc.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Metadata about tables, columns, and other objects from SQL Server.
11//!
12//! ### Tables
13//!
14//! When creating a SQL Server source we will query system tables from the
15//! upstream instance to get a [`SqlServerTableRaw`]. From this raw information
16//! we create a [`SqlServerTableDesc`] which describes how the upstream table
17//! will get represented in Materialize.
18//!
19//! ### Rows
20//!
21//! With a [`SqlServerTableDesc`] and an [`mz_repr::RelationDesc`] we can
22//! create a [`SqlServerRowDecoder`] which will be used when running a source
23//! to efficiently decode [`tiberius::Row`]s into [`mz_repr::Row`]s.
24
25use base64::Engine;
26use chrono::{NaiveDateTime, SubsecRound};
27use dec::OrderedDecimal;
28use mz_ore::cast::CastFrom;
29use mz_proto::{IntoRustIfSome, ProtoType, RustType};
30use mz_repr::adt::numeric::{Numeric, NumericMaxScale};
31use mz_repr::adt::timestamp::{CheckedTimestamp, TimestampPrecision};
32use mz_repr::adt::varchar::VarCharMaxLength;
33use mz_repr::{Datum, RelationDesc, Row, RowArena, SqlColumnType, SqlScalarType};
34#[cfg(any(test, feature = "proptest"))]
35use proptest_derive::Arbitrary;
36use serde::{Deserialize, Serialize};
37
38use std::collections::BTreeSet;
39use std::sync::Arc;
40
41use crate::desc::proto_sql_server_table_constraint::ConstraintType;
42use crate::{SqlServerDecodeError, SqlServerError};
43
44include!(concat!(env!("OUT_DIR"), "/mz_sql_server_util.rs"));
45
46/// Materialize compatible description of a table in Microsoft SQL Server.
47///
48/// See [`SqlServerTableRaw`] for the raw information we read from the upstream
49/// system.
50///
51/// Note: We map a [`SqlServerTableDesc`] to a Materialize [`RelationDesc`] as
52/// part of purification. Specifically we use this description to generate a
53/// SQL statement for subsource and it's the _parsing of that statement_ which
54/// actually generates a [`RelationDesc`].
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
57pub struct SqlServerTableDesc {
58    /// Name of the schema that the table belongs to.
59    pub schema_name: Arc<str>,
60    /// Name of the table.
61    pub name: Arc<str>,
62    /// Columns for the table.
63    pub columns: Box<[SqlServerColumnDesc]>,
64    /// Constraints for the table.
65    ///
66    /// For new instances, it only holds the constraint backing the capture
67    /// instance's index. SQL Server rejects dropping that index while
68    /// the capture instance is the table's newest one, so we have some
69    /// protection against a dropped constraint.
70    pub constraints: Vec<SqlServerTableConstraint>,
71}
72
73impl SqlServerTableDesc {
74    /// Creating a [`SqlServerTableDesc`] from a [`SqlServerTableRaw`] description.
75    ///
76    /// Note: Not all columns from SQL Server can be ingested into Materialize. To determine if a
77    /// column is supported see [`SqlServerColumnDesc::decode_type`].
78    ///
79    /// Of `raw_constraints`, only the one backing the capture instance's index
80    /// is kept, see [`SqlServerTableDesc::constraints`]. A PRIMARY KEY or
81    /// UNIQUE constraint and the index enforcing it share a name.
82    pub fn new(
83        raw: SqlServerTableRaw,
84        raw_constraints: Vec<SqlServerTableConstraintRaw>,
85    ) -> Result<Self, SqlServerError> {
86        let columns: Box<[_]> = raw
87            .columns
88            .into_iter()
89            .map(SqlServerColumnDesc::new)
90            .collect();
91        let index_name = raw.capture_instance.index_name.as_deref();
92        let constraints = raw_constraints
93            .into_iter()
94            .filter(|constraint| Some(constraint.constraint_name.as_str()) == index_name)
95            .map(SqlServerTableConstraint::try_from)
96            .collect::<Result<Vec<_>, _>>()?;
97        Ok(SqlServerTableDesc {
98            schema_name: raw.schema_name,
99            name: raw.name,
100            columns,
101            constraints,
102        })
103    }
104
105    /// Returns the [`SqlServerQualifiedTableName`] for this [`SqlServerTableDesc`].
106    pub fn qualified_name(&self) -> SqlServerQualifiedTableName {
107        SqlServerQualifiedTableName {
108            schema_name: Arc::clone(&self.schema_name),
109            table_name: Arc::clone(&self.name),
110        }
111    }
112
113    /// Update this [`SqlServerTableDesc`] to represent the specified columns
114    /// as text in Materialize.
115    pub fn apply_text_columns(&mut self, text_columns: &BTreeSet<&str>) {
116        for column in &mut self.columns {
117            if text_columns.contains(column.name.as_ref()) {
118                column.represent_as_text();
119            }
120        }
121    }
122
123    /// Update this [`SqlServerTableDesc`] to exclude the specified columns from being
124    /// replicated into Materialize.
125    pub fn apply_excl_columns(&mut self, excl_columns: &BTreeSet<&str>) {
126        for column in &mut self.columns {
127            if excl_columns.contains(column.name.as_ref()) {
128                column.exclude();
129            }
130        }
131    }
132
133    /// Returns a [`SqlServerRowDecoder`] which can be used to decode [`tiberius::Row`]s into
134    /// [`mz_repr::Row`]s that match the shape of the provided [`RelationDesc`].
135    pub fn decoder(&self, desc: &RelationDesc) -> Result<SqlServerRowDecoder, SqlServerError> {
136        let decoder = SqlServerRowDecoder::try_new(self, desc)?;
137        Ok(decoder)
138    }
139}
140
141impl RustType<ProtoSqlServerTableDesc> for SqlServerTableDesc {
142    fn into_proto(&self) -> ProtoSqlServerTableDesc {
143        ProtoSqlServerTableDesc {
144            name: self.name.to_string(),
145            schema_name: self.schema_name.to_string(),
146            columns: self.columns.iter().map(|c| c.into_proto()).collect(),
147            constraints: self.constraints.iter().map(|c| c.into_proto()).collect(),
148        }
149    }
150
151    fn from_proto(proto: ProtoSqlServerTableDesc) -> Result<Self, mz_proto::TryFromProtoError> {
152        let columns = proto
153            .columns
154            .into_iter()
155            .map(|c| c.into_rust())
156            .collect::<Result<_, _>>()?;
157        let constraints = proto
158            .constraints
159            .into_iter()
160            .map(|c| c.into_rust())
161            .collect::<Result<_, _>>()?;
162        Ok(SqlServerTableDesc {
163            schema_name: proto.schema_name.into(),
164            name: proto.name.into(),
165            columns,
166            constraints,
167        })
168    }
169}
170
171/// SQL Server table constraint type (e.g. PRIMARY KEY, UNIQUE, etc.)
172/// See <https://learn.microsoft.com/en-us/sql/relational-databases/system-information-schema-views/table-constraints-transact-sql?view=sql-server-ver17>
173#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
174#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
175pub enum SqlServerTableConstraintType {
176    PrimaryKey,
177    Unique,
178}
179
180impl TryFrom<String> for SqlServerTableConstraintType {
181    type Error = SqlServerError;
182
183    fn try_from(value: String) -> Result<Self, Self::Error> {
184        match value.as_str() {
185            "PRIMARY KEY" => Ok(Self::PrimaryKey),
186            "UNIQUE" => Ok(Self::Unique),
187            name => Err(SqlServerError::InvalidData {
188                column_name: "constraint_type".into(),
189                error: format!("Unknown constraint type: {name}"),
190            }),
191        }
192    }
193}
194
195impl RustType<proto_sql_server_table_constraint::ConstraintType> for SqlServerTableConstraintType {
196    fn into_proto(&self) -> proto_sql_server_table_constraint::ConstraintType {
197        match self {
198            SqlServerTableConstraintType::PrimaryKey => ConstraintType::PrimaryKey(()),
199            SqlServerTableConstraintType::Unique => ConstraintType::Unique(()),
200        }
201    }
202
203    fn from_proto(
204        proto: proto_sql_server_table_constraint::ConstraintType,
205    ) -> Result<Self, mz_proto::TryFromProtoError> {
206        Ok(match proto {
207            ConstraintType::PrimaryKey(_) => SqlServerTableConstraintType::PrimaryKey,
208            ConstraintType::Unique(_) => SqlServerTableConstraintType::Unique,
209        })
210    }
211}
212
213/// SQL Server table constraint.
214#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
215#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
216pub struct SqlServerTableConstraint {
217    pub constraint_name: String,
218    pub constraint_type: SqlServerTableConstraintType,
219    pub column_names: Vec<String>,
220}
221
222impl TryFrom<SqlServerTableConstraintRaw> for SqlServerTableConstraint {
223    type Error = SqlServerError;
224
225    fn try_from(value: SqlServerTableConstraintRaw) -> Result<Self, Self::Error> {
226        Ok(SqlServerTableConstraint {
227            constraint_name: value.constraint_name,
228            constraint_type: value.constraint_type.try_into()?,
229            column_names: value.columns,
230        })
231    }
232}
233
234impl RustType<ProtoSqlServerTableConstraint> for SqlServerTableConstraint {
235    fn into_proto(&self) -> ProtoSqlServerTableConstraint {
236        ProtoSqlServerTableConstraint {
237            constraint_name: self.constraint_name.clone(),
238            constraint_type: Some(self.constraint_type.into_proto()),
239            column_names: self.column_names.clone(),
240        }
241    }
242
243    fn from_proto(
244        proto: ProtoSqlServerTableConstraint,
245    ) -> Result<Self, mz_proto::TryFromProtoError> {
246        Ok(SqlServerTableConstraint {
247            constraint_name: proto.constraint_name,
248            constraint_type: proto
249                .constraint_type
250                .into_rust_if_some("ProtoSqlServerTableConstraint::constraint_type")?,
251            column_names: proto.column_names,
252        })
253    }
254}
255
256/// Partially qualified name of a table from Microsoft SQL Server.
257///
258/// TODO(sql_server3): Change this to use a &str.
259#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
260pub struct SqlServerQualifiedTableName {
261    pub schema_name: Arc<str>,
262    pub table_name: Arc<str>,
263}
264
265impl ToString for SqlServerQualifiedTableName {
266    fn to_string(&self) -> String {
267        format!(
268            "{}.{}",
269            crate::quote_identifier(&self.schema_name),
270            crate::quote_identifier(&self.table_name)
271        )
272    }
273}
274
275/// Raw metadata for a table from Microsoft SQL Server.
276///
277/// See [`SqlServerTableDesc`] for a refined description that is compatible
278/// with Materialize.
279#[derive(Debug, Clone)]
280pub struct SqlServerTableRaw {
281    /// Name of the schema the table belongs to.
282    pub schema_name: Arc<str>,
283    /// Name of the table.
284    pub name: Arc<str>,
285    /// The capture instance replicating changes.
286    pub capture_instance: Arc<SqlServerCaptureInstanceRaw>,
287    /// Columns for the table.
288    pub columns: Arc<[SqlServerColumnRaw]>,
289}
290
291/// Raw capture instance metadata.
292#[derive(Debug, Clone)]
293pub struct SqlServerCaptureInstanceRaw {
294    /// The capture instance replicating changes.
295    pub name: Arc<str>,
296    /// The creation date of the capture instance.
297    pub create_date: Arc<NaiveDateTime>,
298    /// Name of the unique index SQL Server uses to identify rows in the
299    /// change table (`cdc.change_tables.index_name`). `None` when the source
300    /// table had no primary key when CDC was enabled and no index was given.
301    pub index_name: Option<Arc<str>>,
302}
303
304/// Description of a column from a table in Microsoft SQL Server.
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
307pub struct SqlServerColumnDesc {
308    /// Name of the column.
309    pub name: Arc<str>,
310    /// The intended data type of the this column in Materialize. `None` indicates this
311    /// column should be excluded when replicating into Materialize.
312    ///
313    /// Note: This type might differ from the `decode_type`, e.g. a user can
314    /// specify `TEXT COLUMNS` to decode columns as text.
315    pub column_type: Option<SqlColumnType>,
316    /// This field is deprecated and will be removed in a future version.  This exists only for the
317    /// purpose of migrating from old representations.
318    pub primary_key_constraint: Option<Arc<str>>,
319    /// Rust type we should parse the data from a [`tiberius::Row`] as.
320    pub decode_type: SqlServerColumnDecodeType,
321    /// Raw type of the column as we read it from upstream.
322    ///
323    /// This is useful to keep around for debugging purposes.
324    pub raw_type: Arc<str>,
325}
326
327impl SqlServerColumnDesc {
328    /// Create a [`SqlServerColumnDesc`] from a [`SqlServerColumnRaw`] description.
329    pub fn new(raw: &SqlServerColumnRaw) -> Self {
330        let (column_type, decode_type) = match parse_data_type(raw) {
331            Ok((scalar_type, decode_type)) => {
332                let column_type = scalar_type.nullable(raw.is_nullable);
333                (Some(column_type), decode_type)
334            }
335            Err(err) => {
336                tracing::warn!(
337                    ?err,
338                    ?raw,
339                    "found an unsupported data type when parsing raw data"
340                );
341                (
342                    None,
343                    SqlServerColumnDecodeType::Unsupported {
344                        context: err.reason,
345                    },
346                )
347            }
348        };
349        SqlServerColumnDesc {
350            name: Arc::clone(&raw.name),
351            primary_key_constraint: None,
352            column_type,
353            decode_type,
354            raw_type: Arc::clone(&raw.data_type),
355        }
356    }
357
358    /// Change this [`SqlServerColumnDesc`] to be represented as text in Materialize.
359    pub fn represent_as_text(&mut self) {
360        self.column_type = self
361            .column_type
362            .as_ref()
363            .map(|ct| SqlScalarType::String.nullable(ct.nullable));
364    }
365
366    /// Exclude this [`SqlServerColumnDesc`] from being replicated into Materialize.
367    pub fn exclude(&mut self) {
368        self.column_type = None;
369    }
370
371    /// Check if this [`SqlServerColumnDesc`] is excluded from being replicated into Materialize.
372    pub fn is_excluded(&self) -> bool {
373        self.column_type.is_none()
374    }
375}
376
377impl RustType<ProtoSqlServerColumnDesc> for SqlServerColumnDesc {
378    fn into_proto(&self) -> ProtoSqlServerColumnDesc {
379        ProtoSqlServerColumnDesc {
380            name: self.name.to_string(),
381            column_type: self.column_type.into_proto(),
382            primary_key_constraint: self.primary_key_constraint.as_ref().map(|v| v.to_string()),
383            decode_type: Some(self.decode_type.into_proto()),
384            raw_type: self.raw_type.to_string(),
385        }
386    }
387
388    fn from_proto(proto: ProtoSqlServerColumnDesc) -> Result<Self, mz_proto::TryFromProtoError> {
389        Ok(SqlServerColumnDesc {
390            name: proto.name.into(),
391            column_type: proto.column_type.into_rust()?,
392            primary_key_constraint: proto.primary_key_constraint.map(|v| v.into()),
393            decode_type: proto
394                .decode_type
395                .into_rust_if_some("ProtoSqlServerColumnDesc::decode_type")?,
396            raw_type: proto.raw_type.into(),
397        })
398    }
399}
400
401/// The raw datatype from SQL Server is not supported in Materialize.
402#[derive(Debug)]
403#[allow(dead_code)]
404pub struct UnsupportedDataType {
405    column_name: String,
406    column_type: String,
407    reason: String,
408}
409
410/// Parse a raw data type from SQL Server into a Materialize [`SqlScalarType`].
411///
412/// Returns the [`SqlScalarType`] that we'll map this column to and the [`SqlServerColumnDecodeType`]
413/// that we use to decode the raw value.
414fn parse_data_type(
415    raw: &SqlServerColumnRaw,
416) -> Result<(SqlScalarType, SqlServerColumnDecodeType), UnsupportedDataType> {
417    // The value of a computed column, persisted or not, will be readable by the snapshot, but will
418    // always be NULL in the CDC stream.  This can lead to issues in MZ (e.g. decoding errors,
419    // negative accumulations, etc.).
420    if raw.is_computed {
421        return Err(UnsupportedDataType {
422            column_name: raw.name.to_string(),
423            column_type: format!("{} (computed)", raw.data_type.to_lowercase()),
424            reason: "column is computed".into(),
425        });
426    }
427
428    let scalar = match raw.data_type.to_lowercase().as_str() {
429        "tinyint" => (SqlScalarType::Int16, SqlServerColumnDecodeType::U8),
430        "smallint" => (SqlScalarType::Int16, SqlServerColumnDecodeType::I16),
431        "int" => (SqlScalarType::Int32, SqlServerColumnDecodeType::I32),
432        "bigint" => (SqlScalarType::Int64, SqlServerColumnDecodeType::I64),
433        "bit" => (SqlScalarType::Bool, SqlServerColumnDecodeType::Bool),
434        "decimal" | "numeric" | "money" | "smallmoney" => {
435            // SQL Server supports a precision in the range of [1, 38] and then
436            // the scale is 0 <= scale <= precision.
437            //
438            // Materialize numerics are floating point with a fixed precision of 39.
439            //
440            // See: <https://learn.microsoft.com/en-us/sql/t-sql/data-types/decimal-and-numeric-transact-sql?view=sql-server-ver16#arguments>
441            if raw.precision > 38 || raw.scale > raw.precision {
442                tracing::warn!(
443                    "unexpected value from SQL Server, precision of {} and scale of {}",
444                    raw.precision,
445                    raw.scale,
446                );
447            }
448            if raw.precision > 39 {
449                let reason = format!(
450                    "precision of {} is greater than our maximum of 39",
451                    raw.precision
452                );
453                return Err(UnsupportedDataType {
454                    column_name: raw.name.to_string(),
455                    column_type: raw.data_type.to_string(),
456                    reason,
457                });
458            }
459
460            let raw_scale = usize::cast_from(raw.scale);
461            let max_scale =
462                NumericMaxScale::try_from(raw_scale).map_err(|_| UnsupportedDataType {
463                    column_type: raw.data_type.to_string(),
464                    column_name: raw.name.to_string(),
465                    reason: format!("scale of {} is too large", raw.scale),
466                })?;
467            let column_type = SqlScalarType::Numeric {
468                max_scale: Some(max_scale),
469            };
470
471            (column_type, SqlServerColumnDecodeType::Numeric)
472        }
473        // SQL Server has a few IEEE 754 floating point type names. The underlying type is float(n),
474        // where n is the number of bits used. SQL Server still ends up with only 2 distinct types
475        // as it treats 1 <= n <= 24 as n=24, and 25 <= n <= 53 as n=53.
476        //
477        // Additionally, `real` and `double precision` exist as synonyms of float(24) and float(53),
478        // respectively.  What doesn't appear to be documented is how these appear in `sys.types`.
479        // See <https://learn.microsoft.com/en-us/sql/t-sql/data-types/float-and-real-transact-sql?view=sql-server-ver17>
480        "real" | "float" | "double precision" => match raw.max_length {
481            // Decide the MZ type based on the number of bytes rather than the name, just in case
482            // there is inconsistency among versions.
483            4 => (SqlScalarType::Float32, SqlServerColumnDecodeType::F32),
484            8 => (SqlScalarType::Float64, SqlServerColumnDecodeType::F64),
485            _ => {
486                return Err(UnsupportedDataType {
487                    column_name: raw.name.to_string(),
488                    column_type: raw.data_type.to_string(),
489                    reason: format!("unsupported length {}", raw.max_length),
490                });
491            }
492        },
493        "char" | "nchar" | "sysname" => {
494            // There isn't a char(max) or nchar(max), so it isn't clear if this condition
495            // is possible.
496            if raw.max_length == -1 {
497                return Err(UnsupportedDataType {
498                    column_name: raw.name.to_string(),
499                    column_type: raw.data_type.to_string(),
500                    reason: "columns with unlimited size do not support CDC".to_string(),
501                });
502            }
503
504            // We represent these as "text" rather than a fixed-length
505            // `character(n)`. SQL Server sizes them in bytes, while
506            // Materialize's `Char` length is a character count. Under a
507            // multi-byte collation a single character can span several
508            // bytes, so `sys.columns.max_length` is not a usable character
509            // count. Rather than guess, we pass the value through as-is.
510            (SqlScalarType::String, SqlServerColumnDecodeType::String)
511        }
512        "varchar" | "nvarchar" => {
513            // `max text repl size` is 64KB by default.  If a user attempts to insert a value
514            // that exceeds this limit, SQL Server will return an error and the insert fails
515            // with error `7139`.  This is also true for updates that increase the field length
516            // beyond the limit.
517            //
518            // See <https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors-7000-to-7999?view=sql-server-ver17>
519            //
520            // If the `max text repl size` changes, it does not affect events already written to
521            // the CDC table, nor does it change the behavior of what CDC captures for updates
522            // to non-LOB columns (based on testing).
523            let max_length = if raw.max_length != -1 {
524                let length =
525                    VarCharMaxLength::try_from(i64::from(raw.max_length)).map_err(|e| {
526                        UnsupportedDataType {
527                            column_name: raw.name.to_string(),
528                            column_type: raw.data_type.to_string(),
529                            reason: e.to_string(),
530                        }
531                    })?;
532                Some(length)
533            } else {
534                None
535            };
536            let column_type = SqlScalarType::VarChar { max_length };
537            (column_type, SqlServerColumnDecodeType::String)
538        }
539        "text" | "ntext" | "image" => {
540            // SQL Server docs indicate this should always be 16. There's no
541            // issue if it's not, but it's good to track.
542            mz_ore::soft_assert_eq_no_log!(raw.max_length, 16);
543
544            // TODO(sql_server3): Support UPSERT semantics for SQL Server.
545            return Err(UnsupportedDataType {
546                column_name: raw.name.to_string(),
547                column_type: raw.data_type.to_string(),
548                reason: "columns with unlimited size do not support CDC".to_string(),
549            });
550        }
551        "xml" => {
552            // When the `max_length` is -1 SQL Server will not present us with the "before" value
553            // for updated columns.
554            //
555            // TODO(sql_server3): Support UPSERT semantics for SQL Server.
556            if raw.max_length == -1 {
557                return Err(UnsupportedDataType {
558                    column_name: raw.name.to_string(),
559                    column_type: raw.data_type.to_string(),
560                    reason: "columns with unlimited size do not support CDC".to_string(),
561                });
562            }
563            (SqlScalarType::String, SqlServerColumnDecodeType::Xml)
564        }
565        "binary" | "varbinary" => {
566            // [`SqlScalarType`] does not support tracking max_length for binary data. To ensure
567            // columns of type varbinary(max) (Large Object) are decoded properly, it is
568            // necessary to know that the length is `max`. `varchar` and `nvarchar` track this
569            // using [`SqlScalarType::VarChar`] max_length field.
570            if raw.max_length == -1 {
571                return Err(UnsupportedDataType {
572                    column_name: raw.name.to_string(),
573                    column_type: raw.data_type.to_string(),
574                    reason: "columns with unlimited size do not support CDC".to_string(),
575                });
576            }
577            (SqlScalarType::Bytes, SqlServerColumnDecodeType::Bytes)
578        }
579        "json" => (SqlScalarType::Jsonb, SqlServerColumnDecodeType::String),
580        "date" => (SqlScalarType::Date, SqlServerColumnDecodeType::NaiveDate),
581        // SQL Server supports a scale of (and defaults to) 7 digits (aka 100 nanoseconds)
582        // for time related types.
583        //
584        // Internally Materialize supports a scale of 9 (aka nanoseconds), but for Postgres
585        // compatibility we constraint ourselves to a scale of 6 (aka microseconds). By
586        // default we will round values we get from  SQL Server to fit in Materialize.
587        //
588        // TODO(sql_server3): Support a "strict" mode where we're fail the creation of the
589        // source if the scale is too large.
590        // TODO(sql_server3): Support specifying a precision for SqlScalarType::Time.
591        //
592        // See: <https://learn.microsoft.com/en-us/sql/t-sql/data-types/datetime2-transact-sql?view=sql-server-ver16>.
593        "time" => (SqlScalarType::Time, SqlServerColumnDecodeType::NaiveTime),
594        dt @ ("smalldatetime" | "datetime" | "datetime2" | "datetimeoffset") => {
595            if raw.scale > 7 {
596                tracing::warn!("unexpected scale '{}' from SQL Server", raw.scale);
597            }
598            if raw.scale > mz_repr::adt::timestamp::MAX_PRECISION {
599                tracing::warn!("truncating scale of '{}' for '{}'", raw.scale, dt);
600            }
601            let precision = std::cmp::min(raw.scale, mz_repr::adt::timestamp::MAX_PRECISION);
602            let precision =
603                Some(TimestampPrecision::try_from(i64::from(precision)).expect("known to fit"));
604
605            match dt {
606                "smalldatetime" | "datetime" | "datetime2" => (
607                    SqlScalarType::Timestamp { precision },
608                    SqlServerColumnDecodeType::NaiveDateTime,
609                ),
610                "datetimeoffset" => (
611                    SqlScalarType::TimestampTz { precision },
612                    SqlServerColumnDecodeType::DateTime,
613                ),
614                other => unreachable!("'{other}' checked above"),
615            }
616        }
617        "uniqueidentifier" => (SqlScalarType::Uuid, SqlServerColumnDecodeType::Uuid),
618        // TODO(sql_server3): Support reading the following types, at least as text:
619        //
620        // * geography
621        // * geometry
622        // * json (preview)
623        // * vector (preview)
624        //
625        // None of these types are implemented in `tiberius`, the crate that
626        // provides our SQL Server client, so we'll need to implement support
627        // for decoding them.
628        //
629        // See <https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/355f7890-6e91-4978-ab76-2ded17ee09bc>.
630        other => {
631            return Err(UnsupportedDataType {
632                column_type: other.to_string(),
633                column_name: raw.name.to_string(),
634                reason: format!("'{other}' is unimplemented"),
635            });
636        }
637    };
638    Ok(scalar)
639}
640
641/// Raw metadata for a column from a table in Microsoft SQL Server.
642///
643/// See: <https://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-columns-transact-sql?view=sql-server-ver16>.
644#[derive(Clone, Debug)]
645pub struct SqlServerColumnRaw {
646    /// Name of this column.
647    pub name: Arc<str>,
648    /// Name of the data type.
649    pub data_type: Arc<str>,
650    /// Whether or not the column is nullable.
651    pub is_nullable: bool,
652    /// Maximum length (in bytes) of the column.
653    ///
654    /// For `varchar(max)`, `nvarchar(max)`, `varbinary(max)`, or `xml` this will be `-1`. For
655    /// `text`, `ntext`, and `image` columns this will be 16.
656    ///
657    /// See: <https://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-columns-transact-sql?view=sql-server-ver16>.
658    ///
659    /// TODO(sql_server2): Validate this value for `json` columns where were introduced
660    /// Azure SQL 2024.
661    pub max_length: i16,
662    /// Precision of the column, if numeric-based; otherwise 0.
663    pub precision: u8,
664    /// Scale of the columns, if numeric-based; otherwise 0.
665    pub scale: u8,
666    /// Whether the column is computed.
667    pub is_computed: bool,
668}
669
670/// Raw metadata for a table constraint.
671#[derive(Clone, Debug)]
672pub struct SqlServerTableConstraintRaw {
673    pub constraint_name: String,
674    pub constraint_type: String,
675    pub columns: Vec<String>,
676}
677
678/// Rust type that we should use when reading a column from SQL Server.
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
680#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
681pub enum SqlServerColumnDecodeType {
682    Bool,
683    U8,
684    I16,
685    I32,
686    I64,
687    F32,
688    F64,
689    String,
690    Bytes,
691    /// [`uuid::Uuid`].
692    Uuid,
693    /// [`tiberius::numeric::Numeric`].
694    Numeric,
695    /// [`tiberius::xml::XmlData`].
696    Xml,
697    /// [`chrono::NaiveDate`].
698    NaiveDate,
699    /// [`chrono::NaiveTime`].
700    NaiveTime,
701    /// [`chrono::DateTime`].
702    DateTime,
703    /// [`chrono::NaiveDateTime`].
704    NaiveDateTime,
705    /// Decoding this type isn't supported.
706    Unsupported {
707        /// Any additional context as to why this type isn't supported.
708        context: String,
709    },
710}
711
712impl SqlServerColumnDecodeType {
713    /// Decode the column with `name` out of the provided `data`.
714    pub fn decode<'a>(
715        &self,
716        data: &'a tiberius::Row,
717        name: &'a str,
718        column: &'a SqlColumnType,
719        arena: &'a RowArena,
720    ) -> Result<Datum<'a>, SqlServerDecodeError> {
721        let maybe_datum = match (&column.scalar_type, self) {
722            (SqlScalarType::Bool, SqlServerColumnDecodeType::Bool) => data
723                .try_get(name)
724                .map_err(|_| SqlServerDecodeError::invalid_column(name, "bool"))?
725                .map(|val: bool| if val { Datum::True } else { Datum::False }),
726            (SqlScalarType::Int16, SqlServerColumnDecodeType::U8) => data
727                .try_get(name)
728                .map_err(|_| SqlServerDecodeError::invalid_column(name, "u8"))?
729                .map(|val: u8| Datum::Int16(i16::cast_from(val))),
730            (SqlScalarType::Int16, SqlServerColumnDecodeType::I16) => data
731                .try_get(name)
732                .map_err(|_| SqlServerDecodeError::invalid_column(name, "i16"))?
733                .map(Datum::Int16),
734            (SqlScalarType::Int32, SqlServerColumnDecodeType::I32) => data
735                .try_get(name)
736                .map_err(|_| SqlServerDecodeError::invalid_column(name, "i32"))?
737                .map(Datum::Int32),
738            (SqlScalarType::Int64, SqlServerColumnDecodeType::I64) => data
739                .try_get(name)
740                .map_err(|_| SqlServerDecodeError::invalid_column(name, "i64"))?
741                .map(Datum::Int64),
742            (SqlScalarType::Float32, SqlServerColumnDecodeType::F32) => data
743                .try_get(name)
744                .map_err(|_| SqlServerDecodeError::invalid_column(name, "f32"))?
745                .map(|val: f32| Datum::Float32(ordered_float::OrderedFloat(val))),
746            (SqlScalarType::Float64, SqlServerColumnDecodeType::F64) => data
747                .try_get(name)
748                .map_err(|_| SqlServerDecodeError::invalid_column(name, "f64"))?
749                .map(|val: f64| Datum::Float64(ordered_float::OrderedFloat(val))),
750            (SqlScalarType::String, SqlServerColumnDecodeType::String) => data
751                .try_get(name)
752                .map_err(|_| SqlServerDecodeError::invalid_column(name, "string"))?
753                .map(Datum::String),
754            // `char`, `nchar`, and `sysname` columns now map to `String`, so
755            // new sources never produce this type. It remains to decode sources
756            // created before that change, whose persisted desc still carries
757            // `Char { length }`.
758            (SqlScalarType::Char { length }, SqlServerColumnDecodeType::String) => data
759                .try_get(name)
760                .map_err(|_| SqlServerDecodeError::invalid_column(name, "char"))?
761                .map(|val: &str| match length {
762                    // SQL Server sizes `char(n)` in bytes, and `length` (from
763                    // `sys.columns.max_length`) is that byte count, while
764                    // Materialize's `Char` length is a character count. SQL
765                    // Server blank-pads the value to fill the byte length, so a
766                    // multi-byte collation yields fewer characters than bytes.
767                    // The character count can therefore be at most `length`,
768                    // never more, so we only reject when it exceeds `length`.
769                    //
770                    // This encodes at a different length in Materialize vs.
771                    // the upstream, which is why it is deprecated. An
772                    // upstream CHAR(10) column with the string "café" would
773                    // have 5 trailing spaces. In Materialize, this would
774                    // have 6 trailing spaces.
775                    Some(max) => {
776                        let found_chars = val.chars().count();
777                        let max_chars = usize::cast_from(max.into_u32());
778                        if found_chars > max_chars {
779                            Err(SqlServerDecodeError::invalid_char(
780                                name,
781                                max_chars,
782                                found_chars,
783                            ))
784                        } else {
785                            Ok(Datum::String(val))
786                        }
787                    }
788                    None => Ok(Datum::String(val)),
789                })
790                .transpose()?,
791            (SqlScalarType::VarChar { max_length }, SqlServerColumnDecodeType::String) => data
792                .try_get(name)
793                .map_err(|_| SqlServerDecodeError::invalid_column(name, "varchar"))?
794                .map(|val: &str| match max_length {
795                    Some(max) => {
796                        let found_chars = val.chars().count();
797                        let max_chars = usize::cast_from(max.into_u32());
798                        if found_chars > max_chars {
799                            Err(SqlServerDecodeError::invalid_varchar(
800                                name,
801                                max_chars,
802                                found_chars,
803                            ))
804                        } else {
805                            Ok(Datum::String(val))
806                        }
807                    }
808                    None => Ok(Datum::String(val)),
809                })
810                .transpose()?,
811            (SqlScalarType::Bytes, SqlServerColumnDecodeType::Bytes) => data
812                .try_get(name)
813                .map_err(|_| SqlServerDecodeError::invalid_column(name, "bytes"))?
814                .map(Datum::Bytes),
815            (SqlScalarType::Uuid, SqlServerColumnDecodeType::Uuid) => data
816                .try_get(name)
817                .map_err(|_| SqlServerDecodeError::invalid_column(name, "uuid"))?
818                .map(Datum::Uuid),
819            (SqlScalarType::Numeric { .. }, SqlServerColumnDecodeType::Numeric) => data
820                .try_get(name)
821                .map_err(|_| SqlServerDecodeError::invalid_column(name, "numeric"))?
822                .map(|val: tiberius::numeric::Numeric| {
823                    let numeric = tiberius_numeric_to_mz_numeric(val);
824                    Datum::Numeric(OrderedDecimal(numeric))
825                }),
826            (SqlScalarType::String, SqlServerColumnDecodeType::Xml) => data
827                .try_get(name)
828                .map_err(|_| SqlServerDecodeError::invalid_column(name, "xml"))?
829                .map(|val: &tiberius::xml::XmlData| Datum::String(val.as_ref())),
830            (SqlScalarType::Date, SqlServerColumnDecodeType::NaiveDate) => data
831                .try_get(name)
832                .map_err(|_| SqlServerDecodeError::invalid_column(name, "date"))?
833                .map(|val: chrono::NaiveDate| {
834                    let date = val
835                        .try_into()
836                        .map_err(|e| SqlServerDecodeError::invalid_date(name, e))?;
837                    Ok::<_, SqlServerDecodeError>(Datum::Date(date))
838                })
839                .transpose()?,
840            (SqlScalarType::Time, SqlServerColumnDecodeType::NaiveTime) => data
841                .try_get(name)
842                .map_err(|_| SqlServerDecodeError::invalid_column(name, "time"))?
843                .map(|val: chrono::NaiveTime| {
844                    // Postgres' maximum precision is 6 (aka microseconds).
845                    //
846                    // While the Postgres spec supports specifying a precision
847                    // Materialize does not.
848                    let rounded = val.round_subsecs(6);
849                    // Overflowed.
850                    let val = if rounded < val {
851                        val.trunc_subsecs(6)
852                    } else {
853                        val
854                    };
855                    Datum::Time(val)
856                }),
857            (SqlScalarType::Timestamp { precision }, SqlServerColumnDecodeType::NaiveDateTime) => {
858                data.try_get(name)
859                    .map_err(|_| SqlServerDecodeError::invalid_column(name, "timestamp"))?
860                    .map(|val: chrono::NaiveDateTime| {
861                        let ts: CheckedTimestamp<chrono::NaiveDateTime> = val
862                            .try_into()
863                            .map_err(|e| SqlServerDecodeError::invalid_timestamp(name, e))?;
864                        let rounded = ts
865                            .round_to_precision(*precision)
866                            .map_err(|e| SqlServerDecodeError::invalid_timestamp(name, e))?;
867                        Ok::<_, SqlServerDecodeError>(Datum::Timestamp(rounded))
868                    })
869                    .transpose()?
870            }
871            (SqlScalarType::TimestampTz { precision }, SqlServerColumnDecodeType::DateTime) => data
872                .try_get(name)
873                .map_err(|_| SqlServerDecodeError::invalid_column(name, "timestamptz"))?
874                .map(|val: chrono::DateTime<chrono::Utc>| {
875                    let ts: CheckedTimestamp<chrono::DateTime<chrono::Utc>> = val
876                        .try_into()
877                        .map_err(|e| SqlServerDecodeError::invalid_timestamp(name, e))?;
878                    let rounded = ts
879                        .round_to_precision(*precision)
880                        .map_err(|e| SqlServerDecodeError::invalid_timestamp(name, e))?;
881                    Ok::<_, SqlServerDecodeError>(Datum::TimestampTz(rounded))
882                })
883                .transpose()?,
884            // We support mapping any type to a string.
885            (SqlScalarType::String, SqlServerColumnDecodeType::Bool) => data
886                .try_get(name)
887                .map_err(|_| SqlServerDecodeError::invalid_column(name, "bool-text"))?
888                .map(|val: bool| {
889                    if val {
890                        Datum::String("true")
891                    } else {
892                        Datum::String("false")
893                    }
894                }),
895            (SqlScalarType::String, SqlServerColumnDecodeType::U8) => data
896                .try_get(name)
897                .map_err(|_| SqlServerDecodeError::invalid_column(name, "u8-text"))?
898                .map(|val: u8| {
899                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
900                }),
901            (SqlScalarType::String, SqlServerColumnDecodeType::I16) => data
902                .try_get(name)
903                .map_err(|_| SqlServerDecodeError::invalid_column(name, "i16-text"))?
904                .map(|val: i16| {
905                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
906                }),
907            (SqlScalarType::String, SqlServerColumnDecodeType::I32) => data
908                .try_get(name)
909                .map_err(|_| SqlServerDecodeError::invalid_column(name, "i32-text"))?
910                .map(|val: i32| {
911                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
912                }),
913            (SqlScalarType::String, SqlServerColumnDecodeType::I64) => data
914                .try_get(name)
915                .map_err(|_| SqlServerDecodeError::invalid_column(name, "i64-text"))?
916                .map(|val: i64| {
917                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
918                }),
919            (SqlScalarType::String, SqlServerColumnDecodeType::F32) => data
920                .try_get(name)
921                .map_err(|_| SqlServerDecodeError::invalid_column(name, "f32-text"))?
922                .map(|val: f32| {
923                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
924                }),
925            (SqlScalarType::String, SqlServerColumnDecodeType::F64) => data
926                .try_get(name)
927                .map_err(|_| SqlServerDecodeError::invalid_column(name, "f64-text"))?
928                .map(|val: f64| {
929                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
930                }),
931            (SqlScalarType::String, SqlServerColumnDecodeType::Uuid) => data
932                .try_get(name)
933                .map_err(|_| SqlServerDecodeError::invalid_column(name, "uuid-text"))?
934                .map(|val: uuid::Uuid| {
935                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
936                }),
937            (SqlScalarType::String, SqlServerColumnDecodeType::Bytes) => data
938                .try_get(name)
939                .map_err(|_| SqlServerDecodeError::invalid_column(name, "bytes-text"))?
940                .map(|val: &[u8]| {
941                    let encoded = base64::engine::general_purpose::STANDARD.encode(val);
942                    arena.make_datum(|packer| packer.push(Datum::String(&encoded)))
943                }),
944            (SqlScalarType::String, SqlServerColumnDecodeType::Numeric) => data
945                .try_get(name)
946                .map_err(|_| SqlServerDecodeError::invalid_column(name, "numeric-text"))?
947                .map(|val: tiberius::numeric::Numeric| {
948                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
949                }),
950            (SqlScalarType::String, SqlServerColumnDecodeType::NaiveDate) => data
951                .try_get(name)
952                .map_err(|_| SqlServerDecodeError::invalid_column(name, "naivedate-text"))?
953                .map(|val: chrono::NaiveDate| {
954                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
955                }),
956            (SqlScalarType::String, SqlServerColumnDecodeType::NaiveTime) => data
957                .try_get(name)
958                .map_err(|_| SqlServerDecodeError::invalid_column(name, "naivetime-text"))?
959                .map(|val: chrono::NaiveTime| {
960                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
961                }),
962            (SqlScalarType::String, SqlServerColumnDecodeType::DateTime) => data
963                .try_get(name)
964                .map_err(|_| SqlServerDecodeError::invalid_column(name, "datetime-text"))?
965                .map(|val: chrono::DateTime<chrono::Utc>| {
966                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
967                }),
968            (SqlScalarType::String, SqlServerColumnDecodeType::NaiveDateTime) => data
969                .try_get(name)
970                .map_err(|_| SqlServerDecodeError::invalid_column(name, "naivedatetime-text"))?
971                .map(|val: chrono::NaiveDateTime| {
972                    arena.make_datum(|packer| packer.push(Datum::String(&val.to_string())))
973                }),
974            (column_type, decode_type) => {
975                return Err(SqlServerDecodeError::Unsupported {
976                    sql_server_type: decode_type.clone(),
977                    mz_type: column_type.clone(),
978                });
979            }
980        };
981
982        match (maybe_datum, column.nullable) {
983            (Some(datum), _) => Ok(datum),
984            (None, true) => Ok(Datum::Null),
985            (None, false) => Err(SqlServerDecodeError::InvalidData {
986                column_name: name.to_string(),
987                // Note: This error string is durably recorded in Persist, do not change.
988                error: "found Null in non-nullable column".to_string(),
989            }),
990        }
991    }
992}
993
994impl RustType<proto_sql_server_column_desc::DecodeType> for SqlServerColumnDecodeType {
995    fn into_proto(&self) -> proto_sql_server_column_desc::DecodeType {
996        match self {
997            SqlServerColumnDecodeType::Bool => proto_sql_server_column_desc::DecodeType::Bool(()),
998            SqlServerColumnDecodeType::U8 => proto_sql_server_column_desc::DecodeType::U8(()),
999            SqlServerColumnDecodeType::I16 => proto_sql_server_column_desc::DecodeType::I16(()),
1000            SqlServerColumnDecodeType::I32 => proto_sql_server_column_desc::DecodeType::I32(()),
1001            SqlServerColumnDecodeType::I64 => proto_sql_server_column_desc::DecodeType::I64(()),
1002            SqlServerColumnDecodeType::F32 => proto_sql_server_column_desc::DecodeType::F32(()),
1003            SqlServerColumnDecodeType::F64 => proto_sql_server_column_desc::DecodeType::F64(()),
1004            SqlServerColumnDecodeType::String => {
1005                proto_sql_server_column_desc::DecodeType::String(())
1006            }
1007            SqlServerColumnDecodeType::Bytes => proto_sql_server_column_desc::DecodeType::Bytes(()),
1008            SqlServerColumnDecodeType::Uuid => proto_sql_server_column_desc::DecodeType::Uuid(()),
1009            SqlServerColumnDecodeType::Numeric => {
1010                proto_sql_server_column_desc::DecodeType::Numeric(())
1011            }
1012            SqlServerColumnDecodeType::Xml => proto_sql_server_column_desc::DecodeType::Xml(()),
1013            SqlServerColumnDecodeType::NaiveDate => {
1014                proto_sql_server_column_desc::DecodeType::NaiveDate(())
1015            }
1016            SqlServerColumnDecodeType::NaiveTime => {
1017                proto_sql_server_column_desc::DecodeType::NaiveTime(())
1018            }
1019            SqlServerColumnDecodeType::DateTime => {
1020                proto_sql_server_column_desc::DecodeType::DateTime(())
1021            }
1022            SqlServerColumnDecodeType::NaiveDateTime => {
1023                proto_sql_server_column_desc::DecodeType::NaiveDateTime(())
1024            }
1025            SqlServerColumnDecodeType::Unsupported { context } => {
1026                proto_sql_server_column_desc::DecodeType::Unsupported(context.clone())
1027            }
1028        }
1029    }
1030
1031    fn from_proto(
1032        proto: proto_sql_server_column_desc::DecodeType,
1033    ) -> Result<Self, mz_proto::TryFromProtoError> {
1034        let val = match proto {
1035            proto_sql_server_column_desc::DecodeType::Bool(()) => SqlServerColumnDecodeType::Bool,
1036            proto_sql_server_column_desc::DecodeType::U8(()) => SqlServerColumnDecodeType::U8,
1037            proto_sql_server_column_desc::DecodeType::I16(()) => SqlServerColumnDecodeType::I16,
1038            proto_sql_server_column_desc::DecodeType::I32(()) => SqlServerColumnDecodeType::I32,
1039            proto_sql_server_column_desc::DecodeType::I64(()) => SqlServerColumnDecodeType::I64,
1040            proto_sql_server_column_desc::DecodeType::F32(()) => SqlServerColumnDecodeType::F32,
1041            proto_sql_server_column_desc::DecodeType::F64(()) => SqlServerColumnDecodeType::F64,
1042            proto_sql_server_column_desc::DecodeType::String(()) => {
1043                SqlServerColumnDecodeType::String
1044            }
1045            proto_sql_server_column_desc::DecodeType::Bytes(()) => SqlServerColumnDecodeType::Bytes,
1046            proto_sql_server_column_desc::DecodeType::Uuid(()) => SqlServerColumnDecodeType::Uuid,
1047            proto_sql_server_column_desc::DecodeType::Numeric(()) => {
1048                SqlServerColumnDecodeType::Numeric
1049            }
1050            proto_sql_server_column_desc::DecodeType::Xml(()) => SqlServerColumnDecodeType::Xml,
1051            proto_sql_server_column_desc::DecodeType::NaiveDate(()) => {
1052                SqlServerColumnDecodeType::NaiveDate
1053            }
1054            proto_sql_server_column_desc::DecodeType::NaiveTime(()) => {
1055                SqlServerColumnDecodeType::NaiveTime
1056            }
1057            proto_sql_server_column_desc::DecodeType::DateTime(()) => {
1058                SqlServerColumnDecodeType::DateTime
1059            }
1060            proto_sql_server_column_desc::DecodeType::NaiveDateTime(()) => {
1061                SqlServerColumnDecodeType::NaiveDateTime
1062            }
1063            proto_sql_server_column_desc::DecodeType::Unsupported(context) => {
1064                SqlServerColumnDecodeType::Unsupported { context }
1065            }
1066        };
1067        Ok(val)
1068    }
1069}
1070
1071/// Numerics in SQL Server have a maximum precision of 38 digits, where [`Numeric`]s in
1072/// Materialize have a maximum precision of 39 digits, so this conversion is infallible.
1073fn tiberius_numeric_to_mz_numeric(val: tiberius::numeric::Numeric) -> Numeric {
1074    let mut numeric = mz_repr::adt::numeric::cx_datum().from_i128(val.value());
1075    // Use scaleb to adjust the exponent directly, avoiding precision loss from division
1076    // scaleb(x, -n) computes x * 10^(-n)
1077    mz_repr::adt::numeric::cx_datum().scaleb(&mut numeric, &Numeric::from(-i32::from(val.scale())));
1078    numeric
1079}
1080
1081/// The update mask of a CDC event row, returned by `cdc.fn_cdc_get_all_changes_<capture_instance>`
1082/// as `__$update_mask`.
1083///
1084/// See <https://learn.microsoft.com/en-us/sql/relational-databases/system-functions/cdc-fn-cdc-get-all-changes-capture-instance-transact-sql?view=sql-server-ver17>
1085#[derive(Debug)]
1086pub struct UpdateMask {
1087    mask: Vec<u8>,
1088}
1089
1090impl TryFrom<&tiberius::Row> for UpdateMask {
1091    type Error = SqlServerDecodeError;
1092
1093    fn try_from(row: &tiberius::Row) -> Result<Self, Self::Error> {
1094        static UPDATE_MASK: &str = "__$update_mask";
1095
1096        let mask: Vec<u8> = row
1097            .try_get::<&[u8], _>(UPDATE_MASK)
1098            .inspect_err(|e| tracing::warn!("Failed extracting update mask: {e:?}"))
1099            .map_err(|_| SqlServerDecodeError::InvalidColumn {
1100                column_name: UPDATE_MASK.to_string(),
1101                as_type: "bytes",
1102            })?
1103            .ok_or_else(|| SqlServerDecodeError::InvalidData {
1104                column_name: UPDATE_MASK.to_string(),
1105                error: "column cannot be null".to_string(),
1106            })?
1107            .into();
1108        Ok(UpdateMask { mask })
1109    }
1110}
1111
1112impl UpdateMask {
1113    /// Returns true if the data column was updated, false otherwise.
1114    ///
1115    /// This function panics if `col_index` exceeds the mask.
1116    ///
1117    /// The [`tiberius::Row`] returned by `cdc.fn_cdc_get_all_changes_<capture_instance>` contains
1118    /// 4 metadata columns used by CDC:
1119    /// - `__$start_lsn`
1120    /// - `__$seqval`
1121    /// - `__$operation`
1122    /// - `__$update_mask`
1123    ///
1124    /// This function will always return false for the first 4 columns.
1125    pub fn data_col_updated(&self, col_index: usize) -> bool {
1126        const CDC_METADATA_COL_COUNT: usize = 4;
1127
1128        if col_index < CDC_METADATA_COL_COUNT {
1129            return false;
1130        }
1131        let adj_col_index = col_index - CDC_METADATA_COL_COUNT;
1132        let byte_offset = adj_col_index / usize::cast_from(u8::BITS);
1133        assert!(
1134            byte_offset < self.mask.len(),
1135            "byte_offset = {byte_offset} mask_len = {}",
1136            self.mask.len()
1137        );
1138        let bit_offset = adj_col_index % usize::cast_from(u8::BITS);
1139        (self.mask[self.mask.len() - byte_offset - 1] >> bit_offset) & 1 == 1
1140    }
1141}
1142
1143/// A decoder from [`tiberius::Row`] to [`mz_repr::Row`].
1144///
1145/// The goal of this type is to perform any expensive "downcasts" so in the hot
1146/// path of decoding rows we do the minimal amount of work.
1147#[derive(Debug)]
1148pub struct SqlServerRowDecoder {
1149    decoders: Vec<(Arc<str>, SqlColumnType, SqlServerColumnDecodeType, bool)>,
1150    needs_update_mask: bool,
1151}
1152
1153impl SqlServerRowDecoder {
1154    /// Try to create a [`SqlServerRowDecoder`] that will decode [`tiberius::Row`]s that match
1155    /// the shape of the provided [`SqlServerTableDesc`], to [`mz_repr::Row`]s that match the
1156    /// shape of the provided [`RelationDesc`].
1157    pub fn try_new(
1158        table: &SqlServerTableDesc,
1159        desc: &RelationDesc,
1160    ) -> Result<Self, SqlServerError> {
1161        let mut needs_update_mask = false;
1162        let decoders = desc
1163            .iter()
1164            .map(|(col_name, col_type)| {
1165                let sql_server_col = table
1166                    .columns
1167                    .iter()
1168                    .find(|col| col.name.as_ref() == col_name.as_str())
1169                    .ok_or_else(|| {
1170                        // TODO(sql_server2): Structured Error.
1171                        anyhow::anyhow!("no SQL Server column with name {col_name} found")
1172                    })?;
1173                let Some(sql_server_col_typ) = sql_server_col.column_type.as_ref() else {
1174                    return Err(SqlServerError::ProgrammingError(format!(
1175                        "programming error, {col_name} should have been exluded",
1176                    )));
1177                };
1178
1179                // This shouldn't be true, but be defensive.
1180                //
1181                // TODO(sql_server2): Maybe allow the Materialize column type to be
1182                // more nullable than our decoding type?
1183                //
1184                // Sad. Our timestamp types don't roundtrip their precision through
1185                // parsing so we ignore the mismatch here.
1186                let matches = match (&sql_server_col_typ.scalar_type, &col_type.scalar_type) {
1187                    (SqlScalarType::Timestamp { .. }, SqlScalarType::Timestamp { .. })
1188                    | (SqlScalarType::TimestampTz { .. }, SqlScalarType::TimestampTz { .. }) => {
1189                        // Types match so check nullability.
1190                        sql_server_col_typ.nullable == col_type.nullable
1191                    }
1192                    (_, _) => sql_server_col_typ == col_type,
1193                };
1194                if !matches {
1195                    return Err(SqlServerError::ProgrammingError(format!(
1196                        "programming error, {col_name} has mismatched type {:?} vs {:?}",
1197                        sql_server_col.column_type, col_type
1198                    )));
1199                }
1200
1201                let name = Arc::clone(&sql_server_col.name);
1202                let decoder = sql_server_col.decode_type.clone();
1203                // Note: We specifically use the `SqlColumnType` from the SqlServerTableDesc
1204                // because it retains precision.
1205                //
1206                // See: <https://github.com/MaterializeInc/database-issues/issues/3179>.
1207                let col_typ = sql_server_col_typ.clone();
1208
1209                // determine whether this is a large object (LOB) column. There's a global
1210                // used to skip parsing the update mask per row and a per-column boolean that
1211                // avoids having the type matching in multiple places.
1212                let is_lob = is_lob_column(&col_typ.scalar_type);
1213                needs_update_mask |= is_lob;
1214
1215                Ok::<_, SqlServerError>((name, col_typ, decoder, is_lob))
1216            })
1217            .collect::<Result<_, _>>()?;
1218
1219        Ok(SqlServerRowDecoder {
1220            decoders,
1221            needs_update_mask,
1222        })
1223    }
1224
1225    /// Decode data from the provided [`tiberius::Row`] into the provided [`Row`].
1226    ///
1227    /// For updates, the new row data is provided in the event the data contains Large Object
1228    /// (e.g. varchar(max)). [`SqlServerRowDecoder::decode()`] will decode the [`UpdateMask`] from
1229    /// the new row and retrieve LOB values from the new row for any LOB column that was not updated
1230    /// in the old row.
1231    pub fn decode(
1232        &self,
1233        data: &tiberius::Row,
1234        row: &mut Row,
1235        arena: &RowArena,
1236        new_data: Option<&tiberius::Row>,
1237    ) -> Result<(), SqlServerDecodeError> {
1238        let mut packer = row.packer();
1239
1240        // decode update mask only when the row has a lob column
1241        let update_mask = new_data
1242            .filter(|_| self.needs_update_mask)
1243            .map(UpdateMask::try_from)
1244            .transpose()?;
1245        for (col_name, col_type, decoder, is_lob) in &self.decoders {
1246            let datum = if let Some((new_data, mask)) = new_data.zip(update_mask.as_ref())
1247                && *is_lob
1248                && !data_col_updated(new_data, mask, col_name)?
1249            {
1250                // The only time it is valid to pull the LOB column value from the new row
1251                // is if the LOB column was *not* updated. The mask check is necessary to
1252                // differentiate between updating a non-LOB column (the LOB column in old row
1253                // is NULL) and updating a LOB column where the old value is NULL.
1254                decoder.decode(new_data, col_name, col_type, arena)?
1255            } else {
1256                decoder.decode(data, col_name, col_type, arena)?
1257            };
1258
1259            packer.push(datum);
1260        }
1261        Ok(())
1262    }
1263
1264    pub fn included_column_names(&self) -> Vec<Arc<str>> {
1265        self.decoders
1266            .iter()
1267            .map(|decoder| Arc::clone(&decoder.0))
1268            .collect()
1269    }
1270}
1271
1272// Returns true if the Row's update_mask indicates the column was updated, false otherwise.
1273// Returns SqlServerDecodeError::MissingColumn if the column cannot be found by name.
1274fn data_col_updated(
1275    row: &tiberius::Row,
1276    update_mask: &UpdateMask,
1277    col_name: &str,
1278) -> Result<bool, SqlServerDecodeError> {
1279    row.columns()
1280        .iter()
1281        .position(|c| c.name() == col_name)
1282        .map(|idx| update_mask.data_col_updated(idx))
1283        .ok_or_else(|| SqlServerDecodeError::MissingColumn {
1284            column_name: col_name.to_owned(),
1285        })
1286}
1287
1288// for now, there is only a single large object type supported.
1289fn is_lob_column(col_type: &SqlScalarType) -> bool {
1290    matches!(col_type, SqlScalarType::VarChar { max_length: None })
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295    use std::collections::BTreeSet;
1296    use std::sync::Arc;
1297
1298    use chrono::NaiveDateTime;
1299    use itertools::Itertools;
1300    use mz_ore::assert_contains;
1301    use mz_ore::collections::CollectionExt;
1302    use mz_repr::adt::char::CharLength;
1303    use mz_repr::adt::numeric::NumericMaxScale;
1304    use mz_repr::adt::varchar::VarCharMaxLength;
1305    use mz_repr::{Datum, RelationDesc, Row, RowArena, SqlScalarType};
1306    use tiberius::RowTestExt;
1307
1308    use crate::desc::{
1309        SqlServerCaptureInstanceRaw, SqlServerColumnDecodeType, SqlServerColumnDesc,
1310        SqlServerTableConstraintRaw, SqlServerTableConstraintType, SqlServerTableDesc,
1311        SqlServerTableRaw, tiberius_numeric_to_mz_numeric,
1312    };
1313
1314    use super::SqlServerColumnRaw;
1315
1316    impl SqlServerColumnRaw {
1317        /// Create a new [`SqlServerColumnRaw`]. The specified `data_type` is
1318        /// _not_ checked for validity.
1319        fn new(name: &str, data_type: &str) -> Self {
1320            SqlServerColumnRaw {
1321                name: name.into(),
1322                data_type: data_type.into(),
1323                is_nullable: false,
1324                max_length: 0,
1325                precision: 0,
1326                scale: 0,
1327                is_computed: false,
1328            }
1329        }
1330
1331        fn nullable(mut self, nullable: bool) -> Self {
1332            self.is_nullable = nullable;
1333            self
1334        }
1335
1336        fn max_length(mut self, max_length: i16) -> Self {
1337            self.max_length = max_length;
1338            self
1339        }
1340
1341        fn precision(mut self, precision: u8) -> Self {
1342            self.precision = precision;
1343            self
1344        }
1345
1346        fn scale(mut self, scale: u8) -> Self {
1347            self.scale = scale;
1348            self
1349        }
1350    }
1351
1352    #[mz_ore::test]
1353    fn smoketest_column_raw() {
1354        let raw = SqlServerColumnRaw::new("foo", "bit");
1355        let col = SqlServerColumnDesc::new(&raw);
1356
1357        assert_eq!(&*col.name, "foo");
1358        assert_eq!(col.column_type, Some(SqlScalarType::Bool.nullable(false)));
1359        assert_eq!(col.decode_type, SqlServerColumnDecodeType::Bool);
1360
1361        let raw = SqlServerColumnRaw::new("foo", "decimal")
1362            .precision(20)
1363            .scale(10);
1364        let col = SqlServerColumnDesc::new(&raw);
1365
1366        let col_type = SqlScalarType::Numeric {
1367            max_scale: Some(NumericMaxScale::try_from(10i64).expect("known valid")),
1368        }
1369        .nullable(false);
1370        assert_eq!(col.column_type, Some(col_type));
1371        assert_eq!(col.decode_type, SqlServerColumnDecodeType::Numeric);
1372    }
1373
1374    #[mz_ore::test]
1375    fn smoketest_column_raw_invalid() {
1376        let raw = SqlServerColumnRaw::new("foo", "bad_data_type");
1377        let desc = SqlServerColumnDesc::new(&raw);
1378        let SqlServerColumnDecodeType::Unsupported { context } = desc.decode_type else {
1379            panic!("unexpected decode type {desc:?}");
1380        };
1381        assert_contains!(context, "'bad_data_type' is unimplemented");
1382
1383        let raw = SqlServerColumnRaw::new("foo", "decimal")
1384            .precision(100)
1385            .scale(10);
1386        let desc = SqlServerColumnDesc::new(&raw);
1387        assert!(matches!(
1388            desc.decode_type,
1389            SqlServerColumnDecodeType::Unsupported { .. }
1390        ));
1391
1392        let raw = SqlServerColumnRaw::new("foo", "varbinary").max_length(-1);
1393        let desc = SqlServerColumnDesc::new(&raw);
1394        let SqlServerColumnDecodeType::Unsupported { context } = desc.decode_type else {
1395            panic!("unexpected decode type {desc:?}");
1396        };
1397        assert_contains!(context, "columns with unlimited size do not support CDC");
1398    }
1399
1400    #[mz_ore::test]
1401    fn only_capture_instance_index_constraint_is_kept() {
1402        fn table_desc(index_name: Option<&str>) -> SqlServerTableDesc {
1403            let raw = SqlServerTableRaw {
1404                schema_name: "dbo".into(),
1405                name: "t".into(),
1406                capture_instance: Arc::new(SqlServerCaptureInstanceRaw {
1407                    name: "dbo_t".into(),
1408                    create_date: NaiveDateTime::default().into(),
1409                    index_name: index_name.map(Into::into),
1410                }),
1411                columns: [
1412                    SqlServerColumnRaw::new("id", "int"),
1413                    SqlServerColumnRaw::new("code", "int"),
1414                ]
1415                .into(),
1416            };
1417            let constraints = vec![
1418                SqlServerTableConstraintRaw {
1419                    constraint_name: "pk_t".into(),
1420                    constraint_type: "PRIMARY KEY".into(),
1421                    columns: vec!["id".into()],
1422                },
1423                SqlServerTableConstraintRaw {
1424                    constraint_name: "uq_t".into(),
1425                    constraint_type: "UNIQUE".into(),
1426                    columns: vec!["code".into()],
1427                },
1428            ];
1429            SqlServerTableDesc::new(raw, constraints).unwrap()
1430        }
1431
1432        let desc = table_desc(Some("pk_t"));
1433        let constraint = desc.constraints.into_element();
1434        assert_eq!(constraint.constraint_name, "pk_t");
1435        assert_eq!(
1436            constraint.constraint_type,
1437            SqlServerTableConstraintType::PrimaryKey
1438        );
1439
1440        let desc = table_desc(Some("uq_t"));
1441        let constraint = desc.constraints.into_element();
1442        assert_eq!(constraint.constraint_name, "uq_t");
1443        assert_eq!(
1444            constraint.constraint_type,
1445            SqlServerTableConstraintType::Unique
1446        );
1447
1448        assert!(table_desc(None).constraints.is_empty());
1449        assert!(table_desc(Some("ix_t")).constraints.is_empty());
1450    }
1451
1452    #[mz_ore::test]
1453    fn smoketest_decoder() {
1454        let sql_server_columns = [
1455            SqlServerColumnRaw::new("a", "varchar").max_length(16),
1456            SqlServerColumnRaw::new("b", "int").nullable(true),
1457            SqlServerColumnRaw::new("c", "bit"),
1458        ];
1459        let sql_server_desc = SqlServerTableRaw {
1460            schema_name: "my_schema".into(),
1461            name: "my_table".into(),
1462            capture_instance: Arc::new(SqlServerCaptureInstanceRaw {
1463                name: "my_table_CT".into(),
1464                create_date: NaiveDateTime::parse_from_str(
1465                    "2024-01-01 00:00:00",
1466                    "%Y-%m-%d %H:%M:%S",
1467                )
1468                .unwrap()
1469                .into(),
1470                index_name: None,
1471            }),
1472            columns: sql_server_columns.into(),
1473        };
1474        let sql_server_desc = SqlServerTableDesc::new(sql_server_desc, vec![]).unwrap();
1475
1476        let max_length = Some(VarCharMaxLength::try_from(16).unwrap());
1477        let relation_desc = RelationDesc::builder()
1478            .with_column("a", SqlScalarType::VarChar { max_length }.nullable(false))
1479            // Note: In the upstream table 'c' is ordered after 'b'.
1480            .with_column("c", SqlScalarType::Bool.nullable(false))
1481            .with_column("b", SqlScalarType::Int32.nullable(true))
1482            .finish();
1483
1484        // This decoder should shape the SQL Server Rows into Rows compatible with the RelationDesc.
1485        let decoder = sql_server_desc
1486            .decoder(&relation_desc)
1487            .expect("known valid");
1488
1489        let sql_server_columns = [
1490            tiberius::Column::new("a".to_string(), tiberius::ColumnType::BigVarChar),
1491            tiberius::Column::new("b".to_string(), tiberius::ColumnType::Int4),
1492            tiberius::Column::new("c".to_string(), tiberius::ColumnType::Bit),
1493        ];
1494
1495        let data_a = [
1496            tiberius::ColumnData::String(Some("hello world".into())),
1497            tiberius::ColumnData::I32(Some(42)),
1498            tiberius::ColumnData::Bit(Some(true)),
1499        ];
1500        let sql_server_row_a = tiberius::Row::build(
1501            sql_server_columns
1502                .iter()
1503                .cloned()
1504                .zip_eq(data_a.into_iter()),
1505        );
1506
1507        let data_b = [
1508            tiberius::ColumnData::String(Some("foo bar".into())),
1509            tiberius::ColumnData::I32(None),
1510            tiberius::ColumnData::Bit(Some(false)),
1511        ];
1512        let sql_server_row_b =
1513            tiberius::Row::build(sql_server_columns.into_iter().zip_eq(data_b.into_iter()));
1514
1515        let mut rnd_row = Row::default();
1516        let arena = RowArena::default();
1517
1518        decoder
1519            .decode(&sql_server_row_a, &mut rnd_row, &arena, None)
1520            .unwrap();
1521        assert_eq!(
1522            &rnd_row,
1523            &Row::pack_slice(&[Datum::String("hello world"), Datum::True, Datum::Int32(42)])
1524        );
1525
1526        decoder
1527            .decode(&sql_server_row_b, &mut rnd_row, &arena, None)
1528            .unwrap();
1529        assert_eq!(
1530            &rnd_row,
1531            &Row::pack_slice(&[Datum::String("foo bar"), Datum::False, Datum::Null])
1532        );
1533    }
1534
1535    #[mz_ore::test]
1536    fn decode_update_lob_before_image() {
1537        let sql_server_columns = [
1538            SqlServerColumnRaw::new("k", "int"),
1539            SqlServerColumnRaw::new("v", "varchar").max_length(-1),
1540            SqlServerColumnRaw::new("n", "varchar")
1541                .max_length(-1)
1542                .nullable(true),
1543        ];
1544        let sql_server_desc = SqlServerTableRaw {
1545            schema_name: "my_schema".into(),
1546            name: "my_table".into(),
1547            capture_instance: Arc::new(SqlServerCaptureInstanceRaw {
1548                name: "my_table_CT".into(),
1549                create_date: NaiveDateTime::parse_from_str(
1550                    "2024-01-01 00:00:00",
1551                    "%Y-%m-%d %H:%M:%S",
1552                )
1553                .unwrap()
1554                .into(),
1555                index_name: None,
1556            }),
1557            columns: sql_server_columns.into(),
1558        };
1559        let sql_server_desc = SqlServerTableDesc::new(sql_server_desc, vec![]).unwrap();
1560        let relation_desc = RelationDesc::builder()
1561            .with_column("k", SqlScalarType::Int32.nullable(false))
1562            .with_column(
1563                "v",
1564                SqlScalarType::VarChar { max_length: None }.nullable(false),
1565            )
1566            .with_column(
1567                "n",
1568                SqlScalarType::VarChar { max_length: None }.nullable(true),
1569            )
1570            .finish();
1571        let decoder = sql_server_desc
1572            .decoder(&relation_desc)
1573            .expect("known valid");
1574
1575        // Shaped like a `cdc.fn_cdc_get_all_changes_<capture_instance>` row. The update mask
1576        // bits are, from least significant, `k`, `v`, `n`.
1577        let cdc_row =
1578            |op: i32, mask: u8, k: i32, v: Option<&'static str>, n: Option<&'static str>| {
1579                use tiberius::{Column, ColumnData, ColumnType};
1580                let lsn = || ColumnData::Binary(Some(vec![0; 10].into()));
1581                tiberius::Row::build([
1582                    (
1583                        Column::new("__$start_lsn".into(), ColumnType::BigVarBin),
1584                        lsn(),
1585                    ),
1586                    (
1587                        Column::new("__$seqval".into(), ColumnType::BigVarBin),
1588                        lsn(),
1589                    ),
1590                    (
1591                        Column::new("__$operation".into(), ColumnType::Int4),
1592                        ColumnData::I32(Some(op)),
1593                    ),
1594                    (
1595                        Column::new("__$update_mask".into(), ColumnType::BigVarBin),
1596                        ColumnData::Binary(Some(vec![mask].into())),
1597                    ),
1598                    (
1599                        Column::new("k".into(), ColumnType::Int4),
1600                        ColumnData::I32(Some(k)),
1601                    ),
1602                    (
1603                        Column::new("v".into(), ColumnType::BigVarChar),
1604                        ColumnData::String(v.map(Into::into)),
1605                    ),
1606                    (
1607                        Column::new("n".into(), ColumnType::BigVarChar),
1608                        ColumnData::String(n.map(Into::into)),
1609                    ),
1610                ])
1611            };
1612        let arena = RowArena::new();
1613        let mut mz_row = Row::default();
1614
1615        // Only `k` updated, so SQL Server leaves both LOBs NULL in the before-image.
1616        let after = cdc_row(4, 0b001, 2, Some("v"), Some("n"));
1617        decoder
1618            .decode(
1619                &cdc_row(3, 0b001, 1, None, None),
1620                &mut mz_row,
1621                &arena,
1622                Some(&after),
1623            )
1624            .unwrap();
1625        assert_eq!(
1626            mz_row,
1627            Row::pack_slice(&[Datum::Int32(1), Datum::String("v"), Datum::String("n")])
1628        );
1629
1630        // Both LOBs updated, so the before-image carries their prior values, including NULL.
1631        let after = cdc_row(4, 0b110, 1, Some("new"), Some("n"));
1632        decoder
1633            .decode(
1634                &cdc_row(3, 0b110, 1, Some("old"), None),
1635                &mut mz_row,
1636                &arena,
1637                Some(&after),
1638            )
1639            .unwrap();
1640        assert_eq!(
1641            mz_row,
1642            Row::pack_slice(&[Datum::Int32(1), Datum::String("old"), Datum::Null])
1643        );
1644
1645        // A NULL before-image for an updated NOT NULL LOB cannot be recovered.
1646        let after = cdc_row(4, 0b010, 1, Some("new"), None);
1647        let err = decoder
1648            .decode(
1649                &cdc_row(3, 0b010, 1, None, None),
1650                &mut mz_row,
1651                &arena,
1652                Some(&after),
1653            )
1654            .unwrap_err();
1655        assert_contains!(err.to_string(), "found Null in non-nullable column");
1656    }
1657
1658    #[mz_ore::test]
1659    fn decode_legacy_char_column() {
1660        // Sources created when `char` columns mapped to `Char { length }`
1661        // carry that type in their persisted desc. `length` is the upstream
1662        // byte count, so under a multi-byte collation the blank-padded value
1663        // holds fewer characters than `length` and must still decode.
1664        let length = Some(CharLength::try_from(10i64).expect("known valid"));
1665        let column_type = SqlScalarType::Char { length }.nullable(false);
1666        let columns = [SqlServerColumnDesc {
1667            name: "a".into(),
1668            column_type: Some(column_type.clone()),
1669            primary_key_constraint: None,
1670            decode_type: SqlServerColumnDecodeType::String,
1671            raw_type: "char".into(),
1672        }];
1673        let table_desc = SqlServerTableDesc {
1674            schema_name: "my_schema".into(),
1675            name: "my_table".into(),
1676            columns: columns.into(),
1677            constraints: vec![],
1678        };
1679        let relation_desc = RelationDesc::builder()
1680            .with_column("a", column_type)
1681            .finish();
1682        let decoder = table_desc.decoder(&relation_desc).expect("known valid");
1683
1684        let char_row = |val: &'static str| {
1685            tiberius::Row::build([(
1686                tiberius::Column::new("a".to_string(), tiberius::ColumnType::BigChar),
1687                tiberius::ColumnData::String(Some(val.into())),
1688            )])
1689        };
1690        let mut mz_row = Row::default();
1691        let arena = RowArena::default();
1692
1693        // 'café' in a char(10) column under a UTF-8 collation: 10 bytes but
1694        // only 9 characters.
1695        decoder
1696            .decode(&char_row("café     "), &mut mz_row, &arena, None)
1697            .unwrap();
1698        assert_eq!(&mz_row, &Row::pack_slice(&[Datum::String("café     ")]));
1699
1700        // A single-byte collation pads to exactly `length` characters.
1701        decoder
1702            .decode(&char_row("0123456789"), &mut mz_row, &arena, None)
1703            .unwrap();
1704        assert_eq!(&mz_row, &Row::pack_slice(&[Datum::String("0123456789")]));
1705
1706        // More characters than the upstream byte count is impossible, reject.
1707        let err = decoder
1708            .decode(&char_row("0123456789!"), &mut mz_row, &arena, None)
1709            .unwrap_err();
1710        assert_contains!(err.to_string(), "expected 10 chars found 11");
1711    }
1712
1713    #[mz_ore::test]
1714    fn smoketest_decode_to_string() {
1715        #[track_caller]
1716        fn testcase(
1717            data_type: &'static str,
1718            col_type: tiberius::ColumnType,
1719            col_data: tiberius::ColumnData<'static>,
1720        ) {
1721            let columns = [SqlServerColumnRaw::new("a", data_type)];
1722            let sql_server_desc = SqlServerTableRaw {
1723                schema_name: "my_schema".into(),
1724                name: "my_table".into(),
1725                capture_instance: Arc::new(SqlServerCaptureInstanceRaw {
1726                    name: "my_table_CT".into(),
1727                    create_date: NaiveDateTime::parse_from_str(
1728                        "2024-01-01 00:00:00",
1729                        "%Y-%m-%d %H:%M:%S",
1730                    )
1731                    .unwrap()
1732                    .into(),
1733                    index_name: None,
1734                }),
1735                columns: columns.into(),
1736            };
1737            let mut sql_server_desc = SqlServerTableDesc::new(sql_server_desc, vec![]).unwrap();
1738            sql_server_desc.apply_text_columns(&BTreeSet::from(["a"]));
1739
1740            // We should support decoding every datatype to a string.
1741            let relation_desc = RelationDesc::builder()
1742                .with_column("a", SqlScalarType::String.nullable(false))
1743                .finish();
1744
1745            // This decoder should shape the SQL Server Rows into Rows compatible with the RelationDesc.
1746            let decoder = sql_server_desc
1747                .decoder(&relation_desc)
1748                .expect("known valid");
1749
1750            let sql_server_row = tiberius::Row::build([(
1751                tiberius::Column::new("a".to_string(), col_type),
1752                col_data,
1753            )]);
1754            let mut mz_row = Row::default();
1755            let arena = RowArena::new();
1756            decoder
1757                .decode(&sql_server_row, &mut mz_row, &arena, None)
1758                .unwrap();
1759
1760            let str_datum = mz_row.into_element();
1761            assert!(matches!(str_datum, Datum::String(_)));
1762        }
1763
1764        use tiberius::ColumnData;
1765
1766        testcase(
1767            "bit",
1768            tiberius::ColumnType::Bit,
1769            ColumnData::Bit(Some(true)),
1770        );
1771        testcase(
1772            "bit",
1773            tiberius::ColumnType::Bit,
1774            ColumnData::Bit(Some(false)),
1775        );
1776        testcase(
1777            "tinyint",
1778            tiberius::ColumnType::Int1,
1779            ColumnData::U8(Some(33)),
1780        );
1781        testcase(
1782            "smallint",
1783            tiberius::ColumnType::Int2,
1784            ColumnData::I16(Some(101)),
1785        );
1786        testcase(
1787            "int",
1788            tiberius::ColumnType::Int4,
1789            ColumnData::I32(Some(-42)),
1790        );
1791        {
1792            let datetime = tiberius::time::DateTime::new(10, 300);
1793            testcase(
1794                "datetime",
1795                tiberius::ColumnType::Datetime,
1796                ColumnData::DateTime(Some(datetime)),
1797            );
1798        }
1799    }
1800
1801    #[mz_ore::test]
1802    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `decNumberFromInt32` on OS `linux`
1803    fn smoketest_numeric_conversion() {
1804        let a = tiberius::numeric::Numeric::new_with_scale(12345, 2);
1805        let rnd = tiberius_numeric_to_mz_numeric(a);
1806        let og = mz_repr::adt::numeric::cx_datum().parse("123.45").unwrap();
1807        assert_eq!(og, rnd);
1808
1809        let a = tiberius::numeric::Numeric::new_with_scale(-99999, 5);
1810        let rnd = tiberius_numeric_to_mz_numeric(a);
1811        let og = mz_repr::adt::numeric::cx_datum().parse("-.99999").unwrap();
1812        assert_eq!(og, rnd);
1813
1814        let a = tiberius::numeric::Numeric::new_with_scale(1, 29);
1815        let rnd = tiberius_numeric_to_mz_numeric(a);
1816        let og = mz_repr::adt::numeric::cx_datum()
1817            .parse("0.00000000000000000000000000001")
1818            .unwrap();
1819        assert_eq!(og, rnd);
1820
1821        let a = tiberius::numeric::Numeric::new_with_scale(-111111111111111111, 0);
1822        let rnd = tiberius_numeric_to_mz_numeric(a);
1823        let og = mz_repr::adt::numeric::cx_datum()
1824            .parse("-111111111111111111")
1825            .unwrap();
1826        assert_eq!(og, rnd);
1827    }
1828
1829    // TODO(sql_server2): Proptest the decoder.
1830}