feat: modern SQL Server connectivity + TDS protocol + ergonomic APIs - #442
MattJackson wants to merge 50 commits into
Conversation
e23544d to
805c93c
Compare
805c93c to
a8d31a7
Compare
a8d31a7 to
9d162e5
Compare
030ef97 to
e5615c2
Compare
1150911 to
711f4a2
Compare
856ae3f to
951cf91
Compare
8093c88 to
a2a41a7
Compare
a2a41a7 to
f56c4ec
Compare
|
@aqrln rebased onto the merged main, all green and MERGEABLE — ready for review whenever you get a chance. Thanks! |
f56c4ec to
d9f8903
Compare
d9f8903 to
ccee1ce
Compare
|
Updated + green. Fixed a compile break, added decode-time validation of server-supplied numeric scale/precision and |
Add examples/named-pipes.rs demonstrating how to connect to SQL Server over a Windows named pipe by wrapping the pipe stream with the tokio-util compatibility layer and handing it to Client::connect. Register the example in Cargo.toml. The named-pipe transport is Windows-only, so the example is cfg-gated and reports unsupported on other platforms. Documents #131 and #53.
Integrate upstream PR #378 (resolves #337). Add a `multi_subnet_failover` option to `Config` with a setter/getter and ADO/JDBC connection-string parsing of the `MultiSubnetFailover` keyword (via the shared `ConfigString` trait, so it works for both string formats). When enabled, the SQL Browser `connect_named` paths (tokio, async-std, smol) attempt connections to every resolved address in parallel using `FuturesUnordered` and return the first stream to succeed; otherwise they fall back to the existing sequential behaviour. The per-address connect logic is factored into a `connect_addr` helper, and the first observed error is now preserved instead of being masked by a generic "not found". Adds unit tests for keyword parsing and end-to-end `from_ado_string` propagation.
Bump the remaining dependencies and fix the resulting API breakage: - thiserror 1 -> 2 - bigdecimal 0.3 -> 0.4 - pretty-hex 0.3 -> 0.4 - async-native-tls 0.5 -> 0.6 (drop removed runtime-async-std feature) - asynchronous-codec 0.6 -> 0.7 (Encoder::Item is now a GAT) - async-io 1.8 -> 2, async-net 1.7 -> 2, futures-lite 1.12 -> 2 - libgssapi 0.8.1 -> 0.11 (OidSet::new no longer returns Result; add/Name::new take Oid by value) - winauth 0.0.4 -> 0.0.5 - dev: azure_identity 0.20 -> 1.0 (+ azure_core 1; ClientSecretCredential replaces the removed client_credentials_flow), reqwest 0.12 -> 0.13, indoc 1 -> 2, url 2.2 -> 2.5 Drop the now-stale RUSTSEC-2024-0384 (instant) and RUSTSEC-2026-0174 (http-types) advisory ignores from deny.toml since the bumps remove those crates from the graph.
Add an optional `sspi-rs` feature that enables `AuthMethod::Windows` (and `AuthMethod::windows()`) on Unix platforms, implementing the NTLM handshake with the pure-Rust `sspi` crate. This provides Windows-style authentication without requiring a Kerberos/GSSAPI setup, closing the gap for Linux and macOS clients (#407, #276, #97). - Add `sspi` (unix target) dependency and `sspi-rs` feature; include it in the `all` feature set. - Gate `WindowsAuth`, the `AuthMethod::Windows` variant and the `windows()` constructor on `all(unix, feature = "sspi-rs")` in addition to the existing Windows `winauth` path. - Implement the two-leg NTLM negotiate/authenticate exchange in `Connection::login` for the Unix `sspi-rs` path, reusing the existing SSPI token flushing and `integrated_security` login plumbing. - Extend the connection-string parser so `IntegratedSecurity=SSPI` selects NTLM when a username/password is supplied and falls back to Kerberos (`Integrated`) only when `integrated-auth-gssapi` is enabled and no credentials are given. - Add an `Error::SspiRs` variant and `From<sspi::Error>` conversion. The existing Windows `winauth` and Unix `integrated-auth-gssapi` paths are left intact; the gssapi connection-string arm is only disabled when `sspi-rs` is active on Unix to avoid overlapping match arms.
…eters Implement calling a stored procedure by name, resolving the `todo!()` that previously blocked named-procedure RPC requests. - Encode `RpcProcIdValue::Name` as a US_VARCHAR proc name in the RPC request, alongside the existing by-id path used by execute/query. - Introduce `RpcValue` (Scalar/Table) so RPC params can carry either a scalar `ColumnData` or a table-valued parameter, and thread it through the existing execute/query call sites. - Add `TypeInfoTvp` to encode TVP type info and rows (MS-TDS 2.2.5.5.5), rewriting fixed-length column types to their nullable var-len variants. - Add a `Command` public API (`bind_param`, `bind_out_param`, `bind_table`, `bind_table_with_dbtype`, `exec`) plus `CommandStream`, `CommandItem`, `CommandResult` and `CommandReturnValue` for reading OUT parameter values, return codes and result sets. - Add the `tvp-macro` crate providing `#[derive(TableValueRow)]`. - Encode nullable scalar values correctly when no TypeInfo is supplied. Unit tests cover named-proc request encoding, TVP type-info encoding and the derive macro output. Integration tests requiring a live SQL Server are added under tests/command.rs. Resolves #275.
Add support for presenting a client certificate during the TLS handshake to authenticate the client to the server (mutual TLS), required for TDS 8.0 strict connections using ENCRYPT_CLIENT_CERT and usable with the classic TLS handshake when the server requests a client certificate. - Config/ConfigBuilder gain `client_certificate(cert, key)` for PEM/DER certificate + private-key files, and `client_certificate_pkcs12(path, password)` for a PKCS#12/PFX bundle (native-tls and vendored-openssl). The PKCS#12 password is held in a zeroizing buffer and redacted in Debug output. All new public API is feature-gated with docsrs doc(cfg). - rustls: wired via `with_client_auth_cert` on the ConfigBuilder path; loads PEM (chain) or DER certificates and PEM/DER private keys. PKCS#12 is rejected with a clear error. - native-tls: wired via `Identity` (PEM PKCS#8 files or PKCS#12 bundle). - vendored-openssl (opentls): wired via `Identity::from_pkcs12`; separate PEM/DER files are unsupported by the backend and rejected with a clear error documenting the limitation. - Unit tests for the config plumbing (cert/key and PKCS#12 sources, builder wiring, password redaction).
Config::builder() returns a ConfigBuilder, but the type was not nameable from outside the crate. Export it so downstream code can hold and pass a builder value.
(cherry picked from commit 1f7bddf1c157b24b14ee95546847d080694ee672)
SQL Server sets reserved/ODBC bits in the 16-bit COLMETADATA Flags field (MS-TDS 2.2.7.4); strict BitFlags::from_bits rejected the whole token with 'invalid flags' (broke tests/custom-cert). Truncate to the modelled flags. Also: declare MSRV 1.88 (rust-version), add an MSRV CI job and an advisory semver-checks job on PRs, and canonicalize tvp-macro's license expression.
BREAKING: removes async-std as a supported runtime. async-std is discontinued (RUSTSEC-2025-0052); this removes the sql-browser-async-std feature, its SQL Browser impl, the async-std example/named-instance test, and the async-std dev-dependency, eliminating async-std from the dependency graph entirely (clears the RUSTSEC-2025-0052 ignore in deny.toml). - Modernize the runtimes-macro test helper to syn 2 (drop syn 1 + darling) and retarget #[test_on_runtimes] to generate tokio + smol variants (was tokio + async_std). All 111 call sites are unchanged; add smol as a dev-dependency. - Rewrite the async-std crate-doc and SQL Browser doctests to tokio. - Update README/CONTRIBUTING/CI feature lists to tokio + smol. Downstreams on sql-browser-async-std should switch to sql-browser-tokio or sql-browser-smol.
The elaborate workflow carried a second macOS job (cargo-test-macos, macos-26-intel) that ran on every PR yet duplicated the qa-gated integration-macos build+unit lane — and, lacking the krb5/openssl dependency install its feature set needs, red-walled PRs. Remove it; the qa-gated integration-macos already provides macOS build+unit coverage. (A follow-up slice restores a real macOS integration lane via colima.)
Match the smoke/linux lanes: gate on an authenticated SELECT 1 (via a throwaway mssql-tools container) instead of a bare open-port check, so the experimental SQL 2025 lane doesn't race server startup either.
new_with_scale asserted scale < 38, but SQL Server permits scale == 38 (e.g. decimal(38,38)), which the decode_numeric_scale_at_limit test exercises. 10^38 still fits in i128, so allow scale <= 38.
cargo-semver-checks defaulted to all features, enabling the mutually exclusive TLS backends (native-tls + rustls + vendored-openssl) at once; their duplicate TlsStream definitions made rustdoc fail to build (E0428), so the advisory check errored instead of comparing APIs. Pin it to the crate's original, non-conflicting features (present in every baseline).
The feature sync introduces breaking changes (notably dropping the async-std runtime), so this is an effective-major bump for a 0.x crate (0.12.x -> 0.13.0) rather than a patch.
…it-features actions/checkout leaves the PR base branch without a local ref, so `--baseline-rev origin/<base>` couldn't resolve. Fetch the base branch and compare against FETCH_HEAD. Also cargo-semver-checks has no `--no-default-features` flag (that was an unknown-arg error, the <1s fail); use `--only-explicit-features` to check just the coherent, non-conflicting set (verified locally: 0.12.3 -> 0.13.0 major change, no semver update required).
- opentls: drop the unconditional ALPN warning (fired on every encrypted connection); keep the Strict-gated one; remove the duplicate TDS80 ALPN constant and the always-false supports_alpn() helper + its tests - config.rs: fix the HostNameInCertificate default in the ADO doc table - named-pipes example: doc said "reports unsupported" but it panics - command.rs: use `for x in self` instead of explicit into_iter() - pre_login: gate test-only ActivityId::new with cfg(test) - trim mutation-test/duplicate comments in lib.rs, row.rs, connection.rs, token.rs, tests/command.rs
- Merge the two duplicate #[cfg(test)] mod tests modules into one (E0428) - Re-add the crate::Error import dropped by f95bcf9 (E0433 for Error::BulkInput) - Add table_name: None to BaseMetaDataColumn test literals (E0063) in token_col_metadata.rs and token_alt_row.rs
…l_variant lengths STEP 2a: type_info decode of Decimaln/Numericn read scale/precision straight off the wire; a peer sending scale 39..=255 (or invalid precision) would then hit assert!(scale <= 38) in column_data/numeric and panic the connection task (remote DoS). Reject precision outside 1..=38 and scale > precision with Error::Protocol before the assert. Tests: scale 39/255 and precision 0/39/255 now Err; scale=precision=38 boundary still decodes. STEP 2b: sql_variant decode arms hardcoded the bytes they consume and ignored the declared propBytes/data_len, so a wrong/inflated declared length silently desynced every subsequent column. Cross-check declared length against consumed bytes for the Guid/Daten (data_len) and Decimaln/Numericn/char/nchar/binary and temporal (propBytes) arms, returning Error::Protocol on mismatch (mirroring the Timen/Datetime2/DatetimeOffsetn model). Added malformed-length tests.
…flag polarity STEP 3a: encode_b_varchar wrote a single-byte length prefix via `units.len() as u8` while still writing every UTF-16 unit, so a transaction/savepoint name longer than 255 code units truncated the count and corrupted the stream. Return Error::Protocol for > 255 units. Tests: 256-unit name errors, 255-unit boundary still encodes. STEP 3b: ColumnFlag::Updateable/UpdateableUnknown polarity was reversed. Per MS-TDS §2.2.7.4 the 2-bit usUpdateable sub-field (bits 2-3, LSB order) is 0=read-only, 1=read/write (low bit 0x04), 2=unknown (high bit 0x08). The enum had Updateable=1<<3 and UpdateableUnknown=1<<2, so is_updateable() keyed off the 'unknown' bit. Swapped to Updateable=1<<2, UpdateableUnknown=1<<3 and added a bit-exact layout test plus an is_updateable() behavior test.
…t (STEP 4) - var_len: replace the unmatched-VarLenType unimplemented!() fallback (a panic on server-controlled input) with Error::Protocol. Test via VarLenType::Udt. - rpc_request: RpcProcIdValue::Name wrote a u16 length counter with unchecked += 1 over UTF-16 units; a name > 65535 units overflowed it. Reject with a length check + test. - token_fed_auth_info / token_session_state: vec![0u8; len] was sized from an unbounded server u32 (large-alloc DoS). Add a 16 MiB cap (mirroring the MAX_NVARCHAR_SIZE precedent) checked before allocation + tests.
The COLMETADATA Flags USHORT (MS-TDS 2.2.7.4) is a fixed 16-bit, LSB-first field. Computed/FixedLenClrType/SparseColumnSet/Encrypted were offset by two bits, so a computed column's fComputed bit (spec 0x0020) was read at 0x0080, and encryption/sparse/CLR bits were likewise wrong. This meant a computed column would not be filtered from a bulk insert. Correct the four wrong assignments to the spec bit positions: Computed 1<<7 -> 1<<5 (0x0020) FixedLenClrType 1<<10 -> 1<<8 (0x0100) SparseColumnSet 1<<11 -> 1<<10 (0x0400) Encrypted 1<<12 -> 1<<11 (0x0800) Low bits 0-4 (Nullable, CaseSensitive, usUpdateable, Identity) and high bits 13-15 (Hidden, Key, NullableUnknown) already matched the spec and are unchanged. Bits 9 (FRESERVEDBIT) and 12 (usReserved3) are reserved. Add byte-exact unit tests decoding constructed Flags values that lock the layout, and a server-gated bulk_insert test asserting identity and computed columns are skipped against a real server. (cherry picked from commit d8d7d63fd1617f92fad686d24e96e804d2062eb2)
…icting trust-cert TVP derive: bind_fields received &self but generated add_field(self.<f>), moving fields out of &self and failing to compile (E0507) for non-Copy columns like String/Vec<u8>. Borrowing does not work either: the &self borrow is not guaranteed to outlive SqlTableDataRow<'a>. Generate add_field(self.<f>.clone()) instead, which yields an owned (or copied reference) value satisfying IntoSql<'a> for every documented field type. Update macro rustdoc, GUIDE.md example (now uses an owned String), and add a server-free integration test that derives TableValueRow on a struct with String/Vec<u8>/scalar fields plus a borrowed &str struct, and asserts the bound column data. config: Config::from_config_string called Config::trust_cert()/trust_cert_ca() which panic on conflicting order, so a connection string setting both TrustServerCertificate=true and TrustServerCertificateCA=<path> panicked the process. Detect the conflict in the parse path and return Error::Conversion. Add tests for the conflict (ado + jdbc) and each keyword alone. (cherry picked from commit dc079dab1677d74fd74e507e02c75f8409814831)
SQL Server reports usUpdateable as unknown (2) for many bulk-target columns rather than an explicit read/write (1). After correcting the ColumnFlag bit polarity to match MS-TDS 2.2.7.4, filtering the bulk column list on the read/write bit alone dropped every such column, breaking bulk insert entirely. is_updateable() now treats read/write and unknown alike (only explicit read-only is excluded), and bulk_insert_columns filters through it.
s3 narrowed PacketHeader::sspi to #[cfg(all(windows, feature = "winauth"))]; this test (added later in s4) calls it, so it needs the same gate to compile on non-winauth builds.
6fffbc0 to
4f9e546
Compare
The feature layer of the sync — modern connectivity, deeper TDS protocol coverage, and ergonomic APIs.
refactor!: drop async-std(dual-runtime tests retargeted to tokio + smol). Warrants a minor/major version bump per your scheme, not a patch.hostname_in_certificate+client_name; MultiSubnetFailover; SSPI/NTLM on Unix via sspi-rs; client-certificate (mTLS); named-pipes example.ConfigBuilder;column_metadata()+ identity accessors; Transaction Manager (begin/commit/rollback); named-procedure RPC with OUT params + table-valued parameters via a newtiberius-macrosderive crate;TokenRowaccessor; moreIntoSql/FromSqlconversions; DateTime2→datetime coercion under tds73.Granular per-feature commit history preserved.
Supersedes #132, #298, #314, #328, #331, #357, #366, #378, #398, #408, #413, #416.
Sequential series — merge after #432, #433, #434. Based on
main; diff reduces to its own 36 commits as the earlier PRs land.Reviewer note: please rebase-merge or merge-commit, not squash.