1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
57pub struct SqlServerTableDesc {
58 pub schema_name: Arc<str>,
60 pub name: Arc<str>,
62 pub columns: Box<[SqlServerColumnDesc]>,
64 pub constraints: Vec<SqlServerTableConstraint>,
71}
72
73impl SqlServerTableDesc {
74 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 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 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 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 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#[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#[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#[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#[derive(Debug, Clone)]
280pub struct SqlServerTableRaw {
281 pub schema_name: Arc<str>,
283 pub name: Arc<str>,
285 pub capture_instance: Arc<SqlServerCaptureInstanceRaw>,
287 pub columns: Arc<[SqlServerColumnRaw]>,
289}
290
291#[derive(Debug, Clone)]
293pub struct SqlServerCaptureInstanceRaw {
294 pub name: Arc<str>,
296 pub create_date: Arc<NaiveDateTime>,
298 pub index_name: Option<Arc<str>>,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
307pub struct SqlServerColumnDesc {
308 pub name: Arc<str>,
310 pub column_type: Option<SqlColumnType>,
316 pub primary_key_constraint: Option<Arc<str>>,
319 pub decode_type: SqlServerColumnDecodeType,
321 pub raw_type: Arc<str>,
325}
326
327impl SqlServerColumnDesc {
328 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 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 pub fn exclude(&mut self) {
368 self.column_type = None;
369 }
370
371 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#[derive(Debug)]
403#[allow(dead_code)]
404pub struct UnsupportedDataType {
405 column_name: String,
406 column_type: String,
407 reason: String,
408}
409
410fn parse_data_type(
415 raw: &SqlServerColumnRaw,
416) -> Result<(SqlScalarType, SqlServerColumnDecodeType), UnsupportedDataType> {
417 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 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 "real" | "float" | "double precision" => match raw.max_length {
481 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 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 (SqlScalarType::String, SqlServerColumnDecodeType::String)
511 }
512 "varchar" | "nvarchar" => {
513 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 mz_ore::soft_assert_eq_no_log!(raw.max_length, 16);
543
544 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 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 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 "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 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#[derive(Clone, Debug)]
645pub struct SqlServerColumnRaw {
646 pub name: Arc<str>,
648 pub data_type: Arc<str>,
650 pub is_nullable: bool,
652 pub max_length: i16,
662 pub precision: u8,
664 pub scale: u8,
666 pub is_computed: bool,
668}
669
670#[derive(Clone, Debug)]
672pub struct SqlServerTableConstraintRaw {
673 pub constraint_name: String,
674 pub constraint_type: String,
675 pub columns: Vec<String>,
676}
677
678#[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,
693 Numeric,
695 Xml,
697 NaiveDate,
699 NaiveTime,
701 DateTime,
703 NaiveDateTime,
705 Unsupported {
707 context: String,
709 },
710}
711
712impl SqlServerColumnDecodeType {
713 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 (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 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 let rounded = val.round_subsecs(6);
849 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 (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 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
1071fn 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 mz_repr::adt::numeric::cx_datum().scaleb(&mut numeric, &Numeric::from(-i32::from(val.scale())));
1078 numeric
1079}
1080
1081#[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 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#[derive(Debug)]
1148pub struct SqlServerRowDecoder {
1149 decoders: Vec<(Arc<str>, SqlColumnType, SqlServerColumnDecodeType, bool)>,
1150 needs_update_mask: bool,
1151}
1152
1153impl SqlServerRowDecoder {
1154 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 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 let matches = match (&sql_server_col_typ.scalar_type, &col_type.scalar_type) {
1187 (SqlScalarType::Timestamp { .. }, SqlScalarType::Timestamp { .. })
1188 | (SqlScalarType::TimestampTz { .. }, SqlScalarType::TimestampTz { .. }) => {
1189 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 let col_typ = sql_server_col_typ.clone();
1208
1209 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 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 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 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
1272fn 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
1288fn 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 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 .with_column("c", SqlScalarType::Bool.nullable(false))
1481 .with_column("b", SqlScalarType::Int32.nullable(true))
1482 .finish();
1483
1484 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 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 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 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 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 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 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 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 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 let relation_desc = RelationDesc::builder()
1742 .with_column("a", SqlScalarType::String.nullable(false))
1743 .finish();
1744
1745 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)] 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 }