1use std::any::Any;
11use std::borrow::Cow;
12use std::future::IntoFuture;
13use std::pin::Pin;
14use std::sync::Arc;
15
16use anyhow::Context;
17use derivative::Derivative;
18use futures::future::BoxFuture;
19use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
20use mz_ore::result::ResultExt;
21use mz_repr::SqlScalarType;
22use smallvec::{SmallVec, smallvec};
23use tiberius::ToSql;
24use tokio::net::TcpStream;
25use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
26use tokio::sync::oneshot;
27use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
28
29pub mod cdc;
30pub mod config;
31pub mod desc;
32pub mod inspect;
33
34pub use config::Config;
35pub use desc::{ProtoSqlServerColumnDesc, ProtoSqlServerTableDesc};
36
37use crate::cdc::Lsn;
38use crate::config::TunnelConfig;
39use crate::desc::SqlServerColumnDecodeType;
40use crate::inspect::EngineEdition;
41
42#[derive(Debug)]
45pub struct Client {
46 tx: UnboundedSender<Request>,
47 config: Config,
49 engine_edition: Option<EngineEdition>,
52}
53static_assertions::assert_not_impl_all!(Client: Clone);
56
57impl Client {
58 pub async fn connect(config: Config) -> Result<Self, SqlServerError> {
67 let (tcp, resources): (_, Option<Box<dyn Any + Send + Sync>>) = match &config.tunnel {
70 TunnelConfig::Direct { resolved_addresses } => {
71 let tcp = if resolved_addresses.is_empty() {
72 TcpStream::connect(config.inner.get_addr()).await
73 } else {
74 TcpStream::connect(resolved_addresses.as_ref()).await
75 }
76 .context("direct")?;
77 (tcp, None)
78 }
79 TunnelConfig::Ssh {
80 config: ssh_config,
81 manager,
82 timeout,
83 host,
84 port,
85 } => {
86 let tunnel = manager
89 .connect(ssh_config.clone(), host, *port, *timeout, config.in_task)
90 .await?;
91 let tcp = TcpStream::connect(tunnel.local_addr())
92 .await
93 .context("ssh tunnel")?;
94
95 (tcp, Some(Box::new(tunnel)))
96 }
97 TunnelConfig::AwsPrivatelink {
98 connection_id,
99 port,
100 } => {
101 let privatelink_host = mz_cloud_resources::vpc_endpoint_name(*connection_id);
102 let tcp = TcpStream::connect((privatelink_host.as_str(), *port))
103 .await
104 .context(format!("aws privatelink {:?}", privatelink_host))?;
105
106 (tcp, None)
107 }
108 };
109
110 tcp.set_nodelay(true)?;
111
112 let (client, connection) = Self::connect_raw(config, tcp, resources).await?;
113 mz_ore::task::spawn(|| "sql-server-client-connection", async move {
114 connection.await
115 });
116
117 Ok(client)
118 }
119
120 pub async fn new_connection(&self) -> Result<Self, SqlServerError> {
123 Self::connect(self.config.clone()).await
124 }
125
126 pub async fn connect_raw(
127 config: Config,
128 tcp: tokio::net::TcpStream,
129 resources: Option<Box<dyn Any + Send + Sync>>,
130 ) -> Result<(Self, Connection), SqlServerError> {
131 let client = tiberius::Client::connect(config.inner.clone(), tcp.compat_write()).await?;
132 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
133
134 Ok((
137 Client {
138 tx,
139 config,
140 engine_edition: None,
141 },
142 Connection {
143 rx,
144 client,
145 _resources: resources,
146 },
147 ))
148 }
149
150 pub async fn execute<'a>(
159 &mut self,
160 query: impl Into<Cow<'a, str>>,
161 params: &[&dyn ToSql],
162 ) -> Result<SmallVec<[u64; 1]>, SqlServerError> {
163 let (tx, rx) = tokio::sync::oneshot::channel();
164
165 let params = params
166 .iter()
167 .map(|p| OwnedColumnData::from(p.to_sql()))
168 .collect();
169 let kind = RequestKind::Execute {
170 query: query.into().to_string(),
171 params,
172 };
173 self.tx
174 .send(Request { tx, kind })
175 .context("sending request")?;
176
177 let response = rx.await.context("channel")??;
178 match response {
179 Response::Execute { rows_affected } => Ok(rows_affected),
180 other @ Response::Rows(_) | other @ Response::RowStream { .. } => {
181 Err(SqlServerError::ProgrammingError(format!(
182 "expected Response::Execute, got {other:?}"
183 )))
184 }
185 }
186 }
187
188 pub async fn query<'a>(
197 &mut self,
198 query: impl Into<Cow<'a, str>>,
199 params: &[&dyn tiberius::ToSql],
200 ) -> Result<SmallVec<[tiberius::Row; 1]>, SqlServerError> {
201 let (tx, rx) = tokio::sync::oneshot::channel();
202
203 let params = params
204 .iter()
205 .map(|p| OwnedColumnData::from(p.to_sql()))
206 .collect();
207 let kind = RequestKind::Query {
208 query: query.into().to_string(),
209 params,
210 };
211 self.tx
212 .send(Request { tx, kind })
213 .context("sending request")?;
214
215 let response = rx.await.context("channel")??;
216 match response {
217 Response::Rows(rows) => Ok(rows),
218 other @ Response::Execute { .. } | other @ Response::RowStream { .. } => Err(
219 SqlServerError::ProgrammingError(format!("expected Response::Rows, got {other:?}")),
220 ),
221 }
222 }
223
224 pub fn query_streaming<'c, 'q, Q>(
229 &'c mut self,
230 query: Q,
231 params: &[&dyn tiberius::ToSql],
232 ) -> impl Stream<Item = Result<tiberius::Row, SqlServerError>> + Send + use<'c, Q>
233 where
234 Q: Into<Cow<'q, str>>,
235 {
236 let (tx, rx) = tokio::sync::oneshot::channel();
237 let params = params
238 .iter()
239 .map(|p| OwnedColumnData::from(p.to_sql()))
240 .collect();
241 let kind = RequestKind::QueryStreamed {
242 query: query.into().to_string(),
243 params,
244 };
245
246 let request_future = async move {
248 self.tx
249 .send(Request { tx, kind })
250 .context("sending request")?;
251
252 let response = rx.await.context("channel")??;
253 match response {
254 Response::RowStream { stream } => {
255 Ok(tokio_stream::wrappers::ReceiverStream::new(stream))
256 }
257 other @ Response::Execute { .. } | other @ Response::Rows(_) => {
258 Err(SqlServerError::ProgrammingError(format!(
259 "expected Response::Rows, got {other:?}"
260 )))
261 }
262 }
263 };
264
265 futures::stream::once(request_future).try_flatten()
267 }
268
269 pub async fn simple_query<'a>(
279 &mut self,
280 query: impl Into<Cow<'a, str>>,
281 ) -> Result<SmallVec<[tiberius::Row; 1]>, SqlServerError> {
282 let (tx, rx) = tokio::sync::oneshot::channel();
283 let kind = RequestKind::SimpleQuery {
284 query: query.into().to_string(),
285 };
286 self.tx
287 .send(Request { tx, kind })
288 .context("sending request")?;
289
290 let response = rx.await.context("channel")??;
291 match response {
292 Response::Rows(rows) => Ok(rows),
293 other @ Response::Execute { .. } | other @ Response::RowStream { .. } => Err(
294 SqlServerError::ProgrammingError(format!("expected Response::Rows, got {other:?}")),
295 ),
296 }
297 }
298
299 pub async fn transaction(&mut self) -> Result<Transaction<'_>, SqlServerError> {
304 Transaction::new(self).await
305 }
306
307 pub async fn set_transaction_isolation(
309 &mut self,
310 level: TransactionIsolationLevel,
311 ) -> Result<(), SqlServerError> {
312 let query = format!("SET TRANSACTION ISOLATION LEVEL {}", level.as_str());
313 self.simple_query(query).await?;
314 Ok(())
315 }
316
317 pub async fn get_transaction_isolation(
319 &mut self,
320 ) -> Result<TransactionIsolationLevel, SqlServerError> {
321 const QUERY: &str = "SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = @@SPID;";
322 let rows = self.simple_query(QUERY).await?;
323 match &rows[..] {
324 [row] => {
325 let val: i16 = row
326 .try_get(0)
327 .context("getting 0th column")?
328 .ok_or_else(|| anyhow::anyhow!("no 0th column?"))?;
329 let level = TransactionIsolationLevel::try_from_sql_server(val)?;
330 Ok(level)
331 }
332 other => Err(SqlServerError::InvariantViolated(format!(
333 "expected one row, got {other:?}"
334 ))),
335 }
336 }
337
338 pub async fn engine_edition(&mut self) -> Result<EngineEdition, SqlServerError> {
341 if let Some(edition) = self.engine_edition {
342 return Ok(edition);
343 }
344 let edition = crate::inspect::get_engine_edition(self).await?;
345 self.engine_edition = Some(edition);
346 Ok(edition)
347 }
348
349 pub fn cdc<I, M>(&mut self, capture_instances: I, metrics: M) -> crate::cdc::CdcStream<'_, M>
354 where
355 I: IntoIterator,
356 I::Item: Into<Arc<str>>,
357 M: SqlServerCdcMetrics,
358 {
359 let instances = capture_instances
360 .into_iter()
361 .map(|i| (i.into(), None))
362 .collect();
363 crate::cdc::CdcStream::new(self, instances, metrics)
364 }
365}
366
367pub type RowStream<'a> =
369 Pin<Box<dyn Stream<Item = Result<tiberius::Row, SqlServerError>> + Send + 'a>>;
370
371#[derive(Debug)]
372pub struct Transaction<'a> {
373 client: &'a mut Client,
374 closed: bool,
375}
376
377impl<'a> Transaction<'a> {
378 async fn new(client: &'a mut Client) -> Result<Self, SqlServerError> {
379 let tx = Transaction {
382 client,
383 closed: false,
384 };
385 let results = tx
386 .client
387 .simple_query("BEGIN TRANSACTION")
388 .await
389 .context("begin")?;
390 if !results.is_empty() {
391 Err(SqlServerError::InvariantViolated(format!(
392 "expected empty result from BEGIN TRANSACTION. Got: {results:?}"
393 )))
394 } else {
395 Ok(tx)
396 }
397 }
398
399 pub async fn create_savepoint(&mut self, savepoint_name: &str) -> Result<(), SqlServerError> {
409 if savepoint_name.is_empty()
412 || !savepoint_name
413 .chars()
414 .all(|c| c.is_alphanumeric() || c == '_')
415 {
416 Err(SqlServerError::ProgrammingError(format!(
417 "Invalid savepoint name: '{savepoint_name}"
418 )))?;
419 }
420
421 let stmt = format!("SAVE TRANSACTION {}", quote_identifier(savepoint_name));
422 let _result = self.client.simple_query(stmt).await?;
423 Ok(())
424 }
425
426 pub async fn get_lsn(&mut self) -> Result<Lsn, SqlServerError> {
430 static CURRENT_LSN_QUERY: &str = "SELECT dt.database_transaction_most_recent_savepoint_lsn \
431 FROM sys.dm_tran_database_transactions dt \
432 JOIN sys.dm_tran_current_transaction ct \
433 ON ct.transaction_id = dt.transaction_id \
434 WHERE dt.database_transaction_most_recent_savepoint_lsn IS NOT NULL";
435 let result = self.client.simple_query(CURRENT_LSN_QUERY).await?;
436 crate::inspect::parse_numeric_lsn(&result)
437 }
438
439 pub async fn lock_table_shared(
445 &mut self,
446 schema: &str,
447 table: &str,
448 ) -> Result<(), SqlServerError> {
449 static SET_READ_COMMITTED: &str = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED;";
452 let query = format!(
456 "{SET_READ_COMMITTED}\nSELECT * FROM {schema}.{table} WITH (TABLOCK, HOLDLOCK) WHERE 1=0;",
457 schema = quote_identifier(schema),
458 table = quote_identifier(table)
459 );
460 let _result = self.client.simple_query(query).await?;
461 Ok(())
462 }
463
464 pub async fn execute<'q>(
466 &mut self,
467 query: impl Into<Cow<'q, str>>,
468 params: &[&dyn ToSql],
469 ) -> Result<SmallVec<[u64; 1]>, SqlServerError> {
470 self.client.execute(query, params).await
471 }
472
473 pub async fn query<'q>(
475 &mut self,
476 query: impl Into<Cow<'q, str>>,
477 params: &[&dyn tiberius::ToSql],
478 ) -> Result<SmallVec<[tiberius::Row; 1]>, SqlServerError> {
479 self.client.query(query, params).await
480 }
481
482 pub fn query_streaming<'c, 'q, Q>(
484 &'c mut self,
485 query: Q,
486 params: &[&dyn tiberius::ToSql],
487 ) -> impl Stream<Item = Result<tiberius::Row, SqlServerError>> + Send + use<'c, Q>
488 where
489 Q: Into<Cow<'q, str>>,
490 {
491 self.client.query_streaming(query, params)
492 }
493
494 pub async fn simple_query<'q>(
496 &mut self,
497 query: impl Into<Cow<'q, str>>,
498 ) -> Result<SmallVec<[tiberius::Row; 1]>, SqlServerError> {
499 self.client.simple_query(query).await
500 }
501
502 pub async fn rollback(mut self) -> Result<(), SqlServerError> {
504 static ROLLBACK_QUERY: &str = "ROLLBACK TRANSACTION";
505 self.closed = true;
508 self.client.simple_query(ROLLBACK_QUERY).await?;
509 Ok(())
510 }
511
512 pub async fn commit(mut self) -> Result<(), SqlServerError> {
514 static COMMIT_QUERY: &str = "COMMIT TRANSACTION";
515 self.closed = true;
518 self.client.simple_query(COMMIT_QUERY).await?;
519 Ok(())
520 }
521}
522
523impl Drop for Transaction<'_> {
524 fn drop(&mut self) {
525 if !self.closed {
526 let (tx, _rx) = oneshot::channel();
535 let kind = RequestKind::SimpleQuery {
536 query: "ROLLBACK TRANSACTION".to_string(),
537 };
538 let _ = self.client.tx.send(Request { tx, kind });
539 }
540 }
541}
542
543#[derive(Debug, PartialEq, Eq)]
547pub enum TransactionIsolationLevel {
548 ReadUncommitted,
549 ReadCommitted,
550 RepeatableRead,
551 Snapshot,
552 Serializable,
553}
554
555impl TransactionIsolationLevel {
556 fn as_str(&self) -> &'static str {
558 match self {
559 TransactionIsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
560 TransactionIsolationLevel::ReadCommitted => "READ COMMITTED",
561 TransactionIsolationLevel::RepeatableRead => "REPEATABLE READ",
562 TransactionIsolationLevel::Snapshot => "SNAPSHOT",
563 TransactionIsolationLevel::Serializable => "SERIALIZABLE",
564 }
565 }
566
567 fn try_from_sql_server(val: i16) -> Result<TransactionIsolationLevel, anyhow::Error> {
569 let level = match val {
570 1 => TransactionIsolationLevel::ReadUncommitted,
571 2 => TransactionIsolationLevel::ReadCommitted,
572 3 => TransactionIsolationLevel::RepeatableRead,
573 4 => TransactionIsolationLevel::Serializable,
574 5 => TransactionIsolationLevel::Snapshot,
575 x => anyhow::bail!("unknown level {x}"),
576 };
577 Ok(level)
578 }
579}
580
581#[derive(Derivative)]
582#[derivative(Debug)]
583enum Response {
584 Execute {
585 rows_affected: SmallVec<[u64; 1]>,
586 },
587 Rows(SmallVec<[tiberius::Row; 1]>),
588 RowStream {
589 #[derivative(Debug = "ignore")]
590 stream: tokio::sync::mpsc::Receiver<Result<tiberius::Row, SqlServerError>>,
591 },
592}
593
594#[derive(Debug)]
595struct Request {
596 tx: oneshot::Sender<Result<Response, SqlServerError>>,
597 kind: RequestKind,
598}
599
600#[derive(Derivative)]
601#[derivative(Debug)]
602enum RequestKind {
603 Execute {
604 query: String,
605 #[derivative(Debug = "ignore")]
606 params: SmallVec<[OwnedColumnData; 4]>,
607 },
608 Query {
609 query: String,
610 #[derivative(Debug = "ignore")]
611 params: SmallVec<[OwnedColumnData; 4]>,
612 },
613 QueryStreamed {
614 query: String,
615 #[derivative(Debug = "ignore")]
616 params: SmallVec<[OwnedColumnData; 4]>,
617 },
618 SimpleQuery {
619 query: String,
620 },
621}
622
623pub struct Connection {
624 rx: UnboundedReceiver<Request>,
626 client: tiberius::Client<Compat<TcpStream>>,
628 _resources: Option<Box<dyn Any + Send + Sync>>,
630}
631
632impl Connection {
633 async fn run(mut self) {
634 while let Some(Request { tx, kind }) = self.rx.recv().await {
635 tracing::trace!(?kind, "processing SQL Server query");
636 let result = Connection::handle_request(&mut self.client, kind).await;
637 let (response, maybe_extra_work) = match result {
638 Ok((response, work)) => (Ok(response), work),
639 Err(err) => (Err(err), None),
640 };
641
642 let _ = tx.send(response);
644
645 if let Some(extra_work) = maybe_extra_work {
649 extra_work.await;
650 }
651 }
652 tracing::debug!("channel closed, SQL Server InnerClient shutting down");
653 }
654
655 async fn handle_request<'c>(
656 client: &'c mut tiberius::Client<Compat<TcpStream>>,
657 kind: RequestKind,
658 ) -> Result<(Response, Option<BoxFuture<'c, ()>>), SqlServerError> {
659 match kind {
660 RequestKind::Execute { query, params } => {
661 #[allow(clippy::as_conversions)]
662 let params: SmallVec<[&dyn ToSql; 4]> =
663 params.iter().map(|x| x as &dyn ToSql).collect();
664 let result = client.execute(query, ¶ms[..]).await?;
665
666 match result.rows_affected() {
667 rows_affected => {
668 let response = Response::Execute {
669 rows_affected: rows_affected.into(),
670 };
671 Ok((response, None))
672 }
673 }
674 }
675 RequestKind::Query { query, params } => {
676 #[allow(clippy::as_conversions)]
677 let params: SmallVec<[&dyn ToSql; 4]> =
678 params.iter().map(|x| x as &dyn ToSql).collect();
679 let result = client.query(query, params.as_slice()).await?;
680
681 let mut results = result.into_results().await.context("into results")?;
682 if results.is_empty() {
683 Ok((Response::Rows(smallvec![]), None))
684 } else if results.len() == 1 {
685 let rows = results.pop().expect("checked len").into();
688 Ok((Response::Rows(rows), None))
689 } else {
690 Err(SqlServerError::ProgrammingError(format!(
691 "Query only supports 1 statement, got {}",
692 results.len()
693 )))
694 }
695 }
696 RequestKind::QueryStreamed { query, params } => {
697 #[allow(clippy::as_conversions)]
698 let params: SmallVec<[&dyn ToSql; 4]> =
699 params.iter().map(|x| x as &dyn ToSql).collect();
700 let result = client.query(query, params.as_slice()).await?;
701
702 let (tx, rx) = tokio::sync::mpsc::channel(256);
721 let work = Box::pin(async move {
722 let mut stream = result.into_row_stream();
723 while let Some(result) = stream.next().await {
724 if let Err(err) = tx.send(result.err_into()).await {
725 tracing::warn!(?err, "SQL Server row stream receiver went away");
726 }
727 }
728 tracing::info!("SQL Server row stream complete");
729 });
730
731 Ok((Response::RowStream { stream: rx }, Some(work)))
732 }
733 RequestKind::SimpleQuery { query } => {
734 let result = client.simple_query(query).await?;
735
736 let mut results = result.into_results().await.context("into results")?;
737 if results.is_empty() {
738 Ok((Response::Rows(smallvec![]), None))
739 } else if results.len() == 1 {
740 let rows = results.pop().expect("checked len").into();
743 Ok((Response::Rows(rows), None))
744 } else {
745 Err(SqlServerError::ProgrammingError(format!(
746 "Simple query only supports 1 statement, got {}",
747 results.len()
748 )))
749 }
750 }
751 }
752 }
753}
754
755impl IntoFuture for Connection {
756 type Output = ();
757 type IntoFuture = BoxFuture<'static, Self::Output>;
758
759 fn into_future(self) -> Self::IntoFuture {
760 self.run().boxed()
761 }
762}
763
764#[derive(Debug)]
767enum OwnedColumnData {
768 U8(Option<u8>),
769 I16(Option<i16>),
770 I32(Option<i32>),
771 I64(Option<i64>),
772 F32(Option<f32>),
773 F64(Option<f64>),
774 Bit(Option<bool>),
775 String(Option<String>),
776 Guid(Option<uuid::Uuid>),
777 Binary(Option<Vec<u8>>),
778 Numeric(Option<tiberius::numeric::Numeric>),
779 Xml(Option<tiberius::xml::XmlData>),
780 DateTime(Option<tiberius::time::DateTime>),
781 SmallDateTime(Option<tiberius::time::SmallDateTime>),
782 Time(Option<tiberius::time::Time>),
783 Date(Option<tiberius::time::Date>),
784 DateTime2(Option<tiberius::time::DateTime2>),
785 DateTimeOffset(Option<tiberius::time::DateTimeOffset>),
786}
787
788impl<'a> From<tiberius::ColumnData<'a>> for OwnedColumnData {
789 fn from(value: tiberius::ColumnData<'a>) -> Self {
790 match value {
791 tiberius::ColumnData::U8(inner) => OwnedColumnData::U8(inner),
792 tiberius::ColumnData::I16(inner) => OwnedColumnData::I16(inner),
793 tiberius::ColumnData::I32(inner) => OwnedColumnData::I32(inner),
794 tiberius::ColumnData::I64(inner) => OwnedColumnData::I64(inner),
795 tiberius::ColumnData::F32(inner) => OwnedColumnData::F32(inner),
796 tiberius::ColumnData::F64(inner) => OwnedColumnData::F64(inner),
797 tiberius::ColumnData::Bit(inner) => OwnedColumnData::Bit(inner),
798 tiberius::ColumnData::String(inner) => {
799 OwnedColumnData::String(inner.map(|s| s.to_string()))
800 }
801 tiberius::ColumnData::Guid(inner) => OwnedColumnData::Guid(inner),
802 tiberius::ColumnData::Binary(inner) => {
803 OwnedColumnData::Binary(inner.map(|b| b.to_vec()))
804 }
805 tiberius::ColumnData::Numeric(inner) => OwnedColumnData::Numeric(inner),
806 tiberius::ColumnData::Xml(inner) => OwnedColumnData::Xml(inner.map(|x| x.into_owned())),
807 tiberius::ColumnData::DateTime(inner) => OwnedColumnData::DateTime(inner),
808 tiberius::ColumnData::SmallDateTime(inner) => OwnedColumnData::SmallDateTime(inner),
809 tiberius::ColumnData::Time(inner) => OwnedColumnData::Time(inner),
810 tiberius::ColumnData::Date(inner) => OwnedColumnData::Date(inner),
811 tiberius::ColumnData::DateTime2(inner) => OwnedColumnData::DateTime2(inner),
812 tiberius::ColumnData::DateTimeOffset(inner) => OwnedColumnData::DateTimeOffset(inner),
813 }
814 }
815}
816
817impl tiberius::ToSql for OwnedColumnData {
818 fn to_sql(&self) -> tiberius::ColumnData<'_> {
819 match self {
820 OwnedColumnData::U8(inner) => tiberius::ColumnData::U8(*inner),
821 OwnedColumnData::I16(inner) => tiberius::ColumnData::I16(*inner),
822 OwnedColumnData::I32(inner) => tiberius::ColumnData::I32(*inner),
823 OwnedColumnData::I64(inner) => tiberius::ColumnData::I64(*inner),
824 OwnedColumnData::F32(inner) => tiberius::ColumnData::F32(*inner),
825 OwnedColumnData::F64(inner) => tiberius::ColumnData::F64(*inner),
826 OwnedColumnData::Bit(inner) => tiberius::ColumnData::Bit(*inner),
827 OwnedColumnData::String(inner) => {
828 tiberius::ColumnData::String(inner.as_deref().map(Cow::Borrowed))
829 }
830 OwnedColumnData::Guid(inner) => tiberius::ColumnData::Guid(*inner),
831 OwnedColumnData::Binary(inner) => {
832 tiberius::ColumnData::Binary(inner.as_deref().map(Cow::Borrowed))
833 }
834 OwnedColumnData::Numeric(inner) => tiberius::ColumnData::Numeric(*inner),
835 OwnedColumnData::Xml(inner) => {
836 tiberius::ColumnData::Xml(inner.as_ref().map(Cow::Borrowed))
837 }
838 OwnedColumnData::DateTime(inner) => tiberius::ColumnData::DateTime(*inner),
839 OwnedColumnData::SmallDateTime(inner) => tiberius::ColumnData::SmallDateTime(*inner),
840 OwnedColumnData::Time(inner) => tiberius::ColumnData::Time(*inner),
841 OwnedColumnData::Date(inner) => tiberius::ColumnData::Date(*inner),
842 OwnedColumnData::DateTime2(inner) => tiberius::ColumnData::DateTime2(*inner),
843 OwnedColumnData::DateTimeOffset(inner) => tiberius::ColumnData::DateTimeOffset(*inner),
844 }
845 }
846}
847
848impl<'a, T: tiberius::ToSql> From<&'a T> for OwnedColumnData {
849 fn from(value: &'a T) -> Self {
850 OwnedColumnData::from(value.to_sql())
851 }
852}
853
854#[derive(Debug, thiserror::Error)]
855pub enum SqlServerError {
856 #[error(transparent)]
857 SqlServer(#[from] tiberius::error::Error),
858 #[error(transparent)]
859 CdcError(#[from] crate::cdc::CdcError),
860 #[error("expected column '{0}' to be present")]
861 MissingColumn(&'static str),
862 #[error("sql server client encountered I/O error: {0}")]
863 IO(#[from] tokio::io::Error),
864 #[error("found invalid data in the column '{column_name}': {error}")]
865 InvalidData { column_name: String, error: String },
866 #[error("got back a null value when querying for the LSN")]
867 NullLsn,
868 #[error("invalid SQL Server system setting '{name}'. Expected '{expected}'. Got '{actual}'.")]
869 InvalidSystemSetting {
870 name: String,
871 expected: String,
872 actual: String,
873 },
874 #[error("invariant was violated: {0}")]
875 InvariantViolated(String),
876 #[error(transparent)]
877 Generic(#[from] anyhow::Error),
878 #[error("programming error! {0}")]
879 ProgrammingError(String),
880 #[error(
881 "insufficient permissions for tables [{tables}] or capture instances [{capture_instances}]"
882 )]
883 AuthorizationError {
884 tables: String,
885 capture_instances: String,
886 },
887}
888
889#[derive(Debug, thiserror::Error)]
901pub enum SqlServerDecodeError {
902 #[error("column '{column_name}' was invalid when getting as type '{as_type}'")]
903 InvalidColumn {
904 column_name: String,
905 as_type: &'static str,
906 },
907 #[error("found invalid data in the column '{column_name}': {error}")]
908 InvalidData { column_name: String, error: String },
909 #[error("can't decode {sql_server_type:?} as {mz_type:?}")]
910 Unsupported {
911 sql_server_type: SqlServerColumnDecodeType,
912 mz_type: SqlScalarType,
913 },
914}
915
916impl SqlServerDecodeError {
917 fn invalid_timestamp(name: &str, error: mz_repr::adt::timestamp::TimestampError) -> Self {
918 let error = match error {
920 mz_repr::adt::timestamp::TimestampError::OutOfRange => "out of range",
921 };
922 SqlServerDecodeError::InvalidData {
923 column_name: name.to_string(),
924 error: error.to_string(),
925 }
926 }
927
928 fn invalid_date(name: &str, error: mz_repr::adt::date::DateError) -> Self {
929 let error = match error {
931 mz_repr::adt::date::DateError::OutOfRange => "out of range",
932 };
933 SqlServerDecodeError::InvalidData {
934 column_name: name.to_string(),
935 error: error.to_string(),
936 }
937 }
938
939 fn invalid_char(name: &str, expected_chars: usize, found_chars: usize) -> Self {
940 SqlServerDecodeError::InvalidData {
941 column_name: name.to_string(),
942 error: format!("expected {expected_chars} chars found {found_chars}"),
943 }
944 }
945
946 fn invalid_varchar(name: &str, max_chars: usize, found_chars: usize) -> Self {
947 SqlServerDecodeError::InvalidData {
948 column_name: name.to_string(),
949 error: format!("expected max {max_chars} chars found {found_chars}"),
950 }
951 }
952
953 fn invalid_column(name: &str, as_type: &'static str) -> Self {
954 SqlServerDecodeError::InvalidColumn {
955 column_name: name.to_string(),
956 as_type,
957 }
958 }
959}
960
961pub fn quote_identifier(ident: &str) -> String {
968 let mut quoted = ident.replace(']', "]]");
969 quoted.insert(0, '[');
970 quoted.push(']');
971 quoted
972}
973
974pub trait SqlServerCdcMetrics {
975 fn snapshot_table_lock_start(&self, table_name: &str);
977 fn snapshot_table_lock_end(&self, table_name: &str);
979}
980
981pub struct LoggingSqlServerCdcMetrics;
984
985impl SqlServerCdcMetrics for LoggingSqlServerCdcMetrics {
986 fn snapshot_table_lock_start(&self, table_name: &str) {
987 tracing::info!("snapshot_table_lock_start: {table_name}");
988 }
989
990 fn snapshot_table_lock_end(&self, table_name: &str) {
991 tracing::info!("snapshot_table_lock_end: {table_name}");
992 }
993}
994
995#[cfg(test)]
996mod test {
997 use super::*;
998
999 #[mz_ore::test]
1000 fn test_sql_server_escaping() {
1001 assert_eq!("[]", "e_identifier(""));
1002 assert_eq!("[]]]", "e_identifier("]"));
1003 assert_eq!("[a]", "e_identifier("a"));
1004 assert_eq!("[cost(]]\u{00A3})]", "e_identifier("cost(]\u{00A3})"));
1005 assert_eq!("[[g[o[o]][]", "e_identifier("[g[o[o]["));
1006 }
1007}