Skip to main content

connectorx/sources/mssql/
tds_impl.rs

1//! `mssql-tds`-backed implementation of the MsSQL source.
2//!
3//! Added in Phase 2 of the tiberius -> mssql-tds migration (see
4//! sfu-db/connector-x#942). Compiled only when the `src_mssql_tds` feature
5//! is active. This implementation targets the same
6//! [`super::typesystem::MsSQLTypeSystem`] and the same set of native Rust
7//! produce types as [`super::tiberius_impl`], so
8//! [`crate::transports::mssql_arrow::MsSQLArrowTransport`] works unchanged
9//! against either backend.
10//!
11//! Automatic partition-range discovery supports integer and floating-point
12//! columns, matching Tiberius's NULL and float-to-integer conversions.
13//! Metadata, counts and partition readers share a per-source bounded bb8 pool.
14//! Partitions acquire connections only when executing, so their number may
15//! exceed the pool size.
16//!
17//! Known, intentional differences from the Tiberius path (tracked as
18//! documented gaps, not bugs):
19//! - `trust_server_certificate_ca` (validate the server cert against a
20//!   specific CA bundle without disabling hostname/chain checks) is
21//!   rejected outright: `mssql-tds`'s `EncryptionOptions::server_certificate`
22//!   only supports exact-match certificate pinning, not CA validation.
23//! - The default TLS posture differs: Tiberius defaults to
24//!   `EncryptionLevel::NotSupported` (advertise TLS as unsupported) when
25//!   `encrypt` is unset (prelogin 0x02). `mssql-tds` has no equivalent public
26//!   setting: unset and `encrypt=false` both map to `PreferOff` (0x00).
27//!   This permits login-only TLS, or full-session TLS if the server requires it.
28//!   The driver's login-only TLS unconditionally skips certificate-chain validation,
29//!   even when `trust_server_certificate=false`. Use `encrypt=true` for required
30//!   full-session TLS (0x03), with certificate-chain validation enabled unless
31//!   `trust_server_certificate=true` is explicitly requested.
32
33use super::driver;
34use super::errors::MsSQLSourceError;
35use super::typesystem::{FloatN, IntN, MsSQLTypeSystem};
36use crate::constants::DB_BUFFER_SIZE;
37use crate::{
38    data_order::DataOrder,
39    errors::ConnectorXError,
40    sources::{PartitionParser, Produce, Source, SourcePartition},
41    sql::{count_query, get_partition_range_query, CXQuery},
42};
43use anyhow::anyhow;
44use bb8::{ManageConnection, Pool, PooledConnection};
45use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
46use fehler::{throw, throws};
47use log::{debug, warn};
48use mssql_tds::connection::client_context::{ClientContext, TdsAuthenticationMethod};
49use mssql_tds::connection::tds_client::{ResultSet, TdsClient};
50use mssql_tds::connection_provider::tds_connection_provider::TdsConnectionProvider;
51use mssql_tds::core::{EncryptionOptions, EncryptionSetting};
52use mssql_tds::datatypes::column_values::{
53    ColumnValues, SqlDate, SqlDateTime, SqlDateTime2, SqlDateTimeOffset, SqlSmallDateTime, SqlTime,
54};
55use mssql_tds::datatypes::decoder::DecimalParts;
56use mssql_tds::datatypes::sqldatatypes::TdsDataType;
57use rust_decimal::Decimal;
58use sqlparser::dialect::MsSqlDialect;
59use std::collections::HashMap;
60use std::convert::TryFrom;
61use std::ops::{Deref, DerefMut};
62use std::sync::{Arc, OnceLock};
63use std::time::Duration;
64use tokio::runtime::Runtime;
65use url::Url;
66use urlencoding::decode;
67use uuid_old::Uuid;
68
69#[derive(Clone, PartialEq)]
70struct UserAgentInfo {
71    version: String,
72    runtime: String,
73}
74
75static USER_AGENT_INFO: OnceLock<UserAgentInfo> = OnceLock::new();
76
77/// Sets the wrapper's package version and runtime for the TDS User-Agent, leaving LOGIN7 unchanged.
78/// Language bindings should call this before creating connections. Reinitializing
79/// with the same values is allowed; different values are rejected.
80pub fn set_user_agent_info(version: String, runtime: String) -> Result<(), &'static str> {
81    if version.is_empty() {
82        return Err("MSSQL User-Agent version must not be empty");
83    }
84    if runtime.is_empty() {
85        return Err("MSSQL User-Agent runtime must not be empty");
86    }
87    let info = UserAgentInfo { version, runtime };
88    if USER_AGENT_INFO.get_or_init(|| info.clone()) != &info {
89        return Err("MSSQL User-Agent is already initialized with different values");
90    }
91    Ok(())
92}
93
94/// Builds the `mssql-tds` datasource string and [`ClientContext`] from a
95/// ConnectorX MSSQL connection URL. Mirrors
96/// [`super::tiberius_impl::mssql_config`]'s parsing, but expressed against
97/// `mssql-tds`'s config surface.
98#[throws(MsSQLSourceError)]
99fn build_client_context(url: &Url) -> (String, ClientContext) {
100    let host = decode(url.host_str().unwrap_or("localhost"))?.into_owned();
101    let port = url.port().unwrap_or(1433);
102    let hosts: Vec<&str> = host.split('\\').collect();
103    let datasource = match hosts.len() {
104        1 => format!("tcp:{},{}", hosts[0], port),
105        // SQL Server named instance: `server\instance`. Let mssql-tds's own
106        // SSRP resolution figure out the port, same as Tiberius does today.
107        2 => format!("{}\\{}", hosts[0], hosts[1]),
108        _ => throw!(anyhow!("MsSQL hostname parse error: {}", host)),
109    };
110
111    let mut context = ClientContext::with_data_source(&datasource);
112    // The User-Agent feature carries a separate driver name from LOGIN7.
113    context
114        .user_agent
115        .set_library_name("connectorx".to_string());
116    if let Some(info) = USER_AGENT_INFO.get() {
117        context.user_agent.set_driver_version(info.version.clone());
118        context.user_agent.set_runtime(info.runtime.clone());
119    }
120    context.application_name = "ConnectorX".to_string();
121    context.database = decode(&url.path()[1..])?.into_owned();
122
123    let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
124
125    match params.get("trusted_connection") {
126        Some(v) if v == "true" && cfg!(any(windows, feature = "integrated-auth-gssapi")) => {
127            debug!("mssql-tds auth through integrated (SSPI) authentication");
128            context.tds_authentication_method = TdsAuthenticationMethod::SSPI;
129        }
130        _ => {
131            debug!("mssql-tds auth through sqlserver authentication");
132            context.tds_authentication_method = TdsAuthenticationMethod::Password;
133            context.user_name = decode(url.username())?.into_owned();
134            context.password = decode(url.password().unwrap_or(""))?.into_owned();
135        }
136    }
137
138    if let Some(v) = params.get("trust_server_certificate_ca") {
139        // See the module-level doc comment: mssql-tds has no CA-validation
140        // equivalent yet, only exact-match certificate pinning. Reject
141        // rather than silently downgrade TLS trust.
142        throw!(anyhow!(
143            "trust_server_certificate_ca={} is not supported with the src_mssql_tds backend: \
144             mssql-tds only supports exact-match certificate pinning, not CA validation",
145            v
146        ));
147    }
148
149    let mut encryption = EncryptionOptions::new();
150    encryption.mode = EncryptionSetting::PreferOff;
151    match params.get("encrypt") {
152        Some(v) if v.to_lowercase() == "true" => encryption.mode = EncryptionSetting::Required,
153        Some(v) if v.to_lowercase() == "false" => encryption.mode = EncryptionSetting::PreferOff,
154        _ => {}
155    }
156    if let Some(v) = params.get("trust_server_certificate") {
157        if v.to_lowercase() == "true" {
158            encryption.trust_server_certificate = true;
159        }
160    }
161    context.encryption_options = encryption;
162
163    if let Some(appname) = params.get("appname") {
164        context.application_name = decode(appname)?.into_owned();
165    }
166
167    (datasource, context)
168}
169
170#[cfg(test)]
171mod configuration_tests {
172    use super::*;
173
174    #[test]
175    fn python_user_agent_info_preserves_login_version() {
176        let version = "0.4.7a1";
177        let runtime = "Python 3.12.3";
178        assert!(set_user_agent_info(String::new(), runtime.to_string()).is_err());
179        assert!(set_user_agent_info(version.to_string(), String::new()).is_err());
180        set_user_agent_info(version.to_string(), runtime.to_string()).unwrap();
181        set_user_agent_info(version.to_string(), runtime.to_string()).unwrap();
182        assert!(set_user_agent_info("0.4.8".to_string(), runtime.to_string()).is_err());
183        assert!(set_user_agent_info(version.to_string(), "Python 3.13.0".to_string()).is_err());
184
185        let url = Url::parse("mssql://localhost/db").unwrap();
186        let (_, context) = build_client_context(&url).unwrap();
187        assert_eq!(context.user_agent.driver_version, version);
188        assert_eq!(context.user_agent.runtime, runtime);
189        assert_eq!(
190            context.driver_version,
191            ClientContext::with_data_source("tcp:localhost,1433").driver_version
192        );
193    }
194
195    #[test]
196    fn driver_identity_and_application_name() {
197        for (query, expected_appname) in [
198            ("", "ConnectorX"),
199            ("?appname=Custom%20Application", "Custom Application"),
200            ("?appname=", ""),
201        ] {
202            let url = Url::parse(&format!("mssql://localhost/db{query}")).unwrap();
203            let (_, context) = build_client_context(&url).unwrap();
204            assert_eq!(
205                context.library_name,
206                ClientContext::with_data_source("tcp:localhost,1433").library_name
207            );
208            assert_eq!(context.user_agent.library_name, "connectorx");
209            assert_eq!(context.application_name, expected_appname);
210        }
211    }
212
213    #[test]
214    fn integrated_auth_respects_tiberius_platform_and_feature_gates() {
215        for trusted in ["true", "false", "TrUe"] {
216            let url = Url::parse(&format!(
217                "mssql://test_user:test_password@localhost/db?trusted_connection={trusted}"
218            ))
219            .unwrap();
220            let (_, context) = build_client_context(&url).unwrap();
221            if trusted == "true" && cfg!(any(windows, feature = "integrated-auth-gssapi")) {
222                assert!(matches!(
223                    context.tds_authentication_method,
224                    TdsAuthenticationMethod::SSPI
225                ));
226            } else {
227                assert!(matches!(
228                    context.tds_authentication_method,
229                    TdsAuthenticationMethod::Password
230                ));
231                assert_eq!(context.user_name, "test_user");
232                assert_eq!(context.password, "test_password");
233            }
234        }
235    }
236
237    #[test]
238    fn tls_mapping_preserves_explicit_encryption_and_trust() {
239        for (encrypt, mode) in [
240            ("", EncryptionSetting::PreferOff),
241            ("true", EncryptionSetting::Required),
242            ("TrUe", EncryptionSetting::Required),
243            ("false", EncryptionSetting::PreferOff),
244            ("FaLsE", EncryptionSetting::PreferOff),
245            ("invalid", EncryptionSetting::PreferOff),
246        ] {
247            for (trust, expected_trust) in [
248                ("", false),
249                ("true", true),
250                ("TrUe", true),
251                ("false", false),
252                ("FaLsE", false),
253                ("invalid", false),
254            ] {
255                let mut url = Url::parse("mssql://user:password@localhost/db").unwrap();
256                if !encrypt.is_empty() {
257                    url.query_pairs_mut().append_pair("encrypt", encrypt);
258                }
259                if !trust.is_empty() {
260                    url.query_pairs_mut()
261                        .append_pair("trust_server_certificate", trust);
262                }
263                let (_, context) = build_client_context(&url).unwrap();
264                assert_eq!(
265                    context.encryption_options,
266                    EncryptionOptions {
267                        mode,
268                        trust_server_certificate: expected_trust,
269                        host_name_in_cert: None,
270                        server_certificate: None,
271                    },
272                    "encrypt={encrypt:?}, trust_server_certificate={trust:?}"
273                );
274            }
275        }
276    }
277
278    #[test]
279    fn custom_ca_is_rejected_even_when_trust_is_enabled() {
280        for trust in ["true", "false"] {
281            let url = Url::parse(&format!(
282                "mssql://localhost/db?encrypt=true&trust_server_certificate={trust}\
283                 &trust_server_certificate_ca=ca.pem"
284            ))
285            .unwrap();
286            let err = build_client_context(&url).err().unwrap();
287            assert!(err.to_string().contains("not CA validation"));
288        }
289    }
290
291    #[test]
292    fn pool_size_must_be_positive_and_fit_u32() {
293        let rt = Arc::new(Runtime::new().unwrap());
294        assert!(MsSQLSource::new(rt.clone(), "mssql://localhost/db", 0).is_err());
295        if usize::BITS > 32 {
296            assert!(MsSQLSource::new(rt, "mssql://localhost/db", usize::MAX).is_err());
297        }
298    }
299}
300
301#[throws(MsSQLSourceError)]
302fn open_connection(rt: &Runtime, url: &Url) -> TdsClient {
303    let (datasource, context) = build_client_context(url)?;
304    let provider = TdsConnectionProvider::new();
305    rt.block_on(provider.create_client(context, &datasource, None))?
306}
307
308struct ConnectionManager {
309    url: Url,
310}
311
312impl ManageConnection for ConnectionManager {
313    type Connection = TdsClient;
314    type Error = MsSQLSourceError;
315
316    async fn connect(&self) -> Result<TdsClient, Self::Error> {
317        let (datasource, context) = build_client_context(&self.url)?;
318        Ok(TdsConnectionProvider::new()
319            .create_client(context, &datasource, None)
320            .await?)
321    }
322
323    async fn is_valid(&self, client: &mut TdsClient) -> Result<(), Self::Error> {
324        if client.is_connection_dead() {
325            return Err(anyhow!("MsSQL pooled connection is closed").into());
326        }
327        // Reset session settings and transactions before the next borrower's query.
328        client.prepare_reset_connection(false);
329        Ok(())
330    }
331
332    fn has_broken(&self, client: &mut TdsClient) -> bool {
333        client.is_connection_dead() || client.has_open_batch()
334    }
335}
336
337#[throws(MsSQLSourceError)]
338fn connection_pool(rt: &Runtime, url: &Url, nconn: usize) -> Pool<ConnectionManager> {
339    let max_size = u32::try_from(nconn)
340        .ok()
341        .filter(|&size| size > 0)
342        .ok_or_else(|| anyhow!("MsSQL pool size must be between 1 and {}", u32::MAX))?;
343    // Fail on unsupported URL options even before the first checkout.
344    build_client_context(url)?;
345    rt.block_on(
346        Pool::builder()
347            .max_size(max_size)
348            .test_on_check_out(true)
349            .build(ConnectionManager { url: url.clone() }),
350    )?
351}
352
353/// Owns the lease and keeps its runtime alive through cleanup and pool return.
354struct Connection {
355    rt: Arc<Runtime>,
356    lease: Option<PooledConnection<'static, ConnectionManager>>,
357}
358
359impl Connection {
360    #[throws(MsSQLSourceError)]
361    fn get(rt: Arc<Runtime>, pool: &Pool<ConnectionManager>) -> Self {
362        let lease = rt.block_on(pool.get_owned()).map_err(|err| match err {
363            bb8::RunError::User(err) => err,
364            bb8::RunError::TimedOut => anyhow!("Timed out waiting for an MsSQL connection").into(),
365        })?;
366        Self {
367            rt,
368            lease: Some(lease),
369        }
370    }
371
372    #[throws(MsSQLSourceError)]
373    fn finish(&mut self) {
374        let client = self.lease.as_mut().unwrap();
375        if let Err(err) = self.rt.block_on(client.close_query()) {
376            client.mark_connection_dead();
377            throw!(err);
378        }
379    }
380}
381
382impl Deref for Connection {
383    type Target = TdsClient;
384
385    fn deref(&self) -> &TdsClient {
386        self.lease.as_ref().unwrap()
387    }
388}
389
390impl DerefMut for Connection {
391    fn deref_mut(&mut self) -> &mut TdsClient {
392        self.lease.as_mut().unwrap()
393    }
394}
395
396impl Drop for Connection {
397    fn drop(&mut self) {
398        let mut client = self.lease.take().unwrap();
399        if !client.is_connection_dead() && client.has_open_batch() {
400            // Early parser drops must not return unread protocol data to the pool.
401            // Bound destructor work; a timed-out drain is discarded, never reused.
402            match self.rt.block_on(async {
403                tokio::time::timeout(Duration::from_secs(5), client.close_query()).await
404            }) {
405                Ok(Ok(())) => {}
406                Ok(Err(err)) => {
407                    warn!("Discarding MsSQL connection after cleanup failure: {}", err);
408                    client.mark_connection_dead();
409                }
410                Err(_) => {
411                    warn!("Discarding MsSQL connection after cleanup timeout");
412                    client.mark_connection_dead();
413                }
414            }
415        }
416        // bb8 may spawn replacement connections when returning a broken lease.
417        let _guard = self.rt.enter();
418        drop(client);
419    }
420}
421
422/// Runs `sql`, returning its first row for `COUNT(*)` probes.
423#[throws(MsSQLSourceError)]
424fn first_row(rt: &Runtime, client: &mut TdsClient, sql: String) -> Option<Vec<ColumnValues>> {
425    rt.block_on(client.execute(sql, ()))?;
426    rt.block_on(client.next_row())?
427}
428
429#[throws(MsSQLSourceError)]
430pub(crate) fn tds_get_partition_range(url: &Url, query: &str, col: &str) -> (i64, i64) {
431    let range_query = get_partition_range_query(query, col, &MsSqlDialect {})?;
432    let rt = Runtime::new().map_err(anyhow::Error::from)?;
433    let mut client = open_connection(&rt, url)?;
434    rt.block_on(client.execute(range_query, ()))?;
435    let types: Vec<_> = client
436        .get_metadata()
437        .iter()
438        .map(|column| column.data_type)
439        .collect();
440    let row = rt
441        .block_on(client.next_row())?
442        .ok_or_else(|| anyhow!("MsSQL partition range query returned no row"))?;
443    if row.len() != 2 || types.len() != 2 {
444        throw!(anyhow!(
445            "MsSQL partition range query must return two columns"
446        ));
447    }
448    (
449        partition_range_value(&row[0], types[0])?,
450        partition_range_value(&row[1], types[1])?,
451    )
452}
453
454#[throws(MsSQLSourceError)]
455fn partition_range_value(value: &ColumnValues, ty: TdsDataType) -> i64 {
456    if !matches!(
457        ty,
458        TdsDataType::Int1
459            | TdsDataType::Int2
460            | TdsDataType::Int4
461            | TdsDataType::Int8
462            | TdsDataType::IntN
463            | TdsDataType::Flt4
464            | TdsDataType::Flt8
465            | TdsDataType::FltN
466    ) {
467        throw!(anyhow!(
468            "Partition can only be done on int or float columns"
469        ));
470    }
471    // Match Tiberius: NULL aggregates become zero and floats truncate to i64.
472    match value {
473        ColumnValues::Null => 0,
474        ColumnValues::TinyInt(n) => i64::from(*n),
475        ColumnValues::SmallInt(n) => i64::from(*n),
476        ColumnValues::Int(n) => i64::from(*n),
477        ColumnValues::BigInt(n) => *n,
478        ColumnValues::Real(n) => *n as i64,
479        ColumnValues::Float(n) => *n as i64,
480        other => throw!(anyhow!(
481            "Unexpected MsSQL partition range value: {:?}",
482            other
483        )),
484    }
485}
486
487#[cfg(test)]
488mod partition_range_tests {
489    use super::*;
490
491    #[test]
492    fn numeric_bounds_preserve_tiberius_conversions() {
493        for (value, ty, expected) in [
494            (ColumnValues::TinyInt(255), TdsDataType::Int1, 255),
495            (ColumnValues::SmallInt(-32768), TdsDataType::Int2, -32768),
496            (
497                ColumnValues::Int(i32::MIN),
498                TdsDataType::Int4,
499                i64::from(i32::MIN),
500            ),
501            (ColumnValues::BigInt(i64::MIN), TdsDataType::Int8, i64::MIN),
502            (ColumnValues::BigInt(i64::MAX), TdsDataType::IntN, i64::MAX),
503            (ColumnValues::Real(-12.75), TdsDataType::Flt4, -12),
504            (ColumnValues::Float(23.5), TdsDataType::Flt8, 23),
505            (ColumnValues::Float(-12.75), TdsDataType::FltN, -12),
506            (ColumnValues::Null, TdsDataType::IntN, 0),
507            (ColumnValues::Null, TdsDataType::FltN, 0),
508        ] {
509            assert_eq!(partition_range_value(&value, ty).unwrap(), expected);
510        }
511    }
512
513    #[test]
514    fn unsupported_bounds_are_errors_even_when_null() {
515        for ty in [
516            TdsDataType::BigVarChar,
517            TdsDataType::DecimalN,
518            TdsDataType::MoneyN,
519        ] {
520            let err = partition_range_value(&ColumnValues::Null, ty).unwrap_err();
521            assert!(err
522                .to_string()
523                .contains("Partition can only be done on int or float columns"));
524        }
525        assert!(partition_range_value(&ColumnValues::Bit(true), TdsDataType::IntN).is_err());
526    }
527}
528
529pub struct MsSQLSource {
530    rt: Arc<Runtime>,
531    conn_url: Url,
532    pool: Pool<ConnectionManager>,
533    origin_query: Option<String>,
534    queries: Vec<CXQuery<String>>,
535    names: Vec<String>,
536    schema: Vec<MsSQLTypeSystem>,
537}
538
539impl MsSQLSource {
540    #[throws(MsSQLSourceError)]
541    pub fn new(rt: Arc<Runtime>, conn: &str, nconn: usize) -> Self {
542        debug!("mssql source using driver: {:?}", driver::active_driver());
543        let conn_url = Url::parse(conn)?;
544        let pool = connection_pool(&rt, &conn_url, nconn)?;
545
546        Self {
547            rt,
548            conn_url,
549            pool,
550            origin_query: None,
551            queries: vec![],
552            names: vec![],
553            schema: vec![],
554        }
555    }
556}
557
558impl Source for MsSQLSource
559where
560    MsSQLSourcePartition: SourcePartition<TypeSystem = MsSQLTypeSystem, Error = MsSQLSourceError>,
561{
562    const DATA_ORDERS: &'static [DataOrder] = &[DataOrder::RowMajor];
563    type Partition = MsSQLSourcePartition;
564    type TypeSystem = MsSQLTypeSystem;
565    type Error = MsSQLSourceError;
566
567    #[throws(MsSQLSourceError)]
568    fn set_data_order(&mut self, data_order: DataOrder) {
569        if !matches!(data_order, DataOrder::RowMajor) {
570            throw!(ConnectorXError::UnsupportedDataOrder(data_order));
571        }
572    }
573
574    fn set_queries<Q: ToString>(&mut self, queries: &[CXQuery<Q>]) {
575        self.queries = queries.iter().map(|q| q.map(Q::to_string)).collect();
576    }
577
578    fn set_origin_query(&mut self, query: Option<String>) {
579        self.origin_query = query;
580    }
581
582    #[throws(MsSQLSourceError)]
583    fn fetch_metadata(&mut self) {
584        assert!(!self.queries.is_empty());
585
586        let first_query = self.queries[0].clone();
587        let mut client = Connection::get(self.rt.clone(), &self.pool)?;
588        self.rt
589            .block_on(client.execute(first_query.as_str().to_string(), ()))?;
590
591        let metadata = client.get_metadata();
592        if metadata.is_empty() {
593            throw!(anyhow!(
594                "MsSQL returned no columns for query: {}",
595                first_query
596            ));
597        }
598
599        let (names, types) = metadata
600            .iter()
601            .map(|col| (col.column_name.clone(), MsSQLTypeSystem::from(col)))
602            .unzip();
603        client.finish()?;
604
605        self.names = names;
606        self.schema = types;
607    }
608
609    #[throws(MsSQLSourceError)]
610    fn result_rows(&mut self) -> Option<usize> {
611        match &self.origin_query {
612            Some(q) => {
613                let cxq = CXQuery::Naked(q.clone());
614                let cquery = count_query(&cxq, &MsSqlDialect {})?;
615                let mut client = Connection::get(self.rt.clone(), &self.pool)?;
616                let row = first_row(&self.rt, &mut client, cquery.as_str().to_string())?
617                    .ok_or_else(|| anyhow!("MsSQL failed to get the count of query: {}", q))?;
618                client.finish()?;
619                Some(row_count_value(&row, q)?)
620            }
621            None => None,
622        }
623    }
624
625    fn names(&self) -> Vec<String> {
626        self.names.clone()
627    }
628
629    fn schema(&self) -> Vec<Self::TypeSystem> {
630        self.schema.clone()
631    }
632
633    #[throws(MsSQLSourceError)]
634    fn partition(self) -> Vec<Self::Partition> {
635        let mut ret = vec![];
636        for query in self.queries {
637            let mut partition = MsSQLSourcePartition::new(
638                self.rt.clone(),
639                self.conn_url.clone(),
640                &query,
641                &self.schema,
642            );
643            partition.pool = Some(self.pool.clone());
644            ret.push(partition);
645        }
646        ret
647    }
648}
649
650/// Reads the first column of a `COUNT(*)` result row. SQL Server always
651/// returns `COUNT(*)` as `int`.
652#[throws(MsSQLSourceError)]
653fn row_count_value(row: &[ColumnValues], query: &str) -> usize {
654    match row.first() {
655        Some(ColumnValues::Int(n)) => *n as usize,
656        Some(ColumnValues::BigInt(n)) => *n as usize,
657        other => throw!(anyhow!(
658            "MsSQL count query for '{}' returned unexpected value: {:?}",
659            query,
660            other
661        )),
662    }
663}
664
665pub struct MsSQLSourcePartition {
666    rt: Arc<Runtime>,
667    conn_url: Url,
668    pool: Option<Pool<ConnectionManager>>,
669    query: CXQuery<String>,
670    schema: Vec<MsSQLTypeSystem>,
671    nrows: usize,
672    ncols: usize,
673}
674
675impl MsSQLSourcePartition {
676    pub fn new(
677        rt: Arc<Runtime>,
678        conn_url: Url,
679        query: &CXQuery<String>,
680        schema: &[MsSQLTypeSystem],
681    ) -> Self {
682        Self {
683            rt,
684            conn_url,
685            pool: None,
686            query: query.clone(),
687            schema: schema.to_vec(),
688            nrows: 0,
689            ncols: schema.len(),
690        }
691    }
692
693    #[throws(MsSQLSourceError)]
694    fn connection(&mut self) -> Connection {
695        // Preserve the public, infallible standalone partition constructor.
696        if self.pool.is_none() {
697            self.pool = Some(connection_pool(&self.rt, &self.conn_url, 1)?);
698        }
699        Connection::get(self.rt.clone(), self.pool.as_ref().unwrap())?
700    }
701}
702
703impl SourcePartition for MsSQLSourcePartition {
704    type TypeSystem = MsSQLTypeSystem;
705    // Owned leases need no borrow from the partition or unsafe self-reference.
706    type Parser<'a> = MsSQLSourceParser;
707    type Error = MsSQLSourceError;
708
709    #[throws(MsSQLSourceError)]
710    fn result_rows(&mut self) {
711        let cquery = count_query(&self.query, &MsSqlDialect {})?;
712        let mut client = self.connection()?;
713        let row = first_row(&self.rt, &mut client, cquery.as_str().to_string())?
714            .ok_or_else(|| anyhow!("MsSQL failed to get the count of query: {}", self.query))?;
715        client.finish()?;
716        self.nrows = row_count_value(&row, self.query.as_str())?;
717    }
718
719    #[throws(MsSQLSourceError)]
720    fn parser<'a>(&'a mut self) -> Self::Parser<'a> {
721        let mut client = self.connection()?;
722        self.rt
723            .block_on(client.execute(self.query.as_str().to_string(), ()))?;
724        MsSQLSourceParser::new(self.rt.clone(), client, self.schema.len())
725    }
726
727    fn nrows(&self) -> usize {
728        self.nrows
729    }
730
731    fn ncols(&self) -> usize {
732        self.ncols
733    }
734}
735
736/// One decoded, native-Rust representation of a single `ColumnValues` cell.
737/// Converting eagerly (once, when the row arrives) rather than on every
738/// `Produce` call keeps the per-cell conversion logic (decimal, date/time,
739/// money math) in one place and lets `&str`/`&[u8]` `Produce` impls borrow
740/// directly from this buffer.
741#[derive(Debug)]
742enum TdsCell {
743    Null,
744    U8(u8),
745    I16(i16),
746    I32(i32),
747    I64(i64),
748    F32(f32),
749    F64(f64),
750    Bool(bool),
751    Str(String),
752    Bytes(Vec<u8>),
753    Uuid(Uuid),
754    Decimal(Decimal),
755    NaiveDate(NaiveDate),
756    NaiveTime(NaiveTime),
757    NaiveDateTime(NaiveDateTime),
758    DateTimeUtc(DateTime<Utc>),
759}
760
761pub struct MsSQLSourceParser {
762    rt: Arc<Runtime>,
763    client: Connection,
764    rowbuf: Vec<Vec<TdsCell>>,
765    ncols: usize,
766    current_col: usize,
767    current_row: usize,
768    is_finished: bool,
769}
770
771impl MsSQLSourceParser {
772    fn new(rt: Arc<Runtime>, client: Connection, ncols: usize) -> Self {
773        Self {
774            rt,
775            client,
776            rowbuf: Vec::with_capacity(DB_BUFFER_SIZE),
777            ncols,
778            current_row: 0,
779            current_col: 0,
780            is_finished: false,
781        }
782    }
783
784    #[throws(MsSQLSourceError)]
785    fn next_loc(&mut self) -> (usize, usize) {
786        let ret = (self.current_row, self.current_col);
787        self.current_row += (self.current_col + 1) / self.ncols;
788        self.current_col = (self.current_col + 1) % self.ncols;
789        ret
790    }
791
792    /// Takes ownership of a cell out of the row buffer, leaving `Null`
793    /// behind. Safe because each cell is produced exactly once per row (see
794    /// `next_loc`).
795    fn take_cell(&mut self, ridx: usize, cidx: usize) -> TdsCell {
796        std::mem::replace(&mut self.rowbuf[ridx][cidx], TdsCell::Null)
797    }
798}
799
800impl<'a> PartitionParser<'a> for MsSQLSourceParser {
801    type TypeSystem = MsSQLTypeSystem;
802    type Error = MsSQLSourceError;
803
804    #[throws(MsSQLSourceError)]
805    fn fetch_next(&mut self) -> (usize, bool) {
806        assert!(self.current_col == 0);
807        let remaining_rows = self.rowbuf.len() - self.current_row;
808        if remaining_rows > 0 {
809            return (remaining_rows, self.is_finished);
810        } else if self.is_finished {
811            return (0, self.is_finished);
812        }
813
814        self.rowbuf.clear();
815
816        for _ in 0..DB_BUFFER_SIZE {
817            match self.rt.block_on(self.client.next_row())? {
818                Some(row) => {
819                    let mut cells = Vec::with_capacity(row.len());
820                    for value in row {
821                        cells.push(column_value_to_cell(value)?);
822                    }
823                    self.rowbuf.push(cells);
824                }
825                None => {
826                    self.client.finish()?;
827                    self.is_finished = true;
828                    break;
829                }
830            }
831        }
832        self.current_row = 0;
833        self.current_col = 0;
834        (self.rowbuf.len(), self.is_finished)
835    }
836}
837
838#[throws(MsSQLSourceError)]
839fn column_value_to_cell(value: ColumnValues) -> TdsCell {
840    match value {
841        ColumnValues::Null => TdsCell::Null,
842        ColumnValues::TinyInt(n) => TdsCell::U8(n),
843        ColumnValues::SmallInt(n) => TdsCell::I16(n),
844        ColumnValues::Int(n) => TdsCell::I32(n),
845        ColumnValues::BigInt(n) => TdsCell::I64(n),
846        ColumnValues::Real(f) => TdsCell::F32(f),
847        ColumnValues::Float(f) => TdsCell::F64(f),
848        ColumnValues::Bit(b) => TdsCell::Bool(b),
849        ColumnValues::String(s) => TdsCell::Str(s.to_utf8_string()),
850        ColumnValues::Bytes(b) => TdsCell::Bytes(b),
851        ColumnValues::Uuid(u) => TdsCell::Uuid(Uuid::from_bytes(*u.as_bytes())),
852        ColumnValues::Decimal(parts) | ColumnValues::Numeric(parts) => {
853            TdsCell::Decimal(decimal_parts_to_decimal(&parts)?)
854        }
855        ColumnValues::Date(d) => TdsCell::NaiveDate(sql_date_to_naive_date(&d)?),
856        ColumnValues::Time(t) => TdsCell::NaiveTime(sql_time_to_naive_time(&t)),
857        ColumnValues::DateTime(dt) => TdsCell::NaiveDateTime(sql_datetime_to_naive_datetime(&dt)?),
858        ColumnValues::DateTime2(dt2) => {
859            TdsCell::NaiveDateTime(sql_datetime2_to_naive_datetime(&dt2)?)
860        }
861        ColumnValues::SmallDateTime(sdt) => {
862            TdsCell::NaiveDateTime(sql_smalldatetime_to_naive_datetime(&sdt))
863        }
864        ColumnValues::DateTimeOffset(dto) => {
865            TdsCell::DateTimeUtc(sql_datetimeoffset_to_datetime_utc(&dto)?)
866        }
867        ColumnValues::Money(m) => TdsCell::F64(sql_money_to_f64(&m)),
868        // Always widened to f64, never rounded through f32: Tiberius's own
869        // money decoder (`money::decode`) computes the value as f64 in every
870        // case (`src.read_i32_le() as f64 / 1e4` for the 4-byte form too),
871        // regardless of whether the static wire type is the fixed `Money4`
872        // or the nullable `MONEYN`. Only the destination typesystem variant
873        // (`typesystem.rs`) differs by nullability, and for the nullable
874        // case it's `Money`/f64 (see there) - so keep full f64 precision
875        // here and let `Produce<f32>` narrow it only if a genuinely
876        // non-nullable smallmoney column ever needs it.
877        ColumnValues::SmallMoney(sm) => TdsCell::F64((sm.int_val as f64) / 10_000.0),
878        other => throw!(anyhow!(
879            "MsSQL: unsupported value for src_mssql_tds backend: {:?}",
880            other
881        )),
882    }
883}
884
885/// Reassembles a `DecimalParts` value into a `rust_decimal::Decimal` using its
886/// public `magnitude()` accessor (already scaled by `10^scale`, unsigned).
887///
888/// `rust_decimal::Decimal` caps out around a 96-bit mantissa (scale 0-28);
889/// this mirrors a limitation ConnectorX's existing Tiberius path already
890/// has today (see `utils::decimal_to_i128`), not a new one introduced here.
891#[throws(MsSQLSourceError)]
892fn decimal_parts_to_decimal(parts: &DecimalParts) -> Decimal {
893    let magnitude = parts.magnitude();
894    let mantissa = i128::try_from(magnitude)
895        .map_err(|_| anyhow!("MsSQL decimal/numeric value overflows 128 bits"))?;
896    let mantissa = if parts.is_positive {
897        mantissa
898    } else {
899        -mantissa
900    };
901    Decimal::try_from_i128_with_scale(mantissa, parts.scale as u32)
902        .map_err(|e| anyhow!("MsSQL decimal/numeric value out of range: {}", e))?
903}
904
905/// `SqlDate::get_days()` is days since `0001-01-01`, matching chrono's
906/// proleptic Gregorian calendar directly (no epoch conversion needed).
907#[throws(MsSQLSourceError)]
908fn sql_date_to_naive_date(d: &SqlDate) -> NaiveDate {
909    NaiveDate::from_ymd_opt(1, 1, 1)
910        .unwrap()
911        .checked_add_signed(chrono::Duration::days(d.get_days() as i64))
912        .ok_or_else(|| {
913            anyhow!(
914                "MsSQL date out of range: {} days since 0001-01-01",
915                d.get_days()
916            )
917        })?
918}
919
920/// Despite its name, `SqlTime::time_nanoseconds` is actually the value in
921/// 100-nanosecond ticks since midnight, not raw nanoseconds - confirmed by
922/// reading `mssql-tds`'s own `scale_time_value()` decoder helper, which
923/// documents "Expands a time value at TDS scale 0..=7 into 100-nanosecond
924/// ticks." The field's doc comment ("Nanoseconds since midnight") does not
925/// match the actual unit; multiply by 100 to get true nanoseconds.
926fn sql_time_to_naive_time(t: &SqlTime) -> NaiveTime {
927    let ns = t.time_nanoseconds * 100;
928    let secs = (ns / 1_000_000_000) as u32;
929    let nanos = (ns % 1_000_000_000) as u32;
930    NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos).unwrap_or_default()
931}
932
933#[throws(MsSQLSourceError)]
934fn sql_datetime2_to_naive_datetime(dt2: &SqlDateTime2) -> NaiveDateTime {
935    let date = NaiveDate::from_ymd_opt(1, 1, 1)
936        .unwrap()
937        .checked_add_signed(chrono::Duration::days(dt2.days as i64))
938        .ok_or_else(|| {
939            anyhow!(
940                "MsSQL datetime2 out of range: {} days since 0001-01-01",
941                dt2.days
942            )
943        })?;
944    date.and_time(sql_time_to_naive_time(&dt2.time))
945}
946
947fn sql_smalldatetime_to_naive_datetime(sdt: &SqlSmallDateTime) -> NaiveDateTime {
948    // Whole minutes since midnight - no rounding needed.
949    let date = NaiveDate::from_ymd_opt(1900, 1, 1)
950        .unwrap()
951        .checked_add_signed(chrono::Duration::days(sdt.days as i64))
952        .expect("smalldatetime day count is always in range (u16 days since 1900-01-01)");
953    let time = NaiveTime::from_hms_opt((sdt.time / 60) as u32, (sdt.time % 60) as u32, 0)
954        .unwrap_or_default();
955    date.and_time(time)
956}
957
958/// The legacy `datetime` type stores 1/300-second ticks since midnight,
959/// rounded by SQL Server to the nearest tick when the value was written.
960/// Converting back to milliseconds requires the classic
961/// `round(ticks * 10 / 3)` algorithm; rounding a tick count near midnight
962/// can round up into the next day, which must be reflected in the date.
963#[throws(MsSQLSourceError)]
964fn sql_datetime_to_naive_datetime(dt: &SqlDateTime) -> NaiveDateTime {
965    let total_ms = (dt.time as i64 * 10 + 1) / 3;
966    let (extra_days, ms_in_day) = if total_ms >= 86_400_000 {
967        (1i64, total_ms - 86_400_000)
968    } else {
969        (0, total_ms)
970    };
971
972    let time = NaiveTime::from_num_seconds_from_midnight_opt(
973        (ms_in_day / 1000) as u32,
974        ((ms_in_day % 1000) * 1_000_000) as u32,
975    )
976    .ok_or_else(|| anyhow!("MsSQL datetime: invalid tick count {}", dt.time))?;
977
978    let date = NaiveDate::from_ymd_opt(1900, 1, 1)
979        .unwrap()
980        .checked_add_signed(chrono::Duration::days(dt.days as i64 + extra_days))
981        .ok_or_else(|| {
982            anyhow!(
983                "MsSQL datetime out of range: {} days since 1900-01-01",
984                dt.days
985            )
986        })?;
987
988    date.and_time(time)
989}
990
991#[throws(MsSQLSourceError)]
992fn sql_datetimeoffset_to_datetime_utc(dto: &SqlDateTimeOffset) -> DateTime<Utc> {
993    // Confirmed against a live container (not the wire-format assumption
994    // originally documented here): the datetime2 component of a
995    // DATETIMEOFFSET value is transmitted as the original *local* wall-clock
996    // time, with `offset` (minutes, e.g. 60 for +01:00) recording how far
997    // ahead of UTC it is. UTC = local - offset.
998    let naive_local = sql_datetime2_to_naive_datetime(&dto.datetime2)?;
999    let naive_utc = naive_local - chrono::Duration::minutes(dto.offset as i64);
1000    DateTime::<Utc>::from_naive_utc_and_offset(naive_utc, Utc)
1001}
1002
1003/// Reassembles the mixed-endian 64-bit integer from `SqlMoney`'s two 32-bit
1004/// halves and scales it down (money is stored as the value * 10^4).
1005fn sql_money_to_f64(m: &mssql_tds::datatypes::column_values::SqlMoney) -> f64 {
1006    let lsb_in_i64 = (m.lsb_part as i64) & 0x0000_0000_FFFF_FFFF;
1007    let money_val = lsb_in_i64 | ((m.msb_part as i64) << 32);
1008    (money_val as f64) / 10_000.0
1009}
1010
1011macro_rules! impl_produce_direct {
1012    ($($t:ty => $variant:ident),+ $(,)?) => {
1013        $(
1014            impl<'r> Produce<'r, $t> for MsSQLSourceParser {
1015                type Error = MsSQLSourceError;
1016
1017                #[throws(MsSQLSourceError)]
1018                fn produce(&'r mut self) -> $t {
1019                    let (ridx, cidx) = self.next_loc()?;
1020                    match self.take_cell(ridx, cidx) {
1021                        TdsCell::$variant(v) => v,
1022                        TdsCell::Null => throw!(anyhow!("MsSQL get None at position: ({}, {})", ridx, cidx)),
1023                        other => throw!(anyhow!("MsSQL type mismatch at ({}, {}): {:?}", ridx, cidx, other)),
1024                    }
1025                }
1026            }
1027
1028            impl<'r> Produce<'r, Option<$t>> for MsSQLSourceParser {
1029                type Error = MsSQLSourceError;
1030
1031                #[throws(MsSQLSourceError)]
1032                fn produce(&'r mut self) -> Option<$t> {
1033                    let (ridx, cidx) = self.next_loc()?;
1034                    match self.take_cell(ridx, cidx) {
1035                        TdsCell::$variant(v) => Some(v),
1036                        TdsCell::Null => None,
1037                        other => throw!(anyhow!("MsSQL type mismatch at ({}, {}): {:?}", ridx, cidx, other)),
1038                    }
1039                }
1040            }
1041        )+
1042    };
1043}
1044
1045impl_produce_direct!(
1046    u8 => U8,
1047    i16 => I16,
1048    i32 => I32,
1049    i64 => I64,
1050    bool => Bool,
1051    Uuid => Uuid,
1052    Decimal => Decimal,
1053    NaiveDateTime => NaiveDateTime,
1054    NaiveDate => NaiveDate,
1055    NaiveTime => NaiveTime,
1056    DateTime<Utc> => DateTimeUtc,
1057);
1058
1059/// `f32` needs a hand-written impl (rather than the generic macro) to accept
1060/// `TdsCell::F64` as well: a genuinely non-nullable `smallmoney` column
1061/// (fixed `Money4` wire type) maps to the `SmallMoney`/f32 destination, but
1062/// its value is decoded and stored here as a full-precision `TdsCell::F64`
1063/// (see `column_value_to_cell`), so it must be narrowed on the way out.
1064impl<'r> Produce<'r, f32> for MsSQLSourceParser {
1065    type Error = MsSQLSourceError;
1066
1067    #[throws(MsSQLSourceError)]
1068    fn produce(&'r mut self) -> f32 {
1069        let (ridx, cidx) = self.next_loc()?;
1070        match self.take_cell(ridx, cidx) {
1071            TdsCell::F32(v) => v,
1072            TdsCell::F64(v) => v as f32,
1073            TdsCell::Null => throw!(anyhow!("MsSQL get None at position: ({}, {})", ridx, cidx)),
1074            other => throw!(anyhow!(
1075                "MsSQL type mismatch at ({}, {}): {:?}",
1076                ridx,
1077                cidx,
1078                other
1079            )),
1080        }
1081    }
1082}
1083
1084impl<'r> Produce<'r, Option<f32>> for MsSQLSourceParser {
1085    type Error = MsSQLSourceError;
1086
1087    #[throws(MsSQLSourceError)]
1088    fn produce(&'r mut self) -> Option<f32> {
1089        let (ridx, cidx) = self.next_loc()?;
1090        match self.take_cell(ridx, cidx) {
1091            TdsCell::F32(v) => Some(v),
1092            TdsCell::F64(v) => Some(v as f32),
1093            TdsCell::Null => None,
1094            other => throw!(anyhow!(
1095                "MsSQL type mismatch at ({}, {}): {:?}",
1096                ridx,
1097                cidx,
1098                other
1099            )),
1100        }
1101    }
1102}
1103
1104/// `f64` needs a hand-written impl (rather than the generic macro) because
1105/// it also has to accept `TdsCell::F32`: a nullable `smallmoney` column
1106/// decodes its per-row wire value as `ColumnValues::SmallMoney` (4 bytes,
1107/// stored here as `TdsCell::F32`), yet the typesystem maps it to the same
1108/// `Money`/f64 destination as regular `money` - matching Tiberius, which
1109/// always reports its nullable `MONEYN` wire type as F64 regardless of the
1110/// per-row byte length (see `typesystem.rs`'s `TdsDataType::MoneyN` arm).
1111impl<'r> Produce<'r, f64> for MsSQLSourceParser {
1112    type Error = MsSQLSourceError;
1113
1114    #[throws(MsSQLSourceError)]
1115    fn produce(&'r mut self) -> f64 {
1116        let (ridx, cidx) = self.next_loc()?;
1117        match self.take_cell(ridx, cidx) {
1118            TdsCell::F64(v) => v,
1119            TdsCell::F32(v) => v as f64,
1120            TdsCell::Null => throw!(anyhow!("MsSQL get None at position: ({}, {})", ridx, cidx)),
1121            other => throw!(anyhow!(
1122                "MsSQL type mismatch at ({}, {}): {:?}",
1123                ridx,
1124                cidx,
1125                other
1126            )),
1127        }
1128    }
1129}
1130
1131impl<'r> Produce<'r, Option<f64>> for MsSQLSourceParser {
1132    type Error = MsSQLSourceError;
1133
1134    #[throws(MsSQLSourceError)]
1135    fn produce(&'r mut self) -> Option<f64> {
1136        let (ridx, cidx) = self.next_loc()?;
1137        match self.take_cell(ridx, cidx) {
1138            TdsCell::F64(v) => Some(v),
1139            TdsCell::F32(v) => Some(v as f64),
1140            TdsCell::Null => None,
1141            other => throw!(anyhow!(
1142                "MsSQL type mismatch at ({}, {}): {:?}",
1143                ridx,
1144                cidx,
1145                other
1146            )),
1147        }
1148    }
1149}
1150
1151impl<'r> Produce<'r, IntN> for MsSQLSourceParser {
1152    type Error = MsSQLSourceError;
1153
1154    #[throws(MsSQLSourceError)]
1155    fn produce(&'r mut self) -> IntN {
1156        let (ridx, cidx) = self.next_loc()?;
1157        match self.take_cell(ridx, cidx) {
1158            TdsCell::U8(v) => IntN(v as i64),
1159            TdsCell::I16(v) => IntN(v as i64),
1160            TdsCell::I32(v) => IntN(v as i64),
1161            TdsCell::I64(v) => IntN(v),
1162            TdsCell::Null => throw!(anyhow!("MsSQL get None at position: ({}, {})", ridx, cidx)),
1163            other => throw!(anyhow!(
1164                "MsSQL type mismatch at ({}, {}): {:?}",
1165                ridx,
1166                cidx,
1167                other
1168            )),
1169        }
1170    }
1171}
1172
1173impl<'r> Produce<'r, Option<IntN>> for MsSQLSourceParser {
1174    type Error = MsSQLSourceError;
1175
1176    #[throws(MsSQLSourceError)]
1177    fn produce(&'r mut self) -> Option<IntN> {
1178        let (ridx, cidx) = self.next_loc()?;
1179        match self.take_cell(ridx, cidx) {
1180            TdsCell::U8(v) => Some(IntN(v as i64)),
1181            TdsCell::I16(v) => Some(IntN(v as i64)),
1182            TdsCell::I32(v) => Some(IntN(v as i64)),
1183            TdsCell::I64(v) => Some(IntN(v)),
1184            TdsCell::Null => None,
1185            other => throw!(anyhow!(
1186                "MsSQL type mismatch at ({}, {}): {:?}",
1187                ridx,
1188                cidx,
1189                other
1190            )),
1191        }
1192    }
1193}
1194
1195impl<'r> Produce<'r, FloatN> for MsSQLSourceParser {
1196    type Error = MsSQLSourceError;
1197
1198    #[throws(MsSQLSourceError)]
1199    fn produce(&'r mut self) -> FloatN {
1200        let (ridx, cidx) = self.next_loc()?;
1201        match self.take_cell(ridx, cidx) {
1202            TdsCell::F32(v) => FloatN(v as f64),
1203            TdsCell::F64(v) => FloatN(v),
1204            TdsCell::Null => throw!(anyhow!("MsSQL get None at position: ({}, {})", ridx, cidx)),
1205            other => throw!(anyhow!(
1206                "MsSQL type mismatch at ({}, {}): {:?}",
1207                ridx,
1208                cidx,
1209                other
1210            )),
1211        }
1212    }
1213}
1214
1215impl<'r> Produce<'r, Option<FloatN>> for MsSQLSourceParser {
1216    type Error = MsSQLSourceError;
1217
1218    #[throws(MsSQLSourceError)]
1219    fn produce(&'r mut self) -> Option<FloatN> {
1220        let (ridx, cidx) = self.next_loc()?;
1221        match self.take_cell(ridx, cidx) {
1222            TdsCell::F32(v) => Some(FloatN(v as f64)),
1223            TdsCell::F64(v) => Some(FloatN(v)),
1224            TdsCell::Null => None,
1225            other => throw!(anyhow!(
1226                "MsSQL type mismatch at ({}, {}): {:?}",
1227                ridx,
1228                cidx,
1229                other
1230            )),
1231        }
1232    }
1233}
1234
1235impl<'r> Produce<'r, &'r str> for MsSQLSourceParser {
1236    type Error = MsSQLSourceError;
1237
1238    #[throws(MsSQLSourceError)]
1239    fn produce(&'r mut self) -> &'r str {
1240        let (ridx, cidx) = self.next_loc()?;
1241        match &self.rowbuf[ridx][cidx] {
1242            TdsCell::Str(s) => s.as_str(),
1243            TdsCell::Null => throw!(anyhow!("MsSQL get None at position: ({}, {})", ridx, cidx)),
1244            other => throw!(anyhow!(
1245                "MsSQL type mismatch at ({}, {}): {:?}",
1246                ridx,
1247                cidx,
1248                other
1249            )),
1250        }
1251    }
1252}
1253
1254impl<'r> Produce<'r, Option<&'r str>> for MsSQLSourceParser {
1255    type Error = MsSQLSourceError;
1256
1257    #[throws(MsSQLSourceError)]
1258    fn produce(&'r mut self) -> Option<&'r str> {
1259        let (ridx, cidx) = self.next_loc()?;
1260        match &self.rowbuf[ridx][cidx] {
1261            TdsCell::Str(s) => Some(s.as_str()),
1262            TdsCell::Null => None,
1263            other => throw!(anyhow!(
1264                "MsSQL type mismatch at ({}, {}): {:?}",
1265                ridx,
1266                cidx,
1267                other
1268            )),
1269        }
1270    }
1271}
1272
1273impl<'r> Produce<'r, &'r [u8]> for MsSQLSourceParser {
1274    type Error = MsSQLSourceError;
1275
1276    #[throws(MsSQLSourceError)]
1277    fn produce(&'r mut self) -> &'r [u8] {
1278        let (ridx, cidx) = self.next_loc()?;
1279        match &self.rowbuf[ridx][cidx] {
1280            TdsCell::Bytes(b) => b.as_slice(),
1281            TdsCell::Null => throw!(anyhow!("MsSQL get None at position: ({}, {})", ridx, cidx)),
1282            other => throw!(anyhow!(
1283                "MsSQL type mismatch at ({}, {}): {:?}",
1284                ridx,
1285                cidx,
1286                other
1287            )),
1288        }
1289    }
1290}
1291
1292impl<'r> Produce<'r, Option<&'r [u8]>> for MsSQLSourceParser {
1293    type Error = MsSQLSourceError;
1294
1295    #[throws(MsSQLSourceError)]
1296    fn produce(&'r mut self) -> Option<&'r [u8]> {
1297        let (ridx, cidx) = self.next_loc()?;
1298        match &self.rowbuf[ridx][cidx] {
1299            TdsCell::Bytes(b) => Some(b.as_slice()),
1300            TdsCell::Null => None,
1301            other => throw!(anyhow!(
1302                "MsSQL type mismatch at ({}, {}): {:?}",
1303                ridx,
1304                cidx,
1305                other
1306            )),
1307        }
1308    }
1309}