Skip to content

feat: modern SQL Server connectivity + TDS protocol + ergonomic APIs - #442

Open
MattJackson wants to merge 50 commits into
stack/s3from
stack/s4
Open

MattJackson wants to merge 50 commits into
stack/s3from
stack/s4

Conversation

@MattJackson

@MattJackson MattJackson commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The feature layer of the sync — modern connectivity, deeper TDS protocol coverage, and ergonomic APIs.

⚠️ Contains a breaking changerefactor!: drop async-std (dual-runtime tests retargeted to tokio + smol). Warrants a minor/major version bump per your scheme, not a patch.

  • Connectivity/TLS: TDS 8.0 strict encryption + hostname_in_certificate + client_name; MultiSubnetFailover; SSPI/NTLM on Unix via sspi-rs; client-certificate (mTLS); named-pipes example.
  • Protocol/tokens: decoders for SESSIONSTATE, COLINFO, FEDAUTHINFO, TABNAME, UDT, SQL_VARIANT (decode + encode), ALTMETADATA/ALTROW; Attention signal + query cancellation.
  • APIs: optional serde; ConfigBuilder; column_metadata() + identity accessors; Transaction Manager (begin/commit/rollback); named-procedure RPC with OUT params + table-valued parameters via a new tiberius-macros derive crate; TokenRow accessor; more IntoSql/FromSql conversions; 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.

@MattJackson

Copy link
Copy Markdown
Contributor Author

@aqrln rebased onto the merged main, all green and MERGEABLE — ready for review whenever you get a chance. Thanks!

@MattJackson

Copy link
Copy Markdown
Contributor Author

Updated + green. Fixed a compile break, added decode-time validation of server-supplied numeric scale/precision and sql_variant element lengths, and corrected the ColumnFlag bit layout to MS-TDS §2.2.7.4 — the usUpdateable polarity was inverted and the Computed/SparseColumnSet/Encrypted bit positions were offset. is_updateable() now means "not read-only" (read/write or unknown), since SQL Server reports usUpdateable = unknown for many valid bulk-target columns; this is server-verified by a new identity/computed bulk test. Also made the TVP derive bind owned String/Vec<u8> fields (the documented GUIDE example now compiles and is tested) and made the connection-string trust-cert conflict return an error instead of panicking.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant