Skip to main content

mz_sql_server_util/
lib.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
10use 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/// Higher level wrapper around a [`tiberius::Client`] that models transaction
43/// management like other database clients.
44#[derive(Debug)]
45pub struct Client {
46    tx: UnboundedSender<Request>,
47    // The configuration used to create this client.
48    config: Config,
49    // Cached engine edition of the connected instance. Immutable for the life
50    // of a connection, so we query it at most once and reuse it.
51    engine_edition: Option<EngineEdition>,
52}
53// While a Client could implement Clone, it's not obvious how multiple Clients
54// using the same SQL Server connection would interact, so ban it for now.
55static_assertions::assert_not_impl_all!(Client: Clone);
56
57impl Client {
58    /// Connect to the specified SQL Server instance, returning a [`Client`]
59    /// that can be used to query it and a [`Connection`] that must be polled
60    /// to send and receive results.
61    ///
62    /// TODO(sql_server2): Maybe return a `ClientBuilder` here that implements
63    /// IntoFuture and does the default good thing of moving the `Connection`
64    /// into a tokio task? And a `.raw()` option that will instead return both
65    /// the Client and Connection for manual polling.
66    pub async fn connect(config: Config) -> Result<Self, SqlServerError> {
67        // Setup our tunnelling and return any resources that need to be kept
68        // alive for the duration of the connection.
69        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                // N.B. If this tunnel is dropped it will close so we need to
87                // keep it alive for the duration of the connection.
88                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    /// Create a new Client instance with the same configuration that created
121    /// this configuration.
122    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        // TODO(sql_server2): Add a lot more logging here like the Postgres and MySQL clients have.
135
136        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    /// Executes SQL statements in SQL Server, returning the number of rows effected.
151    ///
152    /// Passthrough method for [`tiberius::Client::execute`].
153    ///
154    /// Note: The returned [`Future`] does not need to be awaited for the query
155    /// to be sent.
156    ///
157    /// [`Future`]: std::future::Future
158    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    /// Executes SQL statements in SQL Server, returning the resulting rows.
189    ///
190    /// Passthrough method for [`tiberius::Client::query`].
191    ///
192    /// Note: The returned [`Future`] does not need to be awaited for the query
193    /// to be sent.
194    ///
195    /// [`Future`]: std::future::Future
196    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    /// Executes SQL statements in SQL Server, returning a [`Stream`] of
225    /// resulting rows.
226    ///
227    /// Passthrough method for [`tiberius::Client::query`].
228    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        // Make our initial request which will return a Stream of Rows.
247        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        // "flatten" our initial request into the returned stream.
266        futures::stream::once(request_future).try_flatten()
267    }
268
269    /// Executes multiple queries, delimited with `;` and return multiple
270    /// result sets; one for each query.
271    ///
272    /// Passthrough method for [`tiberius::Client::simple_query`].
273    ///
274    /// Note: The returned [`Future`] does not need to be awaited for the query
275    /// to be sent.
276    ///
277    /// [`Future`]: std::future::Future
278    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    /// Starts a transaction which is automatically rolled back on drop.
300    ///
301    /// To commit or rollback the transaction, see [`Transaction::commit`] and
302    /// [`Transaction::rollback`] respectively.
303    pub async fn transaction(&mut self) -> Result<Transaction<'_>, SqlServerError> {
304        Transaction::new(self).await
305    }
306
307    /// Sets the transaction isolation level for the current session.
308    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    /// Returns the current transaction isolation level for the current session.
318    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    /// Returns the [`EngineEdition`] of the connected instance, querying it on
339    /// first access and caching the result for the life of this [`Client`].
340    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    /// Return a [`CdcStream`] that can be used to track changes for the specified
350    /// `capture_instances`.
351    ///
352    /// [`CdcStream`]: crate::cdc::CdcStream
353    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
367/// A stream of [`tiberius::Row`]s.
368pub 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        // Construct the guard *before* awaiting BEGIN to avoid a potential race where
380        // transaction is started on remote, but the transaction is cancelled before returning.
381        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    /// Creates a savepoint via `SAVE TRANSACTION` with the provided name.
400    /// Creating a savepoint forces a write to the transaction log, which will associate an
401    /// [`Lsn`] with the current transaction.
402    ///
403    /// The savepoint name must follow rules for SQL Server identifiers
404    /// - starts with letter or underscore
405    /// - only contains letters, digits, and underscores
406    /// - no reserved words
407    /// - 32 char max
408    pub async fn create_savepoint(&mut self, savepoint_name: &str) -> Result<(), SqlServerError> {
409        // Limit the name checks to prevent sending a potentially dangerous string to the SQL Server.
410        // We prefer the server do the majority of the validation.
411        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    /// Retrieve the [`Lsn`] associated with the current session.
427    ///
428    /// MS SQL Server will not assign an [`Lsn`] until a write is performed (e.g. via `SAVE TRANSACTION`).
429    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    /// Lock the provided table to prevent writes but allow reads, uses `(TABLOCK, HOLDLOCK)`.
440    ///
441    /// This will set the transaction isolation level to `READ COMMITTED` and then obtain the
442    /// lock using a `SELECT` statement that will not read any data from the table.
443    /// The lock is released after transaction commit or rollback.
444    pub async fn lock_table_shared(
445        &mut self,
446        schema: &str,
447        table: &str,
448    ) -> Result<(), SqlServerError> {
449        // Locks in MS SQL server do not behave the same way under all isolation levels. In testing,
450        // it has been observed that if the isolation level is SNAPSHOT, these locks are ineffective.
451        static SET_READ_COMMITTED: &str = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED;";
452        // This query probably seems odd, but there is no LOCK command in MS SQL. Locks are specified
453        // in SELECT using the WITH keyword.  This query does not need to return any rows to lock the table,
454        // hence the 1=0, which is something short that always evaluates to false in this universe.
455        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    /// See [`Client::execute`].
465    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    /// See [`Client::query`].
474    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    /// See [`Client::query_streaming`]
483    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    /// See [`Client::simple_query`].
495    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    /// Rollback the [`Transaction`].
503    pub async fn rollback(mut self) -> Result<(), SqlServerError> {
504        static ROLLBACK_QUERY: &str = "ROLLBACK TRANSACTION";
505        // N.B. Mark closed _before_ running the query. This prevents us from
506        // double closing the transaction if this query itself fails.
507        self.closed = true;
508        self.client.simple_query(ROLLBACK_QUERY).await?;
509        Ok(())
510    }
511
512    /// Commit the [`Transaction`].
513    pub async fn commit(mut self) -> Result<(), SqlServerError> {
514        static COMMIT_QUERY: &str = "COMMIT TRANSACTION";
515        // N.B. Mark closed _before_ running the query. This prevents us from
516        // double closing the transaction if this query itself fails.
517        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            // Send the ROLLBACK request directly through the channel, bypassing
527            // the async `simple_query` method. We cannot `.await` in `Drop`, and
528            // merely calling an async fn without awaiting it does nothing (the
529            // future is never polled so the channel send inside never executes).
530            //
531            // We intentionally drop the response receiver since we cannot await
532            // it in a synchronous context. The Connection task will execute the
533            // ROLLBACK and discard the response when the receiver is gone.
534            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/// Transaction isolation levels defined by Microsoft's SQL Server.
544///
545/// See: <https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql>
546#[derive(Debug, PartialEq, Eq)]
547pub enum TransactionIsolationLevel {
548    ReadUncommitted,
549    ReadCommitted,
550    RepeatableRead,
551    Snapshot,
552    Serializable,
553}
554
555impl TransactionIsolationLevel {
556    /// Return the string representation of a transaction isolation level.
557    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    /// Try to parse a [`TransactionIsolationLevel`] from the value returned from SQL Server.
568    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    /// Other end of the channel that [`Client`] holds.
625    rx: UnboundedReceiver<Request>,
626    /// Actual client that we use to send requests.
627    client: tiberius::Client<Compat<TcpStream>>,
628    /// Resources (e.g. SSH tunnel) that need to be held open for the life of this connection.
629    _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            // We don't care if our listener for this query has gone away.
643            let _ = tx.send(response);
644
645            // After we handle a request there might still be something in-flight
646            // that we need to continue driving, e.g. when the response is a
647            // Stream of Rows.
648            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, &params[..]).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                    // TODO(sql_server3): Don't use `into_results()` above, instead directly
686                    // push onto a SmallVec to avoid the heap allocations.
687                    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                // ~~ Rust Lifetimes ~~
703                //
704                // What's going on here, why do we have some extra channel and
705                // this 'work' future?
706                //
707                // Remember, we run the actual `tiberius::Client` in a separate
708                // `tokio::task` and the `mz::Client` sends query requests via
709                // a channel, this allows us to "automatically" manage
710                // transactions.
711                //
712                // But the returned `QueryStream` from a `tiberius::Client` has
713                // a lifetime associated with said client running in this
714                // separate task. Thus we cannot send the `QueryStream` back to
715                // the `mz::Client` because the lifetime of these two clients
716                // is not linked at all. The fix is to create a separate owned
717                // channel and return the receiving end, while this work future
718                // pulls events off the `QueryStream` and sends them over the
719                // channel we just returned.
720                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                    // TODO(sql_server3): Don't use `into_results()` above, instead directly
741                    // push onto a SmallVec to avoid the heap allocations.
742                    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/// Owned version of [`tiberius::ColumnData`] that can be more easily sent
765/// across threads or through a channel.
766#[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/// Errors returned from decoding SQL Server rows.
890///
891/// **PLEASE READ**
892///
893/// The string representation of this error type is **durably stored** in a source and thus this
894/// error type needs to be **stable** across releases. For example, if in v11 of Materialize we
895/// fail to decode `Row(["foo bar"])` from SQL Server, we will record the error in the source's
896/// Persist shard. If in v12 of Materialize the user deletes the `Row(["foo bar"])` from their
897/// upstream instance, we need to perfectly retract the error we previously committed.
898///
899/// This means be **very** careful when changing this type.
900#[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        // These error messages need to remain stable, do not change them.
919        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        // These error messages need to remain stable, do not change them.
930        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
961/// Quotes the provided string using '[]' to match SQL Server `QUOTENAME` function. This form
962/// of quotes is unaffected by the SQL Server setting `SET QUOTED_IDENTIFIER`.
963///
964/// See:
965/// - <https://learn.microsoft.com/en-us/sql/t-sql/functions/quotename-transact-sql?view=sql-server-ver17>
966/// - <https://learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql?view=sql-server-ver17>
967pub 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    /// Called before the table lock is aquired
976    fn snapshot_table_lock_start(&self, table_name: &str);
977    /// Called after the table lock is released
978    fn snapshot_table_lock_end(&self, table_name: &str);
979}
980
981/// A simple implementation of [`SqlServerCdcMetrics`] that uses the tracing framework to log
982/// the start and end conditions.
983pub 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!("[]", &quote_identifier(""));
1002        assert_eq!("[]]]", &quote_identifier("]"));
1003        assert_eq!("[a]", &quote_identifier("a"));
1004        assert_eq!("[cost(]]\u{00A3})]", &quote_identifier("cost(]\u{00A3})"));
1005        assert_eq!("[[g[o[o]][]", &quote_identifier("[g[o[o]["));
1006    }
1007}