Skip to content

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

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

feat: modern SQL Server connectivity + TDS protocol + ergonomic APIs#442
MattJackson wants to merge 41 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.

…#412, #340, #414, #224)

Adapted from upstream #413 (author @olback) onto the #419 rustls-0.23 stack:

- EncryptionLevel::Strict (TDS 8.0): TLS handshake before prelogin, ALPN
  'tds/8.0' advertised on all three TLS backends. New tds80 feature (in
  default), with compile_error if enabled without a TLS backend.
- Config::hostname_in_certificate(): validate the server cert against a
  specified name instead of the host (#340).
- Config::client_name() + default login hostname (workstation id) from the
  local machine name (#414).
- Connection-string parsing for HostNameInCertificate / WorkstationID and
  encrypt=strict.
- Deps: async-native-tls 0.4->0.5 (request_alpns), libc (unix hostname).

142 lib tests pass; clippy --features=all -D warnings clean.
Fixes the docs.rs build (doc_status:false on 0.13.0-alpha.1). The crate
gated #![feature(doc_cfg)] behind a 'docs' cargo feature, which failed to
build. Switch all #[doc(cfg(...))] annotations to the standard
#[cfg_attr(docsrs, ...)] pattern, set docs.rs to build with
rustdoc-args=[--cfg docsrs], drop the unused 'docs' feature, and declare
docsrs via [lints.rust] check-cfg so clippy -D warnings stays clean.
Normal builds no longer require nightly.
Adapted from upstream #416 (author @MukundaKatta): new optional `serde`
feature deriving Serialize/Deserialize for Row/Column/ColumnData/Numeric/
ColumnType and the time/xml types, with a round-trip test.
Adapted from upstream #366 (author @LonerDan), non-breaking (existing
Config API retained).
Audited against MS-TDS: per-version support (7.1/7.3 full; 7.2/7.4/8.0
partial), feature matrix (messages/tokens/types/encryption/auth), and a
prioritized gap list (SQL_VARIANT, named-proc RPC, COLINFO, FEDAUTHINFO...).
, #217, #403)

Adapted from upstream #398 (author @etylermoss), reconciled with the
already-integrated bulk_insert_columns (#359):
- Client::column_metadata(table, columns) returns owned Vec<MetaDataColumn>
  (names, types, size/precision/scale, nullability, identity); bulk insert
  now shares this path.
- MetaDataColumn::col_name()/base() accessors; BaseMetaDataColumn::
  is_identity()/is_nullable()/ty()/flags() (resolves #403).
Add support for the SESSIONSTATE token (MS-TDS 2.2.7.22), used by the
connection-resiliency / session-recovery feature. The server sends this
token to communicate session state that a client can replay to
transparently re-establish a broken connection.

- Add TokenType::SessionState (0xE4)
- Add TokenSessionState decoder (SeqNo, Status/fRecoverable, and a
  sequence of SessionStateData entries with the 0xFF long-length escape)
- Dispatch and retain the token in the token stream (ReceivedToken::SessionState)
- Add unit tests for the pure parse helper (short and long state lengths)
Add a TokenColInfo decoder (MS-TDS §2.2.7.4) and wire it into the token
stream dispatch. Previously the COLINFO token type existed in
token_type.rs but had no handler, so browse-mode responses that include
COLINFO caused the stream to panic on an unimplemented token.

The decoder parses each per-column entry (ColNum, TableNum, Status) plus
the optional B_VARCHAR base-table column name emitted when the
fDifferentName status bit is set, and exposes status accessors
(expression/key/hidden/different-name). A new ReceivedToken::ColInfo
variant carries the token; it is treated as informational and otherwise
ignored, consistent with ORDER and INFO tokens. With every TokenType
variant now handled, the unreachable catch-all dispatch arm is removed
so the match is exhaustive.

Includes a decode unit test covering entries with and without a
differing column name.
Add the FEDAUTHINFO token (MS-TDS 2.2.7.12), sent by the server during a
library-driven federated authentication (Azure AD / ADAL / MSAL) flow to
tell the client how to acquire an access token.

- Add TokenType::FedAuthInfo (0xEE).
- Add TokenFedAuthInfo decoder parsing CountOfInfoIDs, the FedAuthInfoOpt
  option table, and the offset-addressed Unicode data region, exposing the
  STSURL and SPN information elements.
- Dispatch the token in the token stream via ReceivedToken::FedAuthInfo,
  replacing the previous unimplemented panic.
- Add unit tests for the pure parse helper (happy path, unknown info id,
  out-of-bounds offset).
The PRELOGIN encoder previously decoded INSTOPT/TRACEID/NONCEOPT but never
emitted the instance-name or trace-id options it decoded. This adds:

- INSTOPT emission: a null-terminated instance name (empty by default),
  letting the server validate the instance the connection landed on.
- TRACEID emission: the client activity GUID + sequence, emitted only
  when an ActivityId is supplied, for server-side trace correlation.
- PreloginMessage::validate_instance() to check the server's INSTOPT
  reply and surface a protocol error on rejection.
- ActivityId::new() constructor for building a trace id.
- Instance name plumbed from the connection config into the prelogin
  handshake, with the server response validated afterwards.

Unit tests assert the encoded packet includes INSTOPT (empty and named),
that TRACEID is present only when set and 20 bytes wide, and cover the
instance-validation success/failure paths.
Implement the client-to-server Attention Signal packet (packet type
0x06, MS-TDS 2.2.1.6) and a query-cancellation API surface.

- Add `PacketHeader::attention` producing an empty end-of-message
  attention packet.
- Add `Connection::cancel_request`, which sends the attention packet and
  drains the token stream until the acknowledging DONE token with the
  DONE_ATTN status bit is received.
- Add `TokenStream::flush_done_attention` to discard the tokens of the
  cancelled request up to that acknowledgement, and `TokenDone::is_attention`.
- Expose `Client::cancel_query`, documenting how it integrates with
  futures cancellation / timeouts.
- Add unit tests for the attention packet header and full-packet encoding.
The opentls (vendored OpenSSL) TLS backend cannot advertise the
"tds/8.0" ALPN protocol required for TDS 8.0 strict-encryption
negotiation ([MS-TDS] 2.2.6.5 PRELOGIN / TDS 8.0 addendum): opentls
0.2.x exposes no API to set the ALPN protocol list on its TlsConnector
(no equivalent of openssl's SslConnectorBuilder::set_alpn_protos, and
the wrapped SslConnector is private).

Rather than silently degrading, the backend now:
- exposes a documented TDS80_ALPN_PROTOCOL constant and a supports_alpn()
  helper (const fn returning false) explaining the limitation in rustdoc;
- emits a clear WARN at TLS handshake time advising use of the native-tls
  or rustls backend when TDS 8.0 ALPN negotiation is required.

Adds unit tests pinning the ALPN identifier and guarding the
unsupported-backend invariant.
Parse the UDT_INFO metadata (MS-TDS §2.2.5.5.4) for the 0xF0 UDT type
in TypeInfo::decode: max byte size, database/schema/type names, and the
assembly-qualified CLR type name. A new UdtInfo struct carries this
metadata and TypeInfo gains a Udt variant.

The column value path surfaces the raw UDT bytes (always transferred in
the partially length-prefixed / PLP format) verbatim as
ColumnData::Binary, without attempting CLR deserialization. ColumnType,
the metadata Display impl, and the null-value mapping are all updated
for the new variant.

Adds unit tests for the metadata encode/decode round trip and for
decoding both a populated and a NULL UDT value.
Add support for TDS Transaction Manager requests (MS-TDS 2.2.6.8),
letting the driver begin, commit, roll back and create savepoints in a
transaction directly through the protocol instead of issuing equivalent
T-SQL batches.

- Add `PacketHeader::transaction_manager` constructor for packet type
  0x14 (TransactionManagerReq).
- Add `TransactionManagerRequest` message with `Encode` covering the
  TM_BEGIN_XACT (5), TM_COMMIT_XACT (7), TM_ROLLBACK_XACT (8) and
  TM_SAVE_XACT (9) request kinds, including the ALL_HEADERS transaction
  descriptor block and B_VARCHAR name encoding.
- Add public `IsolationLevel` and `TransactionManagerRequestType` enums.
- Add `Client::begin_transaction`,
  `Client::begin_transaction_with_isolation`,
  `Client::commit_transaction`, `Client::rollback_transaction` and
  `Client::save_transaction`. The server's BeginTransaction/
  CommitTransaction/RollbackTransaction ENVCHANGE tokens are already
  applied to the connection context, so the active transaction
  descriptor is tracked automatically.
- Re-export `IsolationLevel` from the crate root.
- Add unit tests for the request encoding.
Reading a `sql_variant` column previously hit a `todo!()` in
`token_col_metadata` and panicked. This adds a `VarLenType::SSVariant`
TypeInfo parse path and a decoder for the SQL_VARIANT value, per
MS-TDS 2.2.5.4.4 (metadata) and 2.2.5.5.3 (value).

The value carries a base-type byte, a property-bytes count, the
type-specific property metadata (collation, precision/scale, scale)
and the raw value. The decoder maps the stored intrinsic onto the
matching `ColumnData` variant (e.g. `int` -> `I32`, `nvarchar` ->
`String`). Supported base types: bit, tinyint/smallint/int/bigint,
real/float, money/smallmoney, datetime/smalldatetime, uniqueidentifier,
numeric/decimal, char/varchar/nchar/nvarchar, binary/varbinary, and
(under `tds73`) date/time/datetime2/datetimeoffset. A NULL sql_variant
(zero total length) is surfaced as a generic null.

Adds unit tests decoding representative payloads (int, bigint, bit,
nvarchar, varchar, binary, guid, numeric, date, null) built from
hand-computed spec bytes.
Add support for the ALTMETADATA (0x88) and ALTROW (0xD3) token streams
that SQL Server emits for COMPUTE / COMPUTE BY result sets, per MS-TDS
sections 2.2.7.1 and 2.2.7.2. Previously any query containing a COMPUTE
clause failed with "invalid token type 88" because these tokens were
unknown to the parser.

- Add AltMetaData and AltRow variants to TokenType.
- Add TokenAltMetaData / AltMetaDataColumn with a decoder that reads the
  aggregate operator, operand, per-column metadata (reusing
  BaseMetaDataColumn) and the computed column name.
- Add TokenAltRow with a decoder that parses the computed values using
  the ALTMETADATA identified by the row's compute id.
- Cache ALTMETADATA in the connection Context keyed by compute id so a
  following ALTROW can be resolved.
- Wire both tokens into the token stream (NewAltResultset / AltRow
  received tokens). The user-facing QueryStream ignores them, so normal
  result sets are unaffected and COMPUTE BY queries no longer error.
- Add decode unit tests for both tokens.

Remaining work: surface ALTROW values to consumers as a dedicated
result-set variant (currently decoded and skipped).
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).
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