diff --git a/.github/workflows/pr-code-security.yml b/.github/workflows/pr-code-security.yml
index fde10666c..3448776aa 100644
--- a/.github/workflows/pr-code-security.yml
+++ b/.github/workflows/pr-code-security.yml
@@ -4,13 +4,34 @@ on:
pull_request:
branches: [main]
+permissions:
+ contents: read
+
jobs:
secret-detection:
name: Secret Detection
- if: github.event_name == 'pull_request'
- uses: prisma/.github/.github/workflows/secret_detection.yml@main
- secrets: inherit
- code-scanning:
- name: Code Scanning
- if: github.event_name == 'pull_request'
- uses: prisma/.github/.github/workflows/code_scanning.yml@main
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ # Full history so gitleaks can scan every commit in the PR range.
+ fetch-depth: 0
+ persist-credentials: false
+ # We run the gitleaks CLI directly rather than gitleaks-action: the
+ # action wrapper requires a paid license for repos under a GitHub
+ # organization, while the gitleaks binary itself is MIT-licensed and
+ # free. Pinned by version and verified by SHA-256 before use.
+ - name: Install gitleaks
+ env:
+ GITLEAKS_VERSION: 8.30.1
+ GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
+ run: |
+ set -euo pipefail
+ url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
+ curl -sSfL "$url" -o gitleaks.tar.gz
+ echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum -c -
+ tar -xzf gitleaks.tar.gz gitleaks
+ sudo install gitleaks /usr/local/bin/gitleaks
+ gitleaks version
+ - name: Scan git history for secrets
+ run: gitleaks git --no-banner --redact --exit-code 1 --config .gitleaks.toml .
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
new file mode 100644
index 000000000..71985011d
--- /dev/null
+++ b/.github/workflows/security.yml
@@ -0,0 +1,29 @@
+name: Security audit
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ schedule:
+ # Re-run weekly so newly-published advisories are caught even without a push.
+ - cron: "0 6 * * 1"
+
+permissions:
+ contents: read
+
+jobs:
+ cargo-deny:
+ name: cargo-deny (advisories, bans, sources, licenses)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
+ with:
+ toolchain: stable
+ # Run cargo-deny on the runner directly (the musl container action conflicts with the repo's rust-toolchain file).
+ - name: Install cargo-deny
+ uses: taiki-e/install-action@cargo-deny
+ - name: Check advisories, bans, sources, licenses
+ run: cargo deny check advisories bans sources licenses
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fec4c17a0..4de9c1fdc 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,39 +1,132 @@
name: Cargo tests
on:
+ # qa + manual dispatch run the integration tier
push:
branches:
- main
+ - qa
pull_request:
+ workflow_dispatch:
jobs:
clippy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
- - uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- components: clippy
- override: true
- - name: Install dependencies
- run: sudo apt install -y openssl libkrb5-dev
- - uses: actions-rs/clippy-check@v1
+ persist-credentials: false
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- args: --features=all
+ toolchain: stable
+ components: clippy
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+ - name: Install dependencies
+ run: sudo apt-get update && sudo apt-get install -y openssl libkrb5-dev
+ - name: Clippy
+ # Advisory here: modernizes the retired actions-rs/clippy-check and reports
+ # lints without gating. The strict `-D warnings` gate lands together with its
+ # baseline-lint fixes in the feature stack, so main is never red in between.
+ run: cargo clippy --features=all
format:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions-rs/toolchain@v1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- components: rustfmt
- override: true
- - uses: mbrobbel/rustfmt-check@master
+ persist-credentials: false
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
- token: ${{ secrets.GITHUB_TOKEN }}
+ toolchain: stable
+ components: rustfmt
+ - name: Rustfmt
+ run: cargo fmt --check
- cargo-test-linux:
+ msrv:
+ name: MSRV (1.88)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ # Keep this in sync with `rust-version` in Cargo.toml.
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
+ with:
+ toolchain: "1.88"
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+ - name: Install dependencies
+ run: sudo apt-get update && sudo apt-get install -y openssl libkrb5-dev
+ - name: Check on MSRV
+ run: cargo check --features all
+
+ # The `rustls-webpki-roots` feature (and the rustls trust-store code paths in
+ # general) are excluded from `--features=all` because the three TLS backends
+ # are mutually exclusive, so neither the clippy nor the MSRV job above builds
+ # them. This server-less lane keeps them clippy-clean, buildable, and unit-
+ # tested so a regression (e.g. a `webpki-roots` major bump changing
+ # `TLS_SERVER_ROOTS`) is caught in CI.
+ rustls-features:
+ name: rustls trust-store (unit)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
+ with:
+ toolchain: stable
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+ - name: Install dependencies
+ run: sudo apt-get update && sudo apt-get install -y openssl libkrb5-dev
+ # Build + unit-test both rustls feature sets. `--lib` needs no live server.
+ # (Strict `-D warnings` clippy is deferred repo-wide until the baseline
+ # lints land, matching the advisory `clippy` job above.)
+ - name: Unit tests (rustls)
+ run: cargo test --no-default-features --features rustls,chrono,time,tds73 --lib
+ - name: Unit tests (rustls-webpki-roots)
+ run: cargo test --no-default-features --features rustls-webpki-roots,chrono,time,tds73 --lib
+
+ semver:
+ name: semver-checks (advisory)
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ # Advisory during 0.x: reports API-breaking changes vs the target branch
+ # without blocking (breaking changes are allowed pre-1.0, but should be
+ # visible in review).
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ fetch-depth: 0
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
+ with:
+ toolchain: stable
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+ - name: Install dependencies
+ run: sudo apt-get update && sudo apt-get install -y openssl libkrb5-dev
+ - name: Install cargo-semver-checks
+ uses: taiki-e/install-action@cargo-semver-checks
+ - name: Fetch baseline branch
+ # `actions/checkout` leaves the PR base branch without a local ref, so
+ # `--baseline-rev origin/` can't be resolved; fetch it explicitly
+ # and compare against FETCH_HEAD.
+ run: git fetch --no-tags --depth=100 origin "${{ github.base_ref }}"
+ - name: Check for semver-breaking changes
+ # Scope to one coherent, non-conflicting feature set. Checking all
+ # features at once enables the mutually-exclusive TLS backends
+ # (native-tls + rustls + vendored-openssl) together, whose duplicate
+ # `TlsStream` definitions make rustdoc fail to build (E0428). These are
+ # also the crate's original features, so they exist in every baseline
+ # slice this PR is compared against.
+ run: >-
+ cargo semver-checks --baseline-rev FETCH_HEAD
+ --only-explicit-features
+ --features rustls,chrono,time,tds73,rust_decimal,bigdecimal
+
+ smoke:
+ name: integration smoke (SQL 2022)
+ # Fast real-server signal on the dev lane; the full matrix runs on qa.
+ if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/dev')
runs-on: ubuntu-latest
strategy:
@@ -51,34 +144,143 @@ jobs:
- "--no-default-features --features=time"
- "--no-default-features --features=rustls"
- "--no-default-features --features=vendored-openssl"
+ - "--no-default-features --features=rustls,chrono,time,tds73,rust_decimal,bigdecimal"
env:
TIBERIUS_TEST_CONNECTION_STRING: "server=tcp:localhost,1433;user=SA;password=;TrustServerCertificate=true"
RUSTFLAGS: "-Dwarnings"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
- - uses: actions-rs/toolchain@v1
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
+ with:
+ toolchain: stable
+
+ - name: Compute cache key
+ shell: bash
+ run: |
+ key="${{ matrix.features }}"
+ key="${key//,/+}"
+ echo "RUST_CACHE_KEY=$key" >> "$GITHUB_ENV"
- - uses: actions/cache@v2
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- target
- key: ${{ runner.os }}-cargo-${{ matrix.features }}
+ shared-key: ${{ env.RUST_CACHE_KEY }}
- name: Start SQL Server ${{matrix.database}}
- run: DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml up -d mssql-${{matrix.database}}
+ run: DOCKER_BUILDKIT=1 docker compose -f docker-compose.yml up -d mssql-${{matrix.database}}
- name: Install dependencies
- run: sudo apt install -y openssl libkrb5-dev
+ run: sudo apt-get update && sudo apt-get install -y openssl libkrb5-dev
+
+ - name: Wait for SQL Server
+ # A listening port is not readiness: SQL Server binds 1433 before the SA
+ # login and databases finish initializing, so tests started too early race
+ # it and hit sporadic connection/login failures. Gate on an authenticated
+ # `SELECT 1` from a throwaway mssql-tools container (works uniformly across
+ # the full server images and azure-sql-edge, which ships no in-box sqlcmd).
+ run: |
+ pw=''
+ for _ in $(seq 1 60); do
+ if docker run --rm --network host mcr.microsoft.com/mssql-tools \
+ /opt/mssql-tools/bin/sqlcmd -S localhost,1433 -U SA -P "$pw" -Q "SELECT 1" >/dev/null 2>&1; then
+ echo "SQL Server ready (authenticated login succeeded)"; exit 0
+ fi
+ sleep 3
+ done
+ echo "SQL Server did not accept an authenticated login in time" >&2
+ docker compose -f docker-compose.yml logs mssql-${{matrix.database}} || true
+ exit 1
- name: Run tests
run: cargo test ${{matrix.features}}
- cargo-test-windows:
+ integration-macos:
+ name: macos (SQL 2022 via colima)
+ if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/qa')
+ # Intel runner: hosted macOS has no Linux Docker daemon, so we host the
+ # linux/amd64 SQL Server container inside a colima (Lima) Linux VM. An x86_64
+ # runner runs that image natively — no qemu emulation — which keeps the VM
+ # fast enough for the full integration suite instead of build + unit only.
+ runs-on: macos-15-intel
+ continue-on-error: ${{ matrix.soft_fail }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Exercise every TLS backend:
+ include:
+ # rustls (TLS 1.3)
+ - features: "--no-default-features --features=rustls,chrono,time,tds73,rust_decimal,bigdecimal"
+ soft_fail: false
+ # vendored-openssl (opentls, statically-linked OpenSSL) — cross-platform.
+ - features: "--no-default-features --features=vendored-openssl,chrono,time,tds73,rust_decimal,bigdecimal"
+ soft_fail: false
+ # NOTE: a `--features=all` lane is intentionally omitted here. On macOS
+ # `native-tls` resolves to Apple Secure Transport, which cannot complete
+ # the SQL Server TLS handshake (it works on Linux=OpenSSL and
+ # Windows=SChannel, both covered elsewhere). Running it here could only
+ # ever fail, so the two TLS backends that do work on macOS (rustls and
+ # vendored-openssl, above) provide the macOS integration coverage.
+ env:
+ TIBERIUS_TEST_CONNECTION_STRING: "server=tcp:localhost,1433;user=SA;password=;TrustServerCertificate=true"
+ RUSTFLAGS: "-Dwarnings"
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
+ with:
+ toolchain: stable
+ - name: Compute cache key
+ run: |
+ key="${{ matrix.features }}"
+ key="${key//,/+}"
+ echo "RUST_CACHE_KEY=$key" >> "$GITHUB_ENV"
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+ with:
+ shared-key: ${{ env.RUST_CACHE_KEY }}
+ - name: Install build dependencies (openssl, krb5)
+ run: |
+ brew install openssl krb5
+ # krb5 is keg-only; expose its pkg-config so integrated-auth-gssapi
+ # and any openssl-linking features build against Homebrew's copy.
+ echo "PKG_CONFIG_PATH=$(brew --prefix krb5)/lib/pkgconfig:$(brew --prefix openssl)/lib/pkgconfig" >> "$GITHUB_ENV"
+ - name: Start Docker (colima)
+ run: |
+ brew install colima docker docker-compose
+ # SQL Server wants ~2 GB RAM; give the VM headroom for it + the build.
+ colima start --cpu 3 --memory 6 --disk 20
+ docker version
+ - name: Start SQL Server 2022
+ run: docker compose -f docker-compose.yml up -d mssql-2022
+ - name: Wait for SQL Server
+ # A listening port is not readiness: SQL Server binds 1433 before the SA
+ # login and databases finish initializing, so the connection-heavy bulk
+ # tests can race it and hit "Login failed for user 'SA'" (18456). Gate on
+ # an actual authenticated `SELECT 1` succeeding instead.
+ run: |
+ pw=''
+ for _ in $(seq 1 60); do
+ for bin in /opt/mssql-tools18/bin/sqlcmd /opt/mssql-tools/bin/sqlcmd; do
+ if docker compose -f docker-compose.yml exec -T mssql-2022 \
+ "$bin" -S localhost -U SA -P "$pw" -C -Q "SELECT 1" >/dev/null 2>&1; then
+ echo "SQL Server ready (authenticated login succeeded)"; exit 0
+ fi
+ done
+ sleep 3
+ done
+ echo "SQL Server did not accept an authenticated login in time" >&2
+ docker compose -f docker-compose.yml logs mssql-2022 || true
+ exit 1
+ - name: Run tests
+ run: cargo test ${{ matrix.features }}
+
+ integration-windows:
+ name: windows (SQL 2019, integrated auth)
+ if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/qa')
runs-on: windows-latest
strategy:
@@ -96,41 +298,39 @@ jobs:
TIBERIUS_TEST_CONNECTION_STRING: "server=tcp:127.0.0.1,1433;IntegratedSecurity=true;TrustServerCertificate=true"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
+ with:
+ toolchain: stable
+
+ - name: Compute cache key
+ shell: bash
+ run: |
+ key="${{ matrix.features }}"
+ key="${key//,/+}"
+ echo "RUST_CACHE_KEY=$key" >> "$GITHUB_ENV"
- - uses: actions-rs/toolchain@v1
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+ with:
+ shared-key: ${{ env.RUST_CACHE_KEY }}
- name: Set required PowerShell modules
id: psmodulecache
- uses: potatoqualitee/psmodulecache@v1
+ uses: potatoqualitee/psmodulecache@ee5e9494714abf56f6efbfa51527b2aec5c761b8 # v6.2.1
with:
modules-to-cache: SqlServer
- - name: Setup PowerShell module cache
- id: cacher
- uses: actions/cache@v2
- with:
- path: ${{ steps.psmodulecache.outputs.modulepath }}
- key: ${{ steps.psmodulecache.outputs.keygen }}
-
- name: Setup Chocolatey download cache
id: chococache
- uses: actions/cache@v2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\
key: chocolatey-install
- - name: Setup Cargo build cache
- uses: actions/cache@v2
- with:
- path: |
- C:\Users\runneradmin\.cargo\registry
- C:\Users\runneradmin\.cargo\git
- target
- key: ${{ runner.os }}-cargo
-
- name: Install required PowerShell modules
- if: steps.cacher.outputs.cache-hit != 'true'
shell: powershell
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
@@ -187,31 +387,3 @@ jobs:
- name: Run normal tests
shell: powershell
run: cargo test ${{matrix.features}}
-
- cargo-test-macos:
- runs-on: macos-12
-
- strategy:
- fail-fast: false
- matrix:
- database:
- - 2019
- features:
- - "--no-default-features --features=rustls,chrono,time,tds73,sql-browser-async-std,sql-browser-tokio,sql-browser-smol,integrated-auth-gssapi,rust_decimal,bigdecimal"
- - "--no-default-features --features=vendored-openssl"
-
- env:
- TIBERIUS_TEST_CONNECTION_STRING: "server=tcp:localhost,1433;user=SA;password=;TrustServerCertificate=true"
-
- steps:
- - uses: actions/checkout@v2
-
- - uses: actions-rs/toolchain@v1
-
- - uses: docker-practice/actions-setup-docker@master
-
- - name: Start SQL Server ${{matrix.database}}
- run: DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml up -d mssql-${{matrix.database}}
-
- - name: Run tests
- run: cargo test ${{matrix.features}}
diff --git a/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 000000000..3439483e1
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,10 @@
+# gitleaks config for the PR secret scan: full default rule set, with the
+# self-signed TLS test fixtures under docker/certs/ allowlisted.
+[extend]
+useDefault = true
+
+[allowlist]
+description = "Self-signed TLS test fixtures for the local integration-test SQL Server container"
+paths = [
+ '''docker/certs/.*''',
+]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fed7001e5..42611d30c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,83 @@
# Changes
+## Version 0.13.0
+
+- feat: TLS trust configuration is revamped around two orthogonal axes plus a
+ bypass, unifying community PRs #330 and #290:
+ - `Config::trust_cert_ca_bundle(bytes)` (and the `ConfigBuilder` mirror) trusts
+ additional CA certificates supplied as in-memory bytes, without writing them
+ to a temporary file. The bytes are auto-detected: a `-----BEGIN` marker is
+ parsed as a multi-certificate PEM bundle (e.g. the AWS RDS root bundle),
+ otherwise they are treated as a single DER certificate.
+ - `Config::trust_webpki_roots()` (and the `ConfigBuilder` mirror) bases trust
+ on a compiled-in snapshot of Mozilla's root CA store instead of the OS trust
+ store. rustls-only, behind the new `rustls-webpki-roots` feature. Note: the
+ bundled roots are a pinned snapshot that goes stale (missing newly added or
+ newly distrusted CAs) unless the dependency is updated and the app rebuilt.
+ - `Config::trust_cert_ca(path)` now accepts **multi-certificate** files (the
+ previous "exactly one certificate" restriction is lifted); every certificate
+ in the file is trusted, on all three TLS backends.
+- BREAKING: repeated `trust_cert_ca` calls now **accumulate** rather than
+ replace-last-wins. `trust_cert_ca(a); trust_cert_ca(b)` (and any mix with
+ `trust_cert_ca_bundle`) trusts every supplied CA, layered on top of the base
+ trust anchors. Code that relied on a later call overriding an earlier one must
+ now set the CA only once.
+- fix: a CA source (file or in-memory bundle) that yields zero usable
+ certificates is now a hard error naming the source, on every backend, instead
+ of silently degrading to base-roots-only trust. The `native-tls` and
+ `vendored-openssl` backends also now load *all* certificates from a
+ multi-certificate CA file/bundle (previously only the first was used) and
+ preserve the path plus underlying I/O error in load failures, matching rustls.
+- BREAKING: the connection-string `encrypt` default is now `Required` (was
+ `Off`) when a TLS backend is enabled, matching modern ADO.NET; without a TLS
+ backend it remains `NotSupported`.
+- BREAKING: removed the `sql-browser-async-std` feature and the async-std SQL
+ Browser integration.
+- feat: `Command`/RPC API for parameterized stored-procedure calls, plus a
+ `#[derive(TableValueRow)]` macro (in `tiberius-macros`) for table-valued
+ parameters.
+- feat: `sspi-rs` feature for Windows-style SSPI/NTLM authentication on Unix via
+ the pure-Rust `sspi` crate (no Kerberos required).
+- feat: `serde` feature adding `Serialize`/`Deserialize` impls for query result
+ types (`Row`, `Column`, `ColumnData`, `Numeric`, and the time/xml types).
+- feat: client-certificate authentication, including PEM/DER key files
+ (`Config::client_certificate`) and PKCS#12 bundles
+ (`Config::client_certificate_pkcs12`).
+- BREAKING: credentials (SQL Server / Windows passwords, the AAD bearer token
+ and the PKCS#12 password) are now stored as `secrecy::SecretString` instead of
+ `zeroize::Zeroizing`. They are still zeroized on drop, and their
+ `Debug` now renders as `SecretBox([REDACTED])` (was ``). The
+ `AuthMethod::AADToken` tuple variant consequently holds a `SecretString`: code
+ that pattern-matched it and read the token via `Deref`/`Display` must now call
+ `secrecy::ExposeSecret::expose_secret`. Constructing auth via
+ `AuthMethod::aad_token`/`sql_server`/`windows` is unchanged.
+- feat: connection & command timeouts (closes #375 and #360), matching
+ ADO.NET's two-knob model and backed by a runtime-agnostic timer so they apply
+ under any async runtime:
+ - `Config::handshake_timeout` bounds the whole post-TCP handshake (prelogin,
+ TLS negotiation and login), surfacing a `TimedOut` error instead of hanging
+ forever when a server accepts the TCP connection and then stalls
+ mid-handshake — the reported failure against `azure-sql-edge` on macOS
+ (#375) and the stalled-peer case in #360. Defaults to 15s (ADO.NET
+ `Connect Timeout` parity). The handshake also emits per-stage `tracing`
+ DEBUG events so a stall can be pinpointed.
+ - `Config::command_timeout` bounds each server round-trip while reading
+ command results (`query`/`execute`/`simple_query`, the `bulk_insert`
+ acknowledgement and `column_metadata`). It measures per-round-trip stall,
+ not total enumeration: the deadline resets on every delivered token, so a
+ slow consumer never trips it — only a stalled server does. Defaults to 30s
+ (ADO.NET `Command Timeout` parity).
+ - BREAKING: both knobs now default to a bounded value (15s handshake / 30s
+ command) where pre-0.13 they were effectively unbounded. A command that
+ legitimately runs longer than 30s between server round-trips (e.g. a long
+ `WAITFOR`, a big sort/aggregate or a slow stored procedure) will now fail
+ with a `TimedOut` error unless you raise or disable `command_timeout`. Pass
+ `None` to either knob to restore the pre-0.13 wait-indefinitely behaviour.
+- chore: upgraded the rustls stack to 0.23 (tokio-rustls 0.26) and resolved the
+ associated advisories.
+- fix: numerous decode-path hardening fixes (protocol errors instead of panics
+ or stream desyncs on hostile server input across the codec/token modules).
+
## Version 0.12.3
- feat: improve column type accuracy (#347)
- fix: encoding of zero-length values for large varlen columns (#315)
diff --git a/Cargo.toml b/Cargo.toml
index fb45b46e5..16bd8b76c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,24 +8,24 @@ authors = [
description = "A TDS (MSSQL) driver"
documentation = "https://docs.rs/tiberius/"
edition = "2021"
+rust-version = "1.88"
keywords = ["tds", "mssql", "sql"]
license = "MIT/Apache-2.0"
name = "tiberius"
readme = "README.md"
repository = "https://github.com/prisma/tiberius"
-version = "0.12.3"
+version = "0.13.0"
[workspace]
-members = ["runtimes-macro"]
+members = ["runtimes-macro", "tiberius-macros"]
[[test]]
path = "tests/query.rs"
name = "query"
[[test]]
-path = "tests/named-instance-async.rs"
-name = "named-instance-async"
-required-features = ["sql-browser-async-std"]
+path = "tests/command.rs"
+name = "command"
[[test]]
path = "tests/named-instance-tokio.rs"
@@ -37,29 +37,46 @@ path = "tests/named-instance-smol.rs"
name = "named-instance-smol"
required-features = ["sql-browser-smol"]
+[[test]]
+path = "tests/serde.rs"
+name = "serde"
+required-features = ["serde"]
+
+[[example]]
+name = "named-pipes"
+path = "examples/named-pipes.rs"
+
[dependencies]
enumflags2 = "0.7"
byteorder = "1.0"
encoding_rs = "0.8"
once_cell = "1.3"
-thiserror = "1.0"
+thiserror = "2"
bytes = "1.0"
-pretty-hex = "0.3"
+pretty-hex = "0.4"
pin-project-lite = "0.2"
-asynchronous-codec = "0.6"
+asynchronous-codec = "0.7"
async-trait = "0.1"
connection-string = "0.2"
num-traits = "0.2"
uuid = "1.0"
-
-winauth = { version = "0.0.4", optional = true }
+zeroize = "1.8.2"
+secrecy = "0.10"
+
+# Cross-platform: the NTLMv2 client (`winauth::NtlmV2Client`) is pure Rust and
+# backs `AuthMethod::Windows` on every target. Only its `windows` SSPI module
+# (used by `AuthMethod::Integrated`) is Windows-specific.
+[dependencies.winauth]
+version = "0.0.5"
+optional = true
[target.'cfg(unix)'.dependencies]
-libgssapi = { version = "0.8.1", optional = true, default-features = false }
+libgssapi = { version = "0.11", optional = true, default-features = false }
+sspi = { version = "0.18", optional = true }
+libc = "0.2"
[dependencies.async-native-tls]
-version = "0.4"
-features = ["runtime-async-std"]
+version = "0.6"
optional = true
[dependencies.tokio]
@@ -72,11 +89,6 @@ version = "0.7"
features = ["compat"]
optional = true
-[dependencies.async-std]
-version = "1"
-optional = true
-features = ["attributes"]
-
[dependencies.chrono]
version = "0.4"
optional = true
@@ -91,6 +103,13 @@ version = "0.3"
default-features = false
features = ["io", "sink"]
+# Runtime-agnostic timer used to bound the connection handshake (see
+# `Config::handshake_timeout`). Its `Delay` future works under any executor, so
+# the generic, runtime-agnostic `Connection::connect` path can enforce a timeout
+# without depending on tokio/smol timers.
+[dependencies.futures-timer]
+version = "3"
+
[dependencies.tracing]
features = ["log"]
version = "0.1"
@@ -100,33 +119,55 @@ version = "1.6"
optional = true
[dependencies.bigdecimal_]
-version = "0.3"
+version = "0.4"
optional = true
package = "bigdecimal"
+[dependencies.serde]
+version = "1.0"
+optional = true
+features = ["derive", "rc"]
+
[dependencies.async-io]
-version = "1.8"
+version = "2"
optional = true
[dependencies.async-net]
-version = "1.7"
+version = "2"
optional = true
[dependencies.futures-lite]
-version = "1.12.0"
+version = "2"
optional = true
[dependencies.tokio-rustls]
-version = "0.24.0"
+version = "0.26.4"
+optional = true
+
+[dependencies.rustls-native-certs]
+version = "0.8"
+optional = true
+
+# Version-floor pin for RUSTSEC-2026-0104 (rustls-webpki < 0.103.13); pulled in via tokio-rustls.
+[dependencies.rustls-webpki]
+version = "0.103.13"
optional = true
-features = ["dangerous_configuration"]
+default-features = false
-[dependencies.rustls-pemfile]
+# Shared certificate DER/PEM types used by the backend-agnostic CA loader
+# (`src/client/tls_stream/certs.rs`). Enabled by every TLS backend so all three
+# parse trust anchors through one code path. This is the same `rustls-pki-types`
+# crate `tokio-rustls` re-exports, so the `CertificateDer` handed to rustls is
+# the exact same type.
+[dependencies.rustls-pki-types]
version = "1"
optional = true
-[dependencies.rustls-native-certs]
-version = "0.6"
+# Compiled-in Mozilla root CA bundle for the `rustls-webpki-roots` feature.
+# Pinned to its current major (1.x). Note: this is a *pinned snapshot* that goes
+# stale unless the dependency is updated.
+[dependencies.webpki-roots]
+version = "1"
optional = true
[dependencies.opentls]
@@ -161,50 +202,74 @@ features = [
]
version = "1.0"
-[dev-dependencies.async-std]
-features = ["attributes"]
-version = "1"
+[dev-dependencies.smol]
+version = "2"
[dev-dependencies.runtimes-macro]
path = "./runtimes-macro"
+[dependencies.tiberius-macros]
+path = "./tiberius-macros"
+version = "0.1.0"
+
[dev-dependencies]
names = "0.14"
anyhow = "1"
-env_logger = "0.9"
-azure_identity = "0.5.0"
-oauth2 = "4.2.3"
-url = "2.2.2"
-reqwest = "0.11.10"
+env_logger = "0.11"
+azure_identity = "1.0"
+azure_core = "1"
+url = "2.5"
+reqwest = "0.13"
paste = "1.0"
-indicatif = "0.17"
+indicatif = "0.18"
chrono = "0.4.38"
-indoc = "1.0.7"
+indoc = "2"
+serde_json = "1.0"
[package.metadata.docs.rs]
-features = ["all", "docs"]
+features = ["all"]
+# docs.rs builds on nightly with this cfg set, enabling #[doc(cfg(...))]
+# annotations (feature(doc_cfg)) without requiring nightly for normal builds.
+rustdoc-args = ["--cfg", "docsrs"]
+
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs)'] }
[features]
all = [
"chrono",
"time",
"tds73",
- "sql-browser-async-std",
+ "tds80",
"sql-browser-tokio",
"sql-browser-smol",
"integrated-auth-gssapi",
"rust_decimal",
"bigdecimal",
"native-tls",
+ "serde",
+ "sspi-rs",
]
-default = ["tds73", "winauth", "native-tls"]
+default = ["tds80", "winauth", "native-tls"]
tds73 = []
-docs = []
-sql-browser-async-std = ["async-std"]
+# Enables TDS 8.0 support, including the `Strict` encryption level (TLS before
+# the TDS prelogin, TDS 8.0 "strict" mode). Requires a TLS backend.
+tds80 = ["tds73"]
sql-browser-tokio = ["tokio", "tokio-util"]
sql-browser-smol = ["async-io", "async-net", "futures-lite"]
integrated-auth-gssapi = ["libgssapi"]
bigdecimal = ["bigdecimal_"]
-rustls = ["tokio-rustls", "tokio-util", "rustls-pemfile", "rustls-native-certs"]
-native-tls = ["async-native-tls"]
-vendored-openssl = ["opentls", "schannel"]
+rustls = ["tokio-rustls", "tokio-util", "rustls-native-certs", "rustls-webpki", "rustls-pki-types"]
+# Bundle a compiled-in snapshot of Mozilla's root CA store, selectable via
+# `Config::trust_webpki_roots()` (rustls only). Implies `rustls`; the bundled
+# roots are a pinned snapshot that goes stale without dependency updates.
+rustls-webpki-roots = ["rustls", "dep:webpki-roots"]
+native-tls = ["async-native-tls", "rustls-pki-types"]
+vendored-openssl = ["opentls", "rustls-pki-types", "schannel"]
+# Enables Windows-style SSPI/NTLM authentication (`AuthMethod::Windows`) on Unix
+# platforms without requiring Kerberos, via the pure-Rust `sspi` crate. On
+# Windows the same authentication is provided by the `winauth` feature.
+sspi-rs = ["sspi"]
+# Optional serde Serialize/Deserialize impls for query result types
+# (Row, Column, ColumnData, Numeric, ColumnType and time/xml types).
+serde = ["dep:serde", "uuid/serde"]
diff --git a/README.md b/README.md
index 44398dc55..84a102ea7 100644
--- a/README.md
+++ b/README.md
@@ -44,10 +44,12 @@ A native Microsoft SQL Server (TDS) client for Rust.
| `time` | Read and write date and time values using `time` crate types. | `disabled` |
| `rust_decimal` | Read and write `numeric`/`decimal` values using `rust_decimal`'s `Decimal`. | `disabled` |
| `bigdecimal` | Read and write `numeric`/`decimal` values using `bigdecimal`'s `BigDecimal`. | `disabled` |
-| `sql-browser-async-std` | SQL Browser implementation for the `TcpStream` of async-std. | `disabled` |
| `sql-browser-tokio` | SQL Browser implementation for the `TcpStream` of Tokio. | `disabled` |
| `sql-browser-smol` | SQL Browser implementation for the `TcpStream` of smol. | `disabled` |
| `integrated-auth-gssapi` | Support for using Integrated Auth via GSSAPI | `disabled` |
+| `winauth` | Windows-only SSPI/NTLM integrated authentication (`AuthMethod::Windows`). | `enabled` |
+| `sspi-rs` | Windows-style SSPI/NTLM authentication on Unix via the pure-Rust `sspi` crate (no Kerberos required). | `disabled` |
+| `serde` | `serde` `Serialize`/`Deserialize` impls for query result types (`Row`, `Column`, `ColumnData`, `Numeric`, etc.). | `disabled` |
### Supported protocols
diff --git a/deny.toml b/deny.toml
new file mode 100644
index 000000000..f515700c0
--- /dev/null
+++ b/deny.toml
@@ -0,0 +1,43 @@
+# Run locally with: cargo deny check advisories bans sources
+# CI runs the same via .github/workflows/security.yml.
+#
+# Policy: a vulnerability or yanked crate in the default-built graph fails the build; ignored advisories are only reachable via dev-deps or opt-in features.
+
+[advisories]
+yanked = "deny"
+ignore = [
+ # The following are ALL dev-dependency-only (test harness + the aad-auth
+ # example) and are never compiled into the published library.
+ { id = "RUSTSEC-2024-0375", reason = "atty: dev-dependency only (via `names` -> clap 3); not shipped" },
+ { id = "RUSTSEC-2024-0370", reason = "proc-macro-error: dev-dependency only (via `names` -> clap 3); not shipped" },
+ { id = "RUSTSEC-2024-0436", reason = "paste: dev/test only (tests/bulk.rs + azure_identity example); not shipped" },
+]
+
+[licenses]
+# Allow-list for the current dependency graph (verified locally with
+# `cargo deny check licenses`). Every crate resolves to at least one of these
+# via its SPDX expression; add new entries here rather than loosening the policy.
+allow = [
+ "MIT",
+ "MIT-0",
+ "Apache-2.0",
+ "Apache-2.0 WITH LLVM-exception",
+ "BSD-1-Clause",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "ISC",
+ "CC0-1.0",
+ "Unicode-3.0",
+ "Unlicense",
+]
+# Confidence threshold for detecting a license from its text (0.0 - 1.0).
+confidence-threshold = 0.8
+
+[bans]
+multiple-versions = "warn"
+wildcards = "allow"
+
+[sources]
+unknown-registry = "deny"
+unknown-git = "deny"
+allow-registry = ["https://github.com/rust-lang/crates.io-index"]
diff --git a/docker-compose.yml b/docker-compose.yml
index db5f3a39a..2aef9c6e4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,7 @@
version: "3"
services:
mssql-2022:
+ platform: linux/amd64
build:
context: docker/
dockerfile: docker-mssql-2022.dockerfile
@@ -12,6 +13,7 @@ services:
- "1433:1433"
mssql-2019:
+ platform: linux/amd64
build:
context: docker/
dockerfile: docker-mssql-2019.dockerfile
@@ -23,6 +25,7 @@ services:
- "1433:1433"
mssql-2017:
+ platform: linux/amd64
build:
context: docker/
dockerfile: docker-mssql-2017.dockerfile
@@ -34,6 +37,7 @@ services:
- "1433:1433"
mssql-azure-sql-edge:
+ platform: linux/amd64
build:
context: docker/
dockerfile: docker-azure-sql-edge.dockerfile
diff --git a/docker/certs/customCA.srl b/docker/certs/customCA.srl
index 618df7789..a02a6570a 100644
--- a/docker/certs/customCA.srl
+++ b/docker/certs/customCA.srl
@@ -1 +1 @@
-0DAEECC45C07F5E06E0DD1B05115C3CFD1A46D9C
+0DAEECC45C07F5E06E0DD1B05115C3CFD1A46D9D
diff --git a/docker/certs/generate-signed-cert.sh b/docker/certs/generate-signed-cert.sh
index dc3086f29..db3858cca 100755
--- a/docker/certs/generate-signed-cert.sh
+++ b/docker/certs/generate-signed-cert.sh
@@ -5,8 +5,10 @@ set -o pipefail
# Skript creates a custom-signed certificate
# Parameter1 = name of the cert
+# Parameter2 = validity in days (default 1825)
CERT_KEY_NAME=$1
+CERT_DAYS=${2:-1825}
CERT_FILE=$CERT_KEY_NAME.crt
export CERT_CN=$CERT_KEY_NAME
@@ -32,7 +34,7 @@ openssl x509 -req \
-CAserial customCA.srl \
-out $CERT_FILE \
-passin file:passphrase.txt \
- -days 200
+ -days $CERT_DAYS
echo Generating PEM format
openssl rsa -in ${CERT_KEY_NAME}.key -out ${CERT_KEY_NAME}-nopassword.key
diff --git a/docker/certs/server-full.crt b/docker/certs/server-full.crt
index 31ceafd70..1128cc190 100644
--- a/docker/certs/server-full.crt
+++ b/docker/certs/server-full.crt
@@ -1,33 +1,33 @@
-----BEGIN CERTIFICATE-----
-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
+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
-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
+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
-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
-HTMZ5IJXU9f1XKtMHLdGRpx0YiEGTw0e
+BQcDAjAdBgNVHQ4EFgQUKTH2Ri4hNDGnL4ifUg7HEbEwhbQwHwYDVR0jBBgwFoAU
+RHcTzm1u6x8WiXeAWDblHzwBt9kwDQYJKoZIhvcNAQELBQADggIBAByBbh6Mj+jp
+z0Rb2vdiEV4sK0o+ad96p74ZJdiyeTLki8fLSxtKlnlrlhAzY/YFr49KQJKOzbHM
+X1aoieL4Si72eprWREyNcXuD2N7tuVnw8/p3WpqW7IKBXSDDdqkdppc1B+LvTBwI
++FXSdou7dPuHgim8fHmoz/ogj+Zf1gvog3ohcnAtj9kN0zfQoBjeyjQ7v81uQ0sx
+K8AO+yg/P/IWSNfzEMEGRxT91as9IrV+nmvIfe7k14ljDdJDsf+FRkea9UBOhtJW
+G7cqFeWTCBV7W8bjFB0kBF9HE09E2B7hUtYZwOpVruhxXdy7WdzQzx8RWXG/bJnS
+qML1bw+sdY+RtfbOr1jy8ctcAg+OmBbR0qLDQeuWlXqjTtxoHViMZpa6lNtIuD8+
+1e3+iFJ53djOSgSZ6XW163HI9353nrr1dXtlx7kdPZsb5Z3FXvL940rLiIx69ftR
+dP4hP7iWstrUsrwnk6E3OmVwzc+pD8f72ztFhcqI81rmvgJ/MufGvaKoB254OibT
+ng4pgs4NF2kKSFmqhXG1dTen2XRlg4ZecLrcCcotdcFX4qPEGcPjjQ4UEEaYhgFW
+yWmTUWJEMO9BtqSxUFTZiQ8Ul0cJs16CyAC+oxGhaM92r7w/2xZ7fH4MHGyzJcm6
+WY7hfVHCK4+xjXMLn+k5qZYVEPUPe+0s
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIE/zCCAuegAwIBAgIUATFLyERaRfsQiPasMC5l0vrBMUMwDQYJKoZIhvcNAQEL
diff --git a/docker/certs/server.crt b/docker/certs/server.crt
index 95e4d43e4..2804eb8af 100644
--- a/docker/certs/server.crt
+++ b/docker/certs/server.crt
@@ -1,31 +1,31 @@
-----BEGIN CERTIFICATE-----
-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
+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
-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
+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
-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
-HTMZ5IJXU9f1XKtMHLdGRpx0YiEGTw0e
+BQcDAjAdBgNVHQ4EFgQUKTH2Ri4hNDGnL4ifUg7HEbEwhbQwHwYDVR0jBBgwFoAU
+RHcTzm1u6x8WiXeAWDblHzwBt9kwDQYJKoZIhvcNAQELBQADggIBAByBbh6Mj+jp
+z0Rb2vdiEV4sK0o+ad96p74ZJdiyeTLki8fLSxtKlnlrlhAzY/YFr49KQJKOzbHM
+X1aoieL4Si72eprWREyNcXuD2N7tuVnw8/p3WpqW7IKBXSDDdqkdppc1B+LvTBwI
++FXSdou7dPuHgim8fHmoz/ogj+Zf1gvog3ohcnAtj9kN0zfQoBjeyjQ7v81uQ0sx
+K8AO+yg/P/IWSNfzEMEGRxT91as9IrV+nmvIfe7k14ljDdJDsf+FRkea9UBOhtJW
+G7cqFeWTCBV7W8bjFB0kBF9HE09E2B7hUtYZwOpVruhxXdy7WdzQzx8RWXG/bJnS
+qML1bw+sdY+RtfbOr1jy8ctcAg+OmBbR0qLDQeuWlXqjTtxoHViMZpa6lNtIuD8+
+1e3+iFJ53djOSgSZ6XW163HI9353nrr1dXtlx7kdPZsb5Z3FXvL940rLiIx69ftR
+dP4hP7iWstrUsrwnk6E3OmVwzc+pD8f72ztFhcqI81rmvgJ/MufGvaKoB254OibT
+ng4pgs4NF2kKSFmqhXG1dTen2XRlg4ZecLrcCcotdcFX4qPEGcPjjQ4UEEaYhgFW
+yWmTUWJEMO9BtqSxUFTZiQ8Ul0cJs16CyAC+oxGhaM92r7w/2xZ7fH4MHGyzJcm6
+WY7hfVHCK4+xjXMLn+k5qZYVEPUPe+0s
-----END CERTIFICATE-----
diff --git a/docker/certs/server.key b/docker/certs/server.key
index 7e60bb02e..71c4e52fd 100644
--- a/docker/certs/server.key
+++ b/docker/certs/server.key
@@ -1,52 +1,52 @@
-----BEGIN PRIVATE KEY-----
-MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDO0oLtSWgm7EYx
-o6yVabu8QNx3xiRwL0/g8yoNNLSqCv8hd4rUsHU+3bCIRjgHP8F3duiPYgyBHNP2
-CO0mgPpjcBrfZRanVOCsO0tDoGezfR3qw12TvEnmb2+ozdmklJ0s0qiW+V1x1tRZ
-CS2EpQICs1DMdSgcjvQ81kYoTqKKRaiq86/jFSrcvwl7k7Czx/Dw1jxPJokCIcTz
-M4fSRTJEZpWnb6S7qo0csU9ouT+ImOUyzUJ+x1Q2M0383liHBjEXuKQiGyc1dXv8
-AcpB4zTPmOUPvhXCeq67JdzQyCrgj33GDucldA0sp1Rfu2EMnwEl0ozuLwoqf9SA
-OhTZRIiIXXHwaypnMVH/8tL2W36f+6124R0FftS3Suue/aVBmEvjfXeHLN2/R/mB
-oSMICd1Be7+ir1K60kMBre6zMd6OLIERaCl9jvGJpNPF/lcxO576FvCu5cuVf4fS
-10f1b8Fa5Pv5FHcffmcpngUvI0bSDeOgvmVrdYiFEdv08/mFlIHox/Wada4FaInr
-cwnMBWx+7DIEyKvx95FuP5i8jprM9d+TPzvg51ac30VmLoZRbLfPhQDCcLQ5c7lb
-AfnqAd9mL27ZLvRmGjiM8+cXc4/YqtUlJA2tz2rALUzQck1xjD07gRGcjk5iRyUR
-p+sr3xVVE+Ge4osNqTfWN6dSHifA0QIDAQABAoICAADFLMzFjAZPlVIWYQRYLcVd
-ZDjLt4tlqLVusGSW0niq5HD3ZxBkVRZyKMf0I32m65F2Y1az27YwIVuyZDAzVSNh
-Sa9U6vr97F2F1cGbZ4F2DQJInpjID+okVnkNZbLoxQZThUJVLMd5kGZBvA45N1cD
-XBDb25WyJFeU6HNaWh171Y1H7arxw2xpp3dS6Sq9OxDpilVU4FgeQDOT6LzEKlQS
-AfsK9dUHVUHS6Pfbz0BS6fEYzbdnRoFyatcfDJs5nx2Oj+lq2pg2zxq01sAMsJ/Y
-ittWdtIn5u5OXXp3UV4PWL1/5RVZD5q/x4cY/Xs4nR5rAKB7Mz1t5xCgbr8Ro9TE
-9PVzrbGy8hCWW0Yz+zhwIsDrtkQ7RGIg95W7IjaxnrjCUszK0xG1hXpce1qg1EN0
-rF4u7pU0qEWw4piLfIXepVZxVo27dOYj9qEpDkGiVYXCJ3+HifHBt5tE/rVkStF3
-dzihxyk5E7F4wJd9tz2xAMxFSgG3IeEZ3IOCxFWJib6micXZJ2n6N9uuUnHGW3D2
-o7FC02G1gXsxxgY871b8G6mFyGhmfEJxqrIvek8fBvvgOPWKnroLqJprxYow6miE
-QU6yC4C/1RZgn/l6kj9jz2r6BY2nVjhHjbLGTh9bsqf5dCPdJV01FsVMiJqUzg5+
-HR5XJSf1hXRx/egBYdaBAoIBAQD3Hb12rwXRVaf38wth4VMaZr1Dxgkt0/X58LTf
-SXPzGMChqnhBKdNHPv4pfWpBbvKBPWUcd+uBylgABl4xD8QH6VcspRWdgAJjul4K
-RCRdWJtt0nxOqU4KitaBWOM7d6Ec3oCCaOZI5ZT+6Hj+X/RmAwd9acNM8NQ5166y
-AyVQfO+2QvWRgLWxyYnBIRYkPU0L+ItkBxWpe0W8bRCj2ilAP+UCH0VSGMsnkzKw
-y2HQtLGu8EBODmoW36qeYFYf6iKTMQpdtwyRYjjVq5smYSfJPy5WvdIOvcbcpI4I
-Edpd1GvdjcwdfTKPiCvhDgpjQUCEOeLaKvszSFAxsSyyMFRRAoIBAQDWQfBWEwLT
-jFZ9N07xkMxG4qA28KUXIHZ53DkEQmrDYQWSpJ6OfrhQgwtX9CtTMoyrG4gw1IDJ
-lAcx91o6GVkC4CP8+ssvhPZi+KD9iVAI61hg3gVyxvndXgYg2xBeJ8IBm7Jkg5HK
-A9tZW8jEfH+nO6HhszY0r9VNov2naRwGGZ9JgGpcMvFN5taXOhierfk3L63zaJPJ
-Mx8Aaspxlk7u9ommZ1jkdpmczUzPfEpyRfSD9qoKxA4GOYPxDCUSkAyy6XzlF4rg
-AKetXg5yDNa2Y4MXfbIK40Oh1wz7e9yZDjovSxonjC141RD8ybyOXhfsK67oMMME
-J0gxhBR3vASBAoIBAG0jJVoVUmxxeA15ub0w1pMCbPRRshwbULdiJ3+14Q+sDudX
-cmTVJAqDN5z7VsIvTcrmYpGAJPLdeqAIL/FbFSipVWbSQgmdT3DcDkxaa/UN/Rcz
-rtLO0zi0uKfHqhPJcc5eNkNiMNJhErzBzy4JEtc630P0QdzpP9GMAAt+eCxkATpt
-uCbawWQTrlMtWaoHqM9wpZ83wcloOBRP1tmGsFE/5tRZGzR23sJLsEeEi16xbwfj
-84KFuzT+80ufIGpX7Y00S2+4OES9LHyxnYQFxJyM2tpUW0FHb1xjEJdfyyFFf54J
-0ev0LzBU44wxt0S+vM+pARd5hBfSCBjqNuM7lQECggEALhpmMr9IfmjWO39pN0Wn
-DyG4w9moTH+pvrMKecYo3v3Dizhs/dB6rKhmCnj50Z8w8ais94TiaX22xqOpAJNv
-udStKcR1cDY2JjnFuoiPdjvd+ooLthTmsyGGRA+fSANaFaqBCmvdNRD7ZBEB9HWt
-qjiEruI3KcMkLN6DokBVzWI6CkDdohU8Iz0ms8fGgG6DD8LstVGtaz/azeYsxaBI
-P9dA61OVpyN2Dm2Gt6bRBiHTaYnsMQDa27AImhe46nOgp+bh/xG/yk+ZxQ5WIWht
-0zU6ghWD+B/K78osevi+ERkkoASTDit1pWiDjUGDl0bb8u+7ZS8I553kRPNczB7j
-AQKCAQEA9wJW7rWBuIVMUymSqynSvy4SqClOX2IKFbsJqqe3PO5dby/8YnxPXOZK
-lq7gSXWfSgTN29JY5beVBLJI66spSTiz6AP4/iWQqCpzw9VM0Gv7GxIasZmfP+tp
-l4JV8+yAElOFd1IhjV3RKGU1fGPGJfstIBt5eXQCSVQyQaFYQeGYE0KU5AUD6lvY
-6R9irgVicVa9x1eq5HVcTVYb0gFs4zSZ1YlpqTc/i1ttZEWGyzmOK5cMX2iOeou7
-H/IZyIjtTm6edWgUANXhZdDss3gBUitLUpne579efdPCTJ4vqRjEA8tjZeGgmJpf
-Oeu1HE+LelnM2vOc9TtbJC9FrC8nYw==
+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
+6FJg2C5WHHHhAOWiS+gM1HBaeLSLwA==
-----END PRIVATE KEY-----
diff --git a/docker/certs/server.pem b/docker/certs/server.pem
index 7acbb192f..4fc2c9526 100644
--- a/docker/certs/server.pem
+++ b/docker/certs/server.pem
@@ -1,83 +1,83 @@
-----BEGIN PRIVATE KEY-----
-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
-Oeu1HE+LelnM2vOc9TtbJC9FrC8nYw==
+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
+6FJg2C5WHHHhAOWiS+gM1HBaeLSLwA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
-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
+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
-CgKCAgEAztKC7UloJuxGMaOslWm7vEDcd8YkcC9P4PMqDTS0qgr/IXeK1LB1Pt2w
-iEY4Bz/Bd3boj2IMgRzT9gjtJoD6Y3Aa32UWp1TgrDtLQ6Bns30d6sNdk7xJ5m9v
-qM3ZpJSdLNKolvldcdbUWQkthKUCArNQzHUoHI70PNZGKE6iikWoqvOv4xUq3L8J
-e5Ows8fw8NY8TyaJAiHE8zOH0kUyRGaVp2+ku6qNHLFPaLk/iJjlMs1CfsdUNjNN
-/N5YhwYxF7ikIhsnNXV7/AHKQeM0z5jlD74VwnquuyXc0Mgq4I99xg7nJXQNLKdU
-X7thDJ8BJdKM7i8KKn/UgDoU2USIiF1x8GsqZzFR//LS9lt+n/utduEdBX7Ut0rr
-nv2lQZhL4313hyzdv0f5gaEjCAndQXu/oq9SutJDAa3uszHejiyBEWgpfY7xiaTT
-xf5XMTue+hbwruXLlX+H0tdH9W/BWuT7+RR3H35nKZ4FLyNG0g3joL5la3WIhRHb
-9PP5hZSB6Mf1mnWuBWiJ63MJzAVsfuwyBMir8feRbj+YvI6azPXfkz874OdWnN9F
-Zi6GUWy3z4UAwnC0OXO5WwH56gHfZi9u2S70Zho4jPPnF3OP2KrVJSQNrc9qwC1M
-0HJNcYw9O4ERnI5OYkclEafrK98VVRPhnuKLDak31jenUh4nwNECAwEAAaN3MHUw
+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
-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
-HTMZ5IJXU9f1XKtMHLdGRpx0YiEGTw0e
+BQcDAjAdBgNVHQ4EFgQUKTH2Ri4hNDGnL4ifUg7HEbEwhbQwHwYDVR0jBBgwFoAU
+RHcTzm1u6x8WiXeAWDblHzwBt9kwDQYJKoZIhvcNAQELBQADggIBAByBbh6Mj+jp
+z0Rb2vdiEV4sK0o+ad96p74ZJdiyeTLki8fLSxtKlnlrlhAzY/YFr49KQJKOzbHM
+X1aoieL4Si72eprWREyNcXuD2N7tuVnw8/p3WpqW7IKBXSDDdqkdppc1B+LvTBwI
++FXSdou7dPuHgim8fHmoz/ogj+Zf1gvog3ohcnAtj9kN0zfQoBjeyjQ7v81uQ0sx
+K8AO+yg/P/IWSNfzEMEGRxT91as9IrV+nmvIfe7k14ljDdJDsf+FRkea9UBOhtJW
+G7cqFeWTCBV7W8bjFB0kBF9HE09E2B7hUtYZwOpVruhxXdy7WdzQzx8RWXG/bJnS
+qML1bw+sdY+RtfbOr1jy8ctcAg+OmBbR0qLDQeuWlXqjTtxoHViMZpa6lNtIuD8+
+1e3+iFJ53djOSgSZ6XW163HI9353nrr1dXtlx7kdPZsb5Z3FXvL940rLiIx69ftR
+dP4hP7iWstrUsrwnk6E3OmVwzc+pD8f72ztFhcqI81rmvgJ/MufGvaKoB254OibT
+ng4pgs4NF2kKSFmqhXG1dTen2XRlg4ZecLrcCcotdcFX4qPEGcPjjQ4UEEaYhgFW
+yWmTUWJEMO9BtqSxUFTZiQ8Ul0cJs16CyAC+oxGhaM92r7w/2xZ7fH4MHGyzJcm6
+WY7hfVHCK4+xjXMLn+k5qZYVEPUPe+0s
-----END CERTIFICATE-----
diff --git a/docker/docker-azure-sql-edge.dockerfile b/docker/docker-azure-sql-edge.dockerfile
index 14279c405..d4a009035 100644
--- a/docker/docker-azure-sql-edge.dockerfile
+++ b/docker/docker-azure-sql-edge.dockerfile
@@ -1,5 +1,9 @@
FROM mcr.microsoft.com/azure-sql-edge:latest
-COPY --chmod=440 certs/server.* /certs/
-COPY --chmod=440 certs/customCA.* /certs/
+USER root
+COPY certs/server.* /certs/
+RUN chmod 440 /certs/server.*
+COPY certs/customCA.* /certs/
+RUN chmod 440 /certs/customCA.*
COPY --chown=mssql docker-mssql.conf /var/opt/mssql/mssql.conf
+USER mssql
diff --git a/docker/docker-mssql-2017.dockerfile b/docker/docker-mssql-2017.dockerfile
index 28a3dd4f4..ec4ccf451 100644
--- a/docker/docker-mssql-2017.dockerfile
+++ b/docker/docker-mssql-2017.dockerfile
@@ -1,5 +1,8 @@
FROM mcr.microsoft.com/mssql/server:2017-latest
-COPY --chmod=440 certs/server.* /certs/
-COPY --chmod=440 certs/customCA.* /certs/
+USER root
+COPY certs/server.* /certs/
+RUN chmod 440 /certs/server.*
+COPY certs/customCA.* /certs/
+RUN chmod 440 /certs/customCA.*
COPY docker-mssql.conf /var/opt/mssql/mssql.conf
diff --git a/docker/docker-mssql-2019.dockerfile b/docker/docker-mssql-2019.dockerfile
index 02ffdec0d..097a1d24d 100644
--- a/docker/docker-mssql-2019.dockerfile
+++ b/docker/docker-mssql-2019.dockerfile
@@ -1,5 +1,9 @@
FROM mcr.microsoft.com/mssql/server:2019-latest
-COPY --chmod=440 certs/server.* /certs/
-COPY --chmod=440 certs/customCA.* /certs/
+USER root
+COPY certs/server.* /certs/
+RUN chmod 440 /certs/server.*
+COPY certs/customCA.* /certs/
+RUN chmod 440 /certs/customCA.*
COPY --chown=mssql docker-mssql.conf /var/opt/mssql/mssql.conf
+USER mssql
diff --git a/docker/docker-mssql-2022.dockerfile b/docker/docker-mssql-2022.dockerfile
index 930d3026c..aefdd64e6 100644
--- a/docker/docker-mssql-2022.dockerfile
+++ b/docker/docker-mssql-2022.dockerfile
@@ -1,5 +1,9 @@
FROM mcr.microsoft.com/mssql/server:2022-latest
-COPY --chmod=444 certs/server.* /certs/
-COPY --chmod=444 certs/customCA.* /certs/
+USER root
+COPY certs/server.* /certs/
+RUN chmod 444 /certs/server.*
+COPY certs/customCA.* /certs/
+RUN chmod 444 /certs/customCA.*
COPY --chown=mssql docker-mssql.conf /var/opt/mssql/mssql.conf
+USER mssql
diff --git a/docker/test-server.sh b/docker/test-server.sh
new file mode 100755
index 000000000..a0239280c
--- /dev/null
+++ b/docker/test-server.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+#
+# Start a SQL Server for the test suite, with podman or docker.
+#
+# ./docker/test-server.sh up # build, start, wait until it accepts connections
+# ./docker/test-server.sh down # stop and remove
+# ./docker/test-server.sh logs # follow the server log
+#
+# Then:
+#
+# export TIBERIUS_TEST_CONNECTION_STRING='server=tcp:localhost,1433;user=SA;password=;IntegratedSecurity=true;TrustServerCertificate=true'
+# cargo test
+#
+# IMAGE selects the flavour; the default works on both x86_64 and arm64.
+# The full SQL Server images are x86_64 only, so on an arm64 machine
+# (Apple silicon) they either refuse to run or run under emulation.
+
+set -euo pipefail
+
+ENGINE="${ENGINE:-$(command -v podman >/dev/null 2>&1 && echo podman || echo docker)}"
+NAME="${NAME:-tiberius-test-mssql}"
+PORT="${PORT:-1433}"
+IMAGE="${IMAGE:-azure-sql-edge}"
+PASSWORD=''
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+case "${1:-up}" in
+ up)
+ echo "engine: $ENGINE image: $IMAGE port: $PORT"
+ "$ENGINE" build -q -f "$HERE/docker-$IMAGE.dockerfile" -t "$NAME:local" "$HERE"
+ "$ENGINE" rm -f "$NAME" >/dev/null 2>&1 || true
+ "$ENGINE" run -d --name "$NAME" \
+ -e ACCEPT_EULA=Y \
+ -e "MSSQL_SA_PASSWORD=$PASSWORD" \
+ -e "SA_PASSWORD=$PASSWORD" \
+ -p "$PORT:1433" \
+ "$NAME:local" >/dev/null
+
+ # The port opens well before the server will answer, so poll the log
+ # rather than the socket.
+ #
+ # The log is captured into a variable and matched there, rather than
+ # piped into `grep -q`. Under `set -o pipefail`, `grep -q` exits on the
+ # first match, the writer upstream dies of SIGPIPE, and the pipeline
+ # reports failure even though the match succeeded — so the wait never
+ # ends.
+ echo -n "waiting for SQL Server"
+ for _ in $(seq 1 120); do
+ logs="$("$ENGINE" logs "$NAME" 2>&1 || true)"
+
+ case "$logs" in
+ *"SQL Server is now ready for client connections"*)
+ echo " — ready"
+ exit 0
+ ;;
+ esac
+
+ running="$("$ENGINE" ps --format '{{.Names}}' || true)"
+ case "$running" in
+ *"$NAME"*) ;;
+ *)
+ echo " — container exited:"
+ "$ENGINE" logs --tail 30 "$NAME" || true
+ exit 1
+ ;;
+ esac
+
+ echo -n .
+ sleep 2
+ done
+ echo " — gave up; last lines:"
+ "$ENGINE" logs --tail 30 "$NAME"
+ exit 1
+ ;;
+ down)
+ "$ENGINE" rm -f "$NAME" >/dev/null 2>&1 || true
+ echo "removed $NAME"
+ ;;
+ logs)
+ "$ENGINE" logs -f "$NAME"
+ ;;
+ *)
+ echo "usage: $0 {up|down|logs}" >&2
+ exit 2
+ ;;
+esac
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
new file mode 100644
index 000000000..11e5683d0
--- /dev/null
+++ b/docs/GUIDE.md
@@ -0,0 +1,325 @@
+# Tiberius Guide
+
+A practical tour of the driver. For the full API see [docs.rs](https://docs.rs/tiberius).
+Every example imports the crate as `tiberius` (the package is `tiberius`; see
+the [README](../README.md#installation)).
+
+- [Connecting](#connecting)
+- [Configuration](#configuration)
+- [Encryption & TLS](#encryption--tls)
+- [Authentication](#authentication)
+- [Querying](#querying)
+- [Reading rows](#reading-rows)
+- [Bulk insert](#bulk-insert)
+- [Stored procedures, OUT params & TVPs](#stored-procedures-out-params--tvps)
+- [Transactions](#transactions)
+- [`IN (…)` lists](#in--lists)
+- [Named instances (SQL Browser)](#named-instances-sql-browser)
+- [Query cancellation](#query-cancellation)
+- [Connection pooling](#connection-pooling)
+- [Error handling](#error-handling)
+
+## Connecting
+
+Tiberius is runtime-independent: you create the `TcpStream` and hand it to the
+[`Client`].
+
+**Tokio** (wrap the stream with `tokio_util::compat`):
+
+```rust
+use tiberius::{Client, Config, AuthMethod};
+use tokio::net::TcpStream;
+use tokio_util::compat::TokioAsyncWriteCompatExt;
+
+# async fn f() -> anyhow::Result<()> {
+let mut config = Config::new();
+config.host("localhost");
+config.port(1433);
+config.authentication(AuthMethod::sql_server("SA", ""));
+config.trust_cert(); // dev only
+
+let tcp = TcpStream::connect(config.get_addr()).await?;
+tcp.set_nodelay(true)?;
+let mut client = Client::connect(config, tcp.compat_write()).await?;
+# Ok(()) }
+```
+
+**smol** (pass the stream directly — no compat layer):
+
+```rust,ignore
+let tcp = smol::net::TcpStream::connect(config.get_addr()).await?;
+tcp.set_nodelay(true)?;
+let mut client = tiberius::Client::connect(config, tcp).await?;
+```
+
+## Configuration
+
+Build a [`Config`] fluently, or parse a connection string:
+
+```rust,ignore
+// ADO.NET
+let config = Config::from_ado_string(
+ "Server=tcp:localhost,1433;User Id=SA;Password=pw;Encrypt=strict;",
+)?;
+
+// JDBC
+let config = Config::from_jdbc_string(
+ "jdbc:sqlserver://localhost:1433;user=SA;password=pw;encrypt=true",
+)?;
+
+// Builder
+let config = Config::builder()
+ .host("localhost")
+ .port(1433)
+ .authentication(AuthMethod::sql_server("SA", "pw"))
+ .build();
+```
+
+## Encryption & TLS
+
+TLS is on by default. Pick a backend via a feature flag (mutually exclusive):
+`native-tls` (default), `rustls`, or `vendored-openssl`.
+
+Encryption levels (`Config::encryption`): `NotSupported`, `Off`, `Required`
+(default), and **`Strict`** — TDS 8.0, TLS *before* the pre-login, with the
+`tds/8.0` ALPN (requires the `tds80` feature; native-tls or rustls).
+
+```rust,ignore
+use tiberius::EncryptionLevel;
+config.encryption(EncryptionLevel::Strict);
+config.hostname_in_certificate("my-sql-host"); // validate against a specific CN/SAN
+config.client_certificate("client.pem", "client.key"); // mutual TLS
+// or: config.client_certificate_pkcs12("client.pfx", "password");
+```
+
+## Authentication
+
+```rust,ignore
+// SQL Server login (password buffers are zeroized)
+config.authentication(AuthMethod::sql_server("user", "pw"));
+
+// Windows integrated auth: SSPI on Windows (winauth), NTLM on Unix (sspi-rs)
+config.authentication(AuthMethod::windows("user", "pw"));
+
+// Kerberos on Unix (integrated-auth-gssapi feature)
+config.authentication(AuthMethod::Integrated);
+
+// Azure AD access token
+config.authentication(AuthMethod::aad_token(token));
+```
+
+## Querying
+
+From the [`Client`] when parameters are known at the call site:
+
+```rust,ignore
+// Rows back
+let stream = client.query("SELECT @P1, @P2", &[&1i32, &"foo"]).await?;
+
+// Rows affected
+let result = client.execute("UPDATE t SET x = @P1 WHERE id = @P2", &[&1i32, &2i32]).await?;
+println!("{} rows", result.total());
+```
+
+For dynamic or owned parameters, use the [`Query`] object:
+
+```rust,ignore
+use tiberius::Query;
+let mut select = Query::new("SELECT @P1, @P2");
+for p in ["a", "b"] { select.bind(p); }
+let stream = select.query(&mut client).await?;
+```
+
+## Reading rows
+
+A query returns a stream. Collect it, or take the first row:
+
+```rust,ignore
+// All rows of the first result set
+let rows = client.query("SELECT id, name FROM users", &[]).await?
+ .into_first_result().await?;
+
+for row in rows {
+ let id: i32 = row.get("id").unwrap();
+ let name: &str = row.get("name").unwrap();
+}
+
+// Just the first row
+let row = client.query("SELECT 1 AS n", &[]).await?.into_row().await?.unwrap();
+let n: i32 = row.get("n").unwrap();
+```
+
+Type mappings are available via `FromSql`/`ToSql`, with optional `chrono`,
+`time`, `rust_decimal`, `bigdecimal`, and `serde` support behind their features.
+`Client::column_metadata()` exposes column type, size, precision/scale,
+nullability and identity flags.
+
+## Bulk insert
+
+Efficiently stream many rows into a table:
+
+```rust,ignore
+use tiberius::IntoRow;
+
+let mut req = client.bulk_insert("dbo.target").await?; // all columns
+// or a specific column list:
+// let mut req = client.bulk_insert_columns("dbo.target", &["foo", "bar"]).await?;
+
+for i in 0..1000i32 {
+ req.send(i.into_row()).await?;
+}
+let res = req.finalize().await?;
+println!("{} rows", res.total());
+```
+
+## Stored procedures, OUT params & TVPs
+
+Named RPC with input, output and table-valued parameters is supported via the
+[`Command`] API. A TVP row type derives `TableValueRow`:
+
+```rust,ignore
+use tiberius::{Command, TableValueRow};
+
+// Fields may be `Copy` scalars (`i32`, `Numeric`, `Uuid`, …), owned columns
+// (`String`, `Vec`), or borrowed `&str`/`&[u8]` (with a struct lifetime).
+#[derive(TableValueRow)]
+struct Item {
+ #[colname = "Id"] id: i32,
+ #[colname = "Name"] name: String,
+}
+
+let rows = vec![
+ Item { id: 1, name: "one".to_owned() },
+ Item { id: 2, name: "two".to_owned() },
+];
+
+let mut cmd = Command::new("dbo.InsertItems");
+cmd.bind_table_with_dbtype("items", "dbo.ItemList", rows);
+let mut stream = cmd.exec(&mut client).await?;
+```
+
+## Transactions
+
+Real Transaction Manager requests (not T-SQL batches), with isolation levels:
+
+```rust,ignore
+client.begin_transaction().await?;
+// ... work ...
+client.commit_transaction().await?;
+// or client.rollback_transaction().await?;
+```
+
+## `IN (…)` lists
+
+Helpers make variable-length `IN` lists and the 2,100-parameter limit ergonomic —
+see the `Query` docs on docs.rs for `in_clause`/parameter-expansion helpers.
+
+## Named instances (SQL Browser)
+
+On Windows, a named instance's port is resolved through SQL Browser. Enable
+`sql-browser-tokio` or `sql-browser-smol` and use the `SqlBrowser` extension:
+
+```rust,ignore
+use tiberius::SqlBrowser;
+use tokio::net::TcpStream;
+use tokio_util::compat::TokioAsyncWriteCompatExt;
+
+config.port(1434);
+config.instance_name("INSTANCE");
+let tcp = TcpStream::connect_named(&config).await?;
+let mut client = Client::connect(config, tcp.compat_write()).await?;
+```
+
+## Query cancellation
+
+```rust,ignore
+client.cancel_query().await?; // sends a TDS Attention signal
+```
+
+## Connection pooling
+
+Pooling is delegated to the async pool crates rather than built in. This keeps
+Tiberius runtime-agnostic (a pool has to pick a runtime's timers and tasks) and
+lets the connection lifecycle — sizing, health checks, idle reaping — evolve
+independently of the driver. Use [`bb8`](https://crates.io/crates/bb8),
+[`deadpool`](https://crates.io/crates/deadpool), or
+[`mobc`](https://crates.io/crates/mobc) with a small connection manager.
+
+Because Tiberius has no MARS (one in-flight request per connection), a pool is
+also the natural way to get concurrency: check out a connection per task.
+
+Here is a minimal [`bb8`](https://crates.io/crates/bb8) manager over Tokio:
+
+```rust,ignore
+use bb8::{ManageConnection, Pool};
+use tiberius::{Client, Config};
+use tokio::net::TcpStream;
+use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
+
+struct TiberiusManager {
+ config: Config,
+}
+
+#[async_trait::async_trait]
+impl ManageConnection for TiberiusManager {
+ type Connection = Client>;
+ type Error = tiberius::error::Error;
+
+ async fn connect(&self) -> Result {
+ let tcp = TcpStream::connect(self.config.get_addr()).await?;
+ tcp.set_nodelay(true)?;
+ Client::connect(self.config.clone(), tcp.compat_write()).await
+ }
+
+ async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
+ // Cheap round-trip to confirm the connection is still alive.
+ conn.simple_query("SELECT 1").await?.into_row().await?;
+ Ok(())
+ }
+
+ fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
+ false
+ }
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let mut config = Config::new();
+ config.host("localhost");
+ config.port(1433);
+ config.authentication(tiberius::AuthMethod::sql_server("SA", ""));
+ config.trust_cert(); // don't do this in production
+
+ let pool = Pool::builder()
+ .max_size(16)
+ .build(TiberiusManager { config })
+ .await?;
+
+ // Check out a connection; it returns to the pool on drop.
+ let mut conn = pool.get().await?;
+ let row = conn
+ .query("SELECT @P1 AS n", &[&1i32])
+ .await?
+ .into_row()
+ .await?
+ .unwrap();
+ assert_eq!(Some(1i32), row.get("n"));
+ Ok(())
+}
+```
+
+The [`deadpool`](https://crates.io/crates/deadpool) and
+[`mobc`](https://crates.io/crates/mobc) integrations follow the same shape —
+implement their manager trait with the `connect`/`is_valid` logic above.
+
+## Error handling
+
+All fallible calls return `tiberius::Result` (`Err` is [`tiberius::error::Error`]).
+Server-side errors surface as `Error::Server(TokenError { .. })` carrying the SQL
+Server error code, state, class, message, procedure and line.
+
+[`Client`]: https://docs.rs/tiberius/latest/tiberius/struct.Client.html
+[`Config`]: https://docs.rs/tiberius/latest/tiberius/struct.Config.html
+[`Query`]: https://docs.rs/tiberius/latest/tiberius/struct.Query.html
+[`Command`]: https://docs.rs/tiberius/latest/tiberius/struct.Command.html
+[`tiberius::error::Error`]: https://docs.rs/tiberius/latest/tiberius/error/enum.Error.html
diff --git a/docs/TDS_COMPATIBILITY.md b/docs/TDS_COMPATIBILITY.md
new file mode 100644
index 000000000..2349b86aa
--- /dev/null
+++ b/docs/TDS_COMPATIBILITY.md
@@ -0,0 +1,98 @@
+# TDS Protocol Compatibility
+
+This document tracks `tiberius`'s coverage of the Microsoft Tabular Data
+Stream (MS-TDS) protocol. It is a maintained snapshot — code references may
+drift; treat the ratings as the source of truth and file an issue for
+discrepancies.
+
+Legend: ✅ full · 🟡 partial · ❌ missing · N/A not applicable.
+
+## Protocol-version support
+
+| TDS version | SQL Server | Rating | Notes |
+|---|---|---|---|
+| **8.0** | 2022 | ✅ Full | TLS-before-prelogin "strict" mode (`EncryptionLevel::Strict`), `tds/8.0` ALPN, and client-certificate (mutual-TLS) login on native-tls + rustls. 8.0 reuses 7.4 tokens over mandatory TLS; the only backend caveat is that opentls cannot advertise ALPN (see below). |
+| **7.4** | 2012–2019 | ✅ Full | Login (`FeatureLevel::SqlServerN`), routing ENVCHANGE, `fReadOnlyIntent`, FedAuth prelogin option + FeatureExt, FEATUREEXTACK, FEDAUTHINFO (0xEE), SESSIONSTATE (0xE4). The only deferral is *transparent reconnect* — SESSIONSTATE is decoded and stored, but not yet replayed to silently re-establish a dropped session. |
+| **7.3 A/B** | 2008 / R2 | ✅ Full (`tds73`) | date / time / datetime2 / datetimeoffset types, NBCROW. |
+| **7.2** | 2005 | ✅ Full | PLP / varchar(max), XML, MARS / transaction-descriptor headers, SQL_VARIANT (read + write), UDT (0xF0) raw-value decode. |
+| **7.1** | 2000 | ✅ Full | Collation, UCS-2 strings, `n`-prefixed var-len types, LOGIN7 layout. |
+| **7.0** | 7.0 | ❌ None | Legacy fixed non-nullable types unsupported; the client only negotiates 7.4. Not a target. |
+
+There is **no Microsoft "TDS 6.0"** — the Microsoft protocol line is
+7.0 → 7.1 → 7.2 → 7.3A → 7.3B → 7.4 → 8.0. Sybase-era TDS 4.2/5.0 predate
+MS-TDS and are a separate protocol lineage, out of scope for this driver.
+TDS 8.0 has no distinct LOGIN7 version — it reuses 7.4 tokens over mandatory
+TLS, so "8.0" is a transport/ALPN distinction.
+
+**Summary:** TDS 7.1 through 8.0 are fully supported. The only remaining
+elements are optional and rarely used: transparent session recovery (the
+SESSIONSTATE token is decoded and stored, just not replayed on reconnect),
+CLR UDT *object* deserialization (raw bytes are surfaced), and `tds/8.0` ALPN
+on the opentls backend (an upstream-crate limitation) — none of which any
+standard query, bulk-load, RPC, or transaction path depends on.
+
+## Feature matrix
+
+### Client → server messages
+| Message | Status |
+|---|---|
+| PRELOGIN (0x12) | ✅ (VERSION/ENCRYPTION/INSTOPT/THREADID/MARS emitted; server options decoded and INSTOPT validated) |
+| LOGIN7 (0x10) | ✅ |
+| SQL Batch (0x01) | ✅ |
+| RPC request (0x03) | ✅ by-ID procs and named procs, incl. OUT params and table-valued parameters (#328) |
+| Bulk load (0x07) | ✅ whole-table and column-list |
+| SSPI (0x11) | ✅ |
+| FedAuth token | ✅ via LOGIN7 FeatureExt |
+| Attention / cancel (0x06) | ✅ (`Client::cancel_query`) |
+| Transaction Manager request (0x14) | ✅ (`begin`/`commit`/`rollback` with isolation levels) |
+
+### Server → client tokens
+| Token | Status |
+|---|---|
+| COLMETADATA, ROW, NBCROW, DONE/PROC/INPROC | ✅ |
+| ENVCHANGE, ERROR/INFO, LOGINACK | ✅ |
+| RETURNVALUE, RETURNSTATUS, ORDER, SSPI | ✅ |
+| FEATUREEXTACK | ✅ |
+| ALTMETADATA / ALTROW (compute-by) | ✅ |
+| COLINFO (0xA5) | ✅ |
+| TABNAME (0xA4) | ✅ |
+| FEDAUTHINFO (0xEE) | ✅ (STSURL + SPN surfaced for AAD flows) |
+| SESSIONSTATE (0xE4) | ✅ decoded + stored (transparent-reconnect replay not yet implemented) |
+
+### Data types
+| Type | Status |
+|---|---|
+| Fixed-len ints/bit/float/money/datetime(4) | ✅ |
+| Nullable var-len (Intn/Bitn/Floatn/Guid/Money/Datetimen/Decimaln/Numericn) | ✅ |
+| Char/binary + collation, Text/NText/Image | ✅ |
+| PLP (max types), XML | ✅ |
+| date / time / datetime2 / datetimeoffset (7.3) | ✅ (`tds73`) |
+| Numeric / Decimal (incl. automatic scale rescaling of params) | ✅ |
+| SQL_VARIANT (0x62) | ✅ read + write |
+| UDT (0xF0) | ✅ raw-value decode (surfaced as bytes; CLR object deserialization out of scope) |
+
+### Encryption & auth
+| Feature | Status |
+|---|---|
+| Encryption NotSupported / Off / On / Required | ✅ |
+| Strict (TDS 8.0, TLS-first) | ✅ (`tds80`) |
+| `tds/8.0` ALPN | 🟡 native-tls + rustls ✅, opentls ❌ (backend cannot advertise ALPN) |
+| ENCRYPT_CLIENT_CERT (mutual TLS) | ✅ (`Config::client_certificate` / `client_certificate_pkcs12`) |
+| SQL auth (zeroized) | ✅ |
+| Windows NTLM/SSPI (Windows `winauth`; Unix `sspi-rs`) | ✅ |
+| Kerberos/GSSAPI (Unix `integrated-auth-gssapi`) | ✅ |
+| AAD / federated token | ✅ (`AuthMethod::aad_token`) |
+
+## Remaining optional items
+
+None of the following block standard operation; they are tracked for
+completeness and would be additive, backward-compatible features:
+
+- **Transparent session recovery** — the SESSIONSTATE token is already decoded
+ and stored; replaying it to silently re-establish a dropped connection is the
+ remaining step.
+- **CLR UDT object deserialization** — UDT values are decoded to raw bytes;
+ interpreting them into CLR objects (e.g. `geometry`) is left to the caller.
+- **opentls `tds/8.0` ALPN** — verified infeasible with `opentls` 0.2.1's public
+ API (no ALPN setter; the wrapped `SslConnector` is private). Use native-tls or
+ rustls for TDS 8.0 strict mode. Tracked upstream against the `opentls` crate.
diff --git a/examples/aad-auth.rs b/examples/aad-auth.rs
index 8ef41c472..ebc23165b 100644
--- a/examples/aad-auth.rs
+++ b/examples/aad-auth.rs
@@ -8,41 +8,32 @@
//! - CLIENT_SECRET: service principal secret;
//! - TENANT_ID: tenant id of service principal and sql instance;
//! - SERVER: SQL server URI
-use azure_identity::client_credentials_flow;
-use oauth2::{ClientId, ClientSecret};
-use std::{env, sync::Arc};
+use azure_core::credentials::{Secret, TokenCredential};
+use azure_identity::ClientSecretCredential;
+use std::env;
use tiberius::{AuthMethod, Client, Config, Query};
use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- // following code will retrive token with AAD Service Principal Auth
- let client_id =
- ClientId::new(env::var("CLIENT_ID").expect("Missing CLIENT_ID environment variable."));
- let client_secret = ClientSecret::new(
- env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET environment variable."),
- );
+ let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID environment variable.");
+ let client_secret =
+ env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET environment variable.");
let tenant_id = env::var("TENANT_ID").expect("Missing TENANT_ID environment variable.");
- let client = Arc::new(reqwest::Client::new());
- // This will give you the final token to use in authorization.
- let token = client_credentials_flow::perform(
- client,
- &client_id,
- &client_secret,
- &["https://management.azure.com/"],
- &tenant_id,
- )
- .await?;
+ let credential =
+ ClientSecretCredential::new(&tenant_id, client_id, Secret::new(client_secret), None)?;
+
+ let token = credential
+ .get_token(&["https://database.windows.net/.default"], None)
+ .await?;
let mut config = Config::new();
let server = env::var("SERVER").expect("Missing SERVER environment variable.");
config.host(server);
config.port(1433);
- config.authentication(AuthMethod::AADToken(
- token.access_token().secret().to_string(),
- ));
+ config.authentication(AuthMethod::aad_token(token.token.secret()));
config.trust_cert();
let tcp = TcpStream::connect(config.get_addr()).await?;
diff --git a/examples/async-std.rs b/examples/async-std.rs
deleted file mode 100644
index 88fcf1c8d..000000000
--- a/examples/async-std.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use async_std::net::TcpStream;
-use once_cell::sync::Lazy;
-use std::env;
-use tiberius::{Client, Config};
-
-static CONN_STR: Lazy = Lazy::new(|| {
- env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or_else(|_| {
- "server=tcp:localhost,1433;IntegratedSecurity=true;TrustServerCertificate=true".to_owned()
- })
-});
-
-#[cfg(not(all(windows, feature = "sql-browser-async-std")))]
-#[async_std::main]
-async fn main() -> anyhow::Result<()> {
- let config = Config::from_ado_string(&CONN_STR)?;
-
- let tcp = TcpStream::connect(config.get_addr()).await?;
- tcp.set_nodelay(true)?;
-
- let mut client = Client::connect(config, tcp).await?;
-
- let stream = client.query("SELECT @P1", &[&1i32]).await?;
- let row = stream.into_row().await?.unwrap();
-
- println!("{:?}", row);
- assert_eq!(Some(1), row.get(0));
-
- Ok(())
-}
-
-#[cfg(all(windows, feature = "sql-browser-async-std"))]
-#[async_std::main]
-async fn main() -> anyhow::Result<()> {
- use tiberius::SqlBrowser;
-
- let config = Config::from_ado_string(&CONN_STR)?;
-
- let tcp = TcpStream::connect_named(&config).await?;
- tcp.set_nodelay(true)?;
-
- let mut client = Client::connect(config, tcp).await?;
-
- let stream = client.query("SELECT @P1", &[&1i32]).await?;
- let row = stream.into_row().await?.unwrap();
-
- println!("{:?}", row);
- assert_eq!(Some(1), row.get(0));
-
- Ok(())
-}
diff --git a/examples/named-pipes.rs b/examples/named-pipes.rs
new file mode 100644
index 000000000..fb135dbb2
--- /dev/null
+++ b/examples/named-pipes.rs
@@ -0,0 +1,43 @@
+//! Connecting to SQL Server over a Windows named pipe.
+//!
+//! SQL Server exposes a named pipe endpoint (by default
+//! `\\.\pipe\sql\query` for the default instance). Because a named pipe
+//! implements `AsyncRead`/`AsyncWrite`, it can be handed to
+//! [`Client::connect`] exactly like a TCP stream once it is wrapped with the
+//! `tokio-util` compatibility layer.
+//!
+//! Named pipes are a Windows-only transport, so the real example is compiled
+//! only on Windows; on other platforms `main` panics with an unsupported
+//! message. See tiberius issues #131 and #53 for background.
+
+#[cfg(windows)]
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ use tiberius::{AuthMethod, Client, Config};
+ use tokio::net::windows::named_pipe::ClientOptions;
+ use tokio_util::compat::TokioAsyncWriteCompatExt;
+
+ // The default named pipe for a default SQL Server instance. A named
+ // instance uses `\\.\pipe\MSSQL$\sql\query`.
+ const PIPE_NAME: &str = r"\\.\pipe\sql\query";
+
+ let mut config = Config::new();
+ config.authentication(AuthMethod::Integrated);
+ config.trust_cert();
+
+ let pipe = ClientOptions::new().open(PIPE_NAME)?;
+ let mut client = Client::connect(config, pipe.compat_write()).await?;
+
+ let stream = client.query("SELECT @P1", &[&1i32]).await?;
+ let row = stream.into_row().await?.unwrap();
+
+ println!("{row:?}");
+ assert_eq!(Some(1), row.get(0));
+
+ Ok(())
+}
+
+#[cfg(not(windows))]
+fn main() {
+ panic!("Named pipe connections are only supported on Windows.");
+}
diff --git a/runtimes-macro/Cargo.toml b/runtimes-macro/Cargo.toml
index 6bf114b2a..4b0f68311 100644
--- a/runtimes-macro/Cargo.toml
+++ b/runtimes-macro/Cargo.toml
@@ -1,14 +1,19 @@
+# Test-only helper crate (dev-dependency, path-only): the `#[test_on_runtimes]`
+# attribute that runs each integration test on tokio and smol. Not shipped, not
+# published. `license`/`publish` set so the cargo-deny license gate is satisfied
+# and it can never be accidentally published.
[package]
name = "runtimes-macro"
version = "0.1.0"
authors = ["Eric Sheppard "]
-edition = "2018"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+publish = false
[lib]
proc-macro = true
[dependencies]
quote = "1"
-syn = "1"
-darling = "0.14"
+syn = { version = "2", features = ["full"] }
proc-macro2 = "1"
diff --git a/runtimes-macro/src/lib.rs b/runtimes-macro/src/lib.rs
index cc1d2cabc..94a6b0464 100644
--- a/runtimes-macro/src/lib.rs
+++ b/runtimes-macro/src/lib.rs
@@ -1,50 +1,70 @@
+//! Internal test-only proc-macro for tiberius.
+//!
+//! `#[test_on_runtimes]` takes an `async fn(client) -> Result<()>` and generates
+//! one integration test per supported async runtime, so every test proves the
+//! (runtime-independent) driver works on each of them. Currently: **tokio** and
+//! **smol**.
extern crate proc_macro;
-use darling::FromMeta;
-#[derive(Debug, FromMeta)]
-struct MacroArgs {
- #[darling(default)]
- connection_string: Option,
+use proc_macro::TokenStream;
+use quote::{format_ident, quote};
+use syn::{parse_macro_input, ItemFn, LitStr};
+
+/// Optional `connection_string = "IDENT"` attribute argument naming the `&str`
+/// constant to connect with. Defaults to `CONN_STR`.
+struct Args {
+ conn_str: String,
}
-#[proc_macro_attribute]
-pub fn test_on_runtimes(
- args: proc_macro::TokenStream,
- input: proc_macro::TokenStream,
-) -> proc_macro::TokenStream {
- let attr_args = syn::parse_macro_input!(args as syn::AttributeArgs);
-
- let args = match MacroArgs::from_list(&attr_args) {
- Ok(v) => v,
- Err(e) => {
- return proc_macro::TokenStream::from(e.write_errors());
- }
- };
+impl syn::parse::Parse for Args {
+ fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result {
+ let mut conn_str = String::from("CONN_STR");
- let func = syn::parse_macro_input!(input as syn::ItemFn);
+ if !input.is_empty() {
+ let ident: syn::Ident = input.parse()?;
+ if ident != "connection_string" {
+ return Err(syn::Error::new(
+ ident.span(),
+ "expected `connection_string = \"...\"`",
+ ));
+ }
+ input.parse::()?;
+ let lit: LitStr = input.parse()?;
+ conn_str = lit.value();
+ }
- let conn_str_ident_str = args.connection_string.unwrap_or_else(|| "CONN_STR".into());
+ Ok(Args { conn_str })
+ }
+}
- let conn_str_ident =
- proc_macro2::Ident::new(&conn_str_ident_str, proc_macro2::Span::call_site());
+#[proc_macro_attribute]
+pub fn test_on_runtimes(args: TokenStream, input: TokenStream) -> TokenStream {
+ let args = parse_macro_input!(args as Args);
+ let func = parse_macro_input!(input as ItemFn);
+ let conn_str_ident = format_ident!("{}", args.conn_str);
let func_name = func.sig.ident.clone();
- let async_std_test = quote::format_ident!("{}_{}", func_name, "async_std");
- let tokio_test = quote::format_ident!("{}_{}", func_name, "tokio");
+ let tokio_test = format_ident!("{}_tokio", func_name);
+ let smol_test = format_ident!("{}_smol", func_name);
- let tokens = quote::quote! {
+ let tokens = quote! {
#func
#[test]
- fn #async_std_test()-> Result<()> {
+ fn #tokio_test() -> Result<()> {
LOGGER_SETUP.call_once(|| {
- env_logger::init();
+ let _ = env_logger::builder().is_test(true).try_init();
});
- async_std::task::block_on(async {
+
+ use tokio_util::compat::TokioAsyncWriteCompatExt;
+
+ let rt = tokio::runtime::Runtime::new()?;
+
+ rt.block_on(async {
let config = tiberius::Config::from_ado_string(conn_str_ident)?;
- let tcp = async_std::net::TcpStream::connect(config.get_addr()).await?;
+ let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
tcp.set_nodelay(true)?;
- let mut client = tiberius::Client::connect(config, tcp).await?;
+ let client = tiberius::Client::connect(config, tcp.compat_write()).await?;
#func_name(client).await?;
Ok(())
@@ -52,19 +72,16 @@ pub fn test_on_runtimes(
}
#[test]
- fn #tokio_test()-> Result<()> {
+ fn #smol_test() -> Result<()> {
LOGGER_SETUP.call_once(|| {
- env_logger::init();
+ let _ = env_logger::builder().is_test(true).try_init();
});
- use tokio_util::compat::TokioAsyncWriteCompatExt;
-
- let mut rt = tokio::runtime::Runtime::new()?;
- rt.block_on(async {
+ smol::block_on(async {
let config = tiberius::Config::from_ado_string(conn_str_ident)?;
- let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
+ let tcp = smol::net::TcpStream::connect(config.get_addr()).await?;
tcp.set_nodelay(true)?;
- let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
+ let client = tiberius::Client::connect(config, tcp).await?;
#func_name(client).await?;
Ok(())
@@ -72,5 +89,5 @@ pub fn test_on_runtimes(
}
};
- proc_macro::TokenStream::from(tokens)
+ TokenStream::from(tokens)
}
diff --git a/src/bulk_options.rs b/src/bulk_options.rs
new file mode 100644
index 000000000..45da4b7a5
--- /dev/null
+++ b/src/bulk_options.rs
@@ -0,0 +1,64 @@
+//! Options controlling the behaviour of a bulk-insert (`INSERT BULK`)
+//! operation, mirroring the knobs exposed by `SqlBulkCopyOptions` in
+//! ADO.NET's `SqlBulkCopy`.
+
+use enumflags2::{bitflags, BitFlags};
+
+/// A single bulk-copy option. Combine several with the `|` operator to build a
+/// [`SqlBulkCopyOptions`] set, e.g.
+/// `SqlBulkCopyOption::KeepIdentity | SqlBulkCopyOption::TableLock`.
+///
+/// See the MS docs for `SqlBulkCopyOptions`:
+///
+#[bitflags]
+#[repr(u32)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum SqlBulkCopyOption {
+ /// Preserve source identity values. When not specified, identity values are
+ /// assigned by the destination. Implemented by keeping the identity column
+ /// in the bulk column list (the `INSERT BULK` `WITH (...)` grammar has no
+ /// `KEEP_IDENTITY` keyword), matching ADO.NET's `SqlBulkCopy`.
+ KeepIdentity = 1 << 0,
+ /// Check constraints while data is being inserted. By default, constraints
+ /// are not checked. Emits `CHECK_CONSTRAINTS`.
+ CheckConstraints = 1 << 1,
+ /// Obtain a bulk update lock for the duration of the bulk-copy operation.
+ /// When not specified, row locks are used. Emits `TABLOCK`.
+ TableLock = 1 << 2,
+ /// Preserve null values in the destination table regardless of the settings
+ /// for default values. When not specified, null values are replaced by
+ /// default values where applicable. Emits `KEEP_NULLS`.
+ KeepNulls = 1 << 3,
+ /// Cause the server to fire the insert triggers for the rows being inserted
+ /// into the database. Emits `FIRE_TRIGGERS`.
+ FireTriggers = 1 << 4,
+}
+
+/// A set of [`SqlBulkCopyOption`] flags controlling an `INSERT BULK`.
+///
+/// Build one by combining flags with `|`, or use [`SqlBulkCopyOptions::empty`]
+/// (also the [`Default`]) for no options:
+///
+/// ```
+/// # use tiberius::{SqlBulkCopyOption, SqlBulkCopyOptions};
+/// let opts: SqlBulkCopyOptions =
+/// SqlBulkCopyOption::KeepIdentity | SqlBulkCopyOption::TableLock;
+/// assert!(opts.contains(SqlBulkCopyOption::KeepIdentity));
+/// assert!(SqlBulkCopyOptions::empty().is_empty());
+/// ```
+pub type SqlBulkCopyOptions = BitFlags;
+
+/// The sort order of a column, used as a bulk-insert `ORDER (...)` hint.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum SortOrder {
+ /// Ascending order (`ASC`).
+ Ascending,
+ /// Descending order (`DESC`).
+ Descending,
+}
+
+/// An order hint for a bulk insert: the column name and the [`SortOrder`] the
+/// incoming rows are already sorted by. Passed as a slice, e.g.
+/// `&[("id", SortOrder::Ascending)]`, these become the `ORDER (...)` clause of
+/// the `INSERT BULK` statement.
+pub type ColumnOrderHint<'a> = (&'a str, SortOrder);
diff --git a/src/client.rs b/src/client.rs
index 688721d10..3aaf4b0d0 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1,4 +1,4 @@
-mod auth;
+pub(crate) mod auth;
mod config;
mod connection;
@@ -14,6 +14,8 @@ pub use auth::*;
pub use config::*;
pub(crate) use connection::*;
+use crate::bulk_options::{ColumnOrderHint, SortOrder, SqlBulkCopyOption, SqlBulkCopyOptions};
+use crate::tds::codec::RpcValue;
use crate::tds::stream::ReceivedToken;
use crate::{
result::ExecuteResult,
@@ -21,9 +23,12 @@ use crate::{
codec::{self, IteratorJoin},
stream::{QueryStream, TokenStream},
},
- BulkLoadRequest, ColumnFlag, SqlReadBytes, ToSql,
+ BulkLoadRequest, MetaDataColumn, SqlReadBytes, ToSql,
+};
+use codec::{
+ BatchRequest, ColumnData, IsolationLevel, PacketHeader, RpcParam, RpcProcId, TokenRpcRequest,
+ TransactionManagerRequest,
};
-use codec::{BatchRequest, ColumnData, PacketHeader, RpcParam, RpcProcId, TokenRpcRequest};
use enumflags2::BitFlags;
use futures_util::io::{AsyncRead, AsyncWrite};
use futures_util::stream::TryStreamExt;
@@ -57,6 +62,19 @@ use std::{borrow::Cow, fmt::Debug};
/// # }
/// ```
///
+/// # Cancellation safety
+///
+/// A single [`Client`] drives one connection and one request at a time. If a
+/// `query`/`execute`/`simple_query` future — or the result stream it returns —
+/// is dropped before the request has been sent in full and the response fully
+/// consumed (for example under a `tokio::time::timeout` or a `select!` branch
+/// that loses the race), the connection may be left mid-message and out of sync
+/// with the server. A cancelled *write* is detected and any further use of that
+/// connection fails cleanly; a result stream dropped mid-response cannot be
+/// recovered. In both cases the safe course is to drop the `Client` and open a
+/// new connection (a connection pool should discard the connection on error)
+/// rather than reuse it.
+///
/// [`Config`]: struct.Config.html
#[derive(Debug)]
pub struct Client {
@@ -68,6 +86,11 @@ impl Client {
/// options required to connect to the database using an established
/// tcp connection
///
+ /// Note: `tcp_stream` is a connected stream, so some parts of the `Config`
+ /// (such as multi-subnet failover, which selects between resolved
+ /// addresses) must be handled while establishing that stream, outside of
+ /// this constructor.
+ ///
/// [`Config`]: struct.Config.html
pub async fn connect(config: Config, tcp_stream: S) -> crate::Result> {
Ok(Client {
@@ -89,6 +112,12 @@ impl Client {
/// This API is not quite suitable for dynamic query parameters. In these
/// cases using a [`Query`] object might be easier.
///
+ /// # Errors
+ ///
+ /// Returns an error if the statement cannot be sent, if the server reports
+ /// an error while executing it, or if the connection fails during the
+ /// request.
+ ///
/// # Example
///
/// ```no_run
@@ -148,9 +177,16 @@ impl Client {
/// if fighting too much with the compiler, using a [`Query`] object might be
/// easier.
///
+ /// # Errors
+ ///
+ /// Returns an error if the statement cannot be sent, if the server reports
+ /// an error while executing it, or if the connection fails during the
+ /// request. Note that per-statement server errors may instead surface while
+ /// consuming the returned [`QueryStream`].
+ ///
/// # Example
///
- /// ```
+ /// ```no_run
/// # use tiberius::Config;
/// # use tokio_util::compat::TokioAsyncWriteCompatExt;
/// # use std::env;
@@ -202,9 +238,16 @@ impl Client {
/// Execute multiple queries, delimited with `;` and return multiple result
/// sets; one for each query.
///
+ /// # Errors
+ ///
+ /// Returns an error if the batch cannot be sent, if the server reports an
+ /// error while executing it, or if the connection fails during the request.
+ /// Per-statement server errors may instead surface while consuming the
+ /// returned [`QueryStream`].
+ ///
/// # Example
///
- /// ```
+ /// ```no_run
/// # use tiberius::Config;
/// # use tokio_util::compat::TokioAsyncWriteCompatExt;
/// # use std::env;
@@ -251,13 +294,38 @@ impl Client {
Ok(result)
}
- /// Execute a `BULK INSERT` statement, efficiantly storing a large number of
+ /// Execute a `BULK INSERT` statement, efficiently storing a large number of
/// rows to a specified table. Note: make sure the input row follows the same
/// schema as the table, otherwise calling `send()` will return an error.
///
+ /// This is equivalent to `bulk_insert_columns(table, &["*"])`, inserting into
+ /// all of a table's columns.
+ ///
+ /// # Security
+ ///
+ /// `table` is interpolated **directly** into the SQL batch sent to the
+ /// server. SQL Server does not allow table (or column) identifiers to be
+ /// supplied as bound parameters, so this value cannot be parameterized — it
+ /// becomes part of the SQL text verbatim. The caller MUST therefore pass a
+ /// **trusted, hard-coded or otherwise validated** identifier and MUST NOT
+ /// pass untrusted or user-supplied input, which would open a SQL injection
+ /// vector. As cheap defense-in-depth this method rejects obviously-malformed
+ /// identifiers (NUL/ASCII control characters or an unbalanced `]` bracket),
+ /// but that guard is not a substitute for passing trusted input.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `table` is a malformed identifier, if the query for
+ /// the column metadata and collations fails, or if the server rejects the
+ /// `INSERT BULK` statement. Row-level failures surface later from [`send`] and
+ /// [`finalize`] on the returned request.
+ ///
+ /// [`send`]: BulkLoadRequest::send
+ /// [`finalize`]: BulkLoadRequest::finalize
+ ///
/// # Example
///
- /// ```
+ /// ```no_run
/// # use tiberius::{Config, IntoRow};
/// # use tokio_util::compat::TokioAsyncWriteCompatExt;
/// # use std::env;
@@ -300,41 +368,225 @@ impl Client {
&'a mut self,
table: &'a str,
) -> crate::Result> {
- // Start the bulk request
- self.connection.flush_stream().await?;
-
- // retrieve column metadata from server
- let query = format!("SELECT TOP 0 * FROM {}", table);
-
- let req = BatchRequest::new(query, self.connection.context().transaction_descriptor());
+ self.bulk_insert_columns(table, &["*"]).await
+ }
- let id = self.connection.context_mut().next_packet_id();
- self.connection.send(PacketHeader::batch(id), req).await?;
+ /// Execute a `BULK INSERT` statement, efficiently storing a large number of
+ /// rows to a specified table. Note: make sure the input row follows the same
+ /// schema as the column list, otherwise calling `send()` will return an error.
+ ///
+ /// # Security
+ ///
+ /// Both `table` and the entries of `columns` are interpolated **directly**
+ /// into the SQL batches sent to the server (the `SELECT` used to fetch
+ /// column metadata and the `INSERT BULK` statement). SQL Server does not
+ /// allow identifiers to be supplied as bound parameters, so these values
+ /// cannot be parameterized — they become part of the SQL text verbatim. The
+ /// caller MUST therefore pass **trusted, hard-coded or otherwise validated**
+ /// identifiers and MUST NOT pass untrusted or user-supplied input, which
+ /// would open a SQL injection vector. As cheap defense-in-depth this method
+ /// rejects an obviously-malformed `table` (NUL/ASCII control characters or an
+ /// unbalanced `]` bracket), but that guard is not a substitute for passing
+ /// trusted input.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `table` is a malformed identifier, if the query for
+ /// the column metadata and collations fails, or if the server rejects the
+ /// `INSERT BULK` statement. Row-level failures surface later from [`send`] and
+ /// [`finalize`] on the returned request.
+ ///
+ /// [`send`]: BulkLoadRequest::send
+ /// [`finalize`]: BulkLoadRequest::finalize
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use tiberius::{Config, IntoRow};
+ /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
+ /// # use std::env;
+ /// # #[tokio::main]
+ /// # async fn main() -> Result<(), Box> {
+ /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
+ /// # "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
+ /// # );
+ /// # let config = Config::from_ado_string(&c_str)?;
+ /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
+ /// # tcp.set_nodelay(true)?;
+ /// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
+ /// let create_table = r#"
+ /// CREATE TABLE ##bulk_test_columns (
+ /// id INT IDENTITY PRIMARY KEY,
+ /// foo INT NOT NULL,
+ /// bar FLOAT NOT NULL
+ /// )
+ /// "#;
+ ///
+ /// client.simple_query(create_table).await?;
+ ///
+ /// // Start the bulk insert with the client.
+ /// let mut req = client.bulk_insert_columns("##bulk_test_columns", &["foo", "bar"]).await?;
+ ///
+ /// for (i, j) in [(0i32, 0f64), (1i32, 1f64), (2i32, 2f64)] {
+ /// let row = (i, j).into_row();
+ ///
+ /// // The request will handle flushing to the wire in an optimal way,
+ /// // balancing between memory usage and IO performance.
+ /// req.send(row).await?;
+ /// }
+ ///
+ /// // The request must be finalized.
+ /// let res = req.finalize().await?;
+ /// assert_eq!(3, res.total());
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub async fn bulk_insert_columns<'a>(
+ &'a mut self,
+ table: &'a str,
+ columns: &'a [&'a str],
+ ) -> crate::Result> {
+ self.bulk_insert_with_options(table, columns, SqlBulkCopyOptions::empty(), &[])
+ .await
+ }
- let token_stream = TokenStream::new(&mut self.connection).try_unfold();
+ /// Execute a `BULK INSERT` statement like [`bulk_insert_columns`], with
+ /// additional control over the emitted `WITH (...)` clause.
+ ///
+ /// `options` is a set of [`SqlBulkCopyOption`] flags (combine them with `|`,
+ /// or pass [`SqlBulkCopyOptions::empty`] for none), and `order_hints`
+ /// declares the sort order the incoming rows are already in via an
+ /// `ORDER (...)` clause. Pass `&["*"]` as `columns` to target every column,
+ /// exactly like [`bulk_insert`] does internally.
+ ///
+ /// When `options` is empty and `order_hints` is empty this emits the exact
+ /// same statement as [`bulk_insert_columns`] (no `WITH` clause).
+ ///
+ /// # Security
+ ///
+ /// `table`, every entry of `columns`, and every order-hint column name are
+ /// interpolated **directly** into the SQL batches sent to the server (T-SQL
+ /// does not allow identifiers to be parameterized). The caller MUST pass
+ /// **trusted, hard-coded or otherwise validated** identifiers and MUST NOT
+ /// pass untrusted or user-supplied input. As cheap defense-in-depth this
+ /// method rejects obviously-malformed identifiers — NUL/ASCII control
+ /// characters, an unbalanced `]` bracket, a top-level space, statement-
+ /// breaking punctuation (`;`, quotes, `-`, etc.) and tokens spliced onto a
+ /// closing `]`/`)` — for the table, columns *and* order-hint columns, but
+ /// that guard is not a substitute for passing trusted input.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `table`, a column, or an order-hint column is a
+ /// malformed identifier, if the query for the column metadata and
+ /// collations fails, if the server reports a collation name that is not a
+ /// plain identifier, or if the server rejects the `INSERT BULK` statement.
+ /// Row-level failures surface later from [`send`] and [`finalize`] on the
+ /// returned request.
+ ///
+ /// # Collations
+ ///
+ /// The `INSERT BULK` statement declares each char, varchar, text, nchar,
+ /// nvarchar and ntext column with its own collation (`COLLATE `), as
+ /// SqlClient's `SqlBulkCopy` does, so the server reads the bulk data in the
+ /// column's code page rather than the database default's. The collation
+ /// names come from `sp_tablecollations_100`, run in the same batch as the
+ /// column metadata query (in `tempdb` for a `#` temp table).
+ ///
+ /// [`bulk_insert`]: #method.bulk_insert
+ /// [`bulk_insert_columns`]: #method.bulk_insert_columns
+ /// [`send`]: BulkLoadRequest::send
+ /// [`finalize`]: BulkLoadRequest::finalize
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use tiberius::{Config, IntoRow, SqlBulkCopyOption, SortOrder};
+ /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
+ /// # use std::env;
+ /// # #[tokio::main]
+ /// # async fn main() -> Result<(), Box> {
+ /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
+ /// # "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
+ /// # );
+ /// # let config = Config::from_ado_string(&c_str)?;
+ /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
+ /// # tcp.set_nodelay(true)?;
+ /// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
+ /// let mut req = client
+ /// .bulk_insert_with_options(
+ /// "##bulk_test",
+ /// &["id", "val"],
+ /// SqlBulkCopyOption::KeepIdentity | SqlBulkCopyOption::TableLock,
+ /// &[("id", SortOrder::Ascending)],
+ /// )
+ /// .await?;
+ ///
+ /// for i in [0i32, 1i32, 2i32] {
+ /// req.send((i, i).into_row()).await?;
+ /// }
+ ///
+ /// let res = req.finalize().await?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub async fn bulk_insert_with_options<'a>(
+ &'a mut self,
+ table: &'a str,
+ columns: &'a [&'a str],
+ options: SqlBulkCopyOptions,
+ order_hints: &'a [ColumnOrderHint<'a>],
+ ) -> crate::Result> {
+ // `table` is interpolated directly into the SQL batch (identifiers cannot
+ // be parameterized in T-SQL). Reject obviously-malformed/dangerous input
+ // as cheap defense-in-depth; see the `# Security` note above.
+ validate_bulk_table_identifier(table)?;
- let columns = token_stream
- .try_fold(None, |mut columns, token| async move {
- if let ReceivedToken::NewResultset(metadata) = token {
- columns = Some(metadata.columns.clone());
- };
+ // Each `columns` entry is likewise interpolated directly into the SQL
+ // (both the metadata `SELECT` and the `INSERT BULK` column list), so it
+ // gets the same cheap defense-in-depth guard as `table`.
+ for column in columns {
+ validate_bulk_column_identifier(column)?;
+ }
- Ok(columns)
- })
- .await?;
+ // Order-hint column names are interpolated into the `ORDER (...)` clause,
+ // so they get the exact same guard (and, below, the same bracket
+ // escaping) as the regular column list.
+ for &(column, _) in order_hints {
+ validate_bulk_column_identifier(column)?;
+ }
- // now start bulk upload
- let columns: Vec<_> = columns
- .ok_or_else(|| {
- crate::Error::Protocol("expecting column metadata from query but not found".into())
- })?
+ // Retrieve column metadata from the server, keeping only the updateable
+ // columns as bulk targets (read-only/computed columns are skipped).
+ //
+ // Identity columns are normally read-only (so filtered out, letting the
+ // server assign values), but `KEEP_IDENTITY` is implemented — exactly as
+ // ADO.NET's `SqlBulkCopy` does — by *including* the identity column in
+ // the bulk column list so the caller supplies explicit values; there is
+ // no `KEEP_IDENTITY` keyword in the `INSERT BULK` `WITH (...)` grammar.
+ // So when the flag is set, identity columns are additionally retained.
+ //
+ // The same batch lists the collation of every column, which the
+ // `INSERT BULK` column list declares for character columns.
+ let keep_identity = options.contains(SqlBulkCopyOption::KeepIdentity);
+ let (columns, collations) = self.fetch_column_metadata(table, columns, true).await?;
+ let mut columns: Vec<_> = columns
.into_iter()
- .filter(|column| column.base.flags.contains(ColumnFlag::Updateable))
+ .filter(|column| bulk_column_is_target(&column.base, keep_identity))
.collect();
+ // `text`/`ntext`/`image` columns must carry the destination TableName in
+ // the COLMETADATA we emit for the bulk load (MS-TDS §2.2.7.4). Record the
+ // target table on every column; the encoder only emits it for those
+ // types, so this is a no-op on the wire for all other columns.
+ for column in columns.iter_mut() {
+ column.base.table_name = Some(table.to_string());
+ }
+
+ // now start bulk upload
self.connection.flush_stream().await?;
- let col_data = columns.iter().map(|c| format!("{}", c)).join(", ");
- let query = format!("INSERT BULK {} ({})", table, col_data);
+ let col_data = bulk_column_list(&columns, &collations)?;
+ let query = build_insert_bulk_sql(table, &col_data, options, order_hints);
let req = BatchRequest::new(query, self.connection.context().transaction_descriptor());
let id = self.connection.context_mut().next_packet_id();
@@ -347,22 +599,293 @@ impl Client {
BulkLoadRequest::new(&mut self.connection, columns)
}
+ /// Retrieve the column metadata for a set of columns of a table, including
+ /// the column names, types (with their size, precision and scale) and flags
+ /// such as nullability and whether a column is an identity column.
+ ///
+ /// Pass `&["*"]` as `columns` to return the metadata for every column of the
+ /// table.
+ ///
+ /// # Security
+ ///
+ /// Both `table` and the entries of `columns` are interpolated **directly**
+ /// into the SQL batch sent to the server (`SELECT TOP 0 {columns} FROM
+ /// {table}`). SQL Server does not allow identifiers to be supplied as bound
+ /// parameters, so these values cannot be parameterized — they become part of
+ /// the SQL text verbatim. The caller MUST therefore pass **trusted,
+ /// hard-coded or otherwise validated** identifiers and MUST NOT pass
+ /// untrusted or user-supplied input, which would open a SQL injection
+ /// vector. As cheap defense-in-depth this method rejects obviously-malformed
+ /// identifiers (NUL/ASCII control characters or an unbalanced `]` bracket),
+ /// but that guard is not a substitute for passing trusted input.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `table` is a malformed identifier, if the metadata
+ /// `SELECT` fails, or if the server reports an error while executing it.
+ ///
+ /// ```no_run
+ /// # use tiberius::Config;
+ /// # use tokio_util::compat::TokioAsyncWriteCompatExt;
+ /// # use std::env;
+ /// # #[tokio::main]
+ /// # async fn main() -> Result<(), Box> {
+ /// # let c_str = env::var("TIBERIUS_TEST_CONNECTION_STRING").unwrap_or(
+ /// # "server=tcp:localhost,1433;integratedSecurity=true;TrustServerCertificate=true".to_owned(),
+ /// # );
+ /// # let config = Config::from_ado_string(&c_str)?;
+ /// # let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?;
+ /// # tcp.set_nodelay(true)?;
+ /// # let mut client = tiberius::Client::connect(config, tcp.compat_write()).await?;
+ /// let meta = client.column_metadata("some_table", &["*"]).await?;
+ /// assert!(meta[0].base().is_identity());
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub async fn column_metadata(
+ &mut self,
+ table: &str,
+ columns: &[&str],
+ ) -> crate::Result>> {
+ // `table` and each `column` are interpolated directly into the SQL
+ // batch below (identifiers cannot be parameterized in T-SQL). Reject
+ // obviously-malformed/dangerous input as cheap defense-in-depth; see the
+ // `# Security` note above.
+ validate_bulk_table_identifier(table)?;
+ for column in columns {
+ validate_bulk_column_identifier(column)?;
+ }
+
+ Ok(self.fetch_column_metadata(table, columns, false).await?.0)
+ }
+
+ /// Fetch the metadata of `columns` of `table` like [`column_metadata`],
+ /// and with `collations` also the `(column name, collation name)` of
+ /// every column of the table from `sp_tablecollations_100` (see
+ /// [`table_collations_sql`]), in the same batch. Without `collations` the
+ /// list is empty.
+ ///
+ /// `table` and `columns` must already be validated.
+ ///
+ /// [`column_metadata`]: #method.column_metadata
+ async fn fetch_column_metadata(
+ &mut self,
+ table: &str,
+ columns: &[&str],
+ collations: bool,
+ ) -> crate::Result<(Vec>, Vec<(String, Option)>)> {
+ self.connection.flush_stream().await?;
+
+ // Ask the server for the column layout without returning any rows.
+ let columns = columns.join(", ");
+ let mut query = format!("SELECT TOP 0 {columns} FROM {table}");
+ if collations {
+ let version = self.connection.context().version();
+ query.push_str("; ");
+ query.push_str(&table_collations_sql(table, version));
+ }
+
+ let req = BatchRequest::new(query, self.connection.context().transaction_descriptor());
+ let id = self.connection.context_mut().next_packet_id();
+ self.connection.send(PacketHeader::batch(id), req).await?;
+
+ let token_stream = TokenStream::new(&mut self.connection).try_unfold();
+
+ // The `SELECT TOP 0` result set comes first and has no rows; every
+ // row is one of the collation list.
+ let (columns, collations) = token_stream
+ .try_fold(
+ (None, Vec::new()),
+ |(mut columns, mut collations), token| async move {
+ match token {
+ ReceivedToken::NewResultset(metadata) if columns.is_none() => {
+ columns = Some(metadata.columns.clone());
+ }
+ ReceivedToken::Row(row) => {
+ let string = |i| match row.get(i) {
+ Some(ColumnData::String(s)) => s.as_ref().map(|s| s.to_string()),
+ _ => None,
+ };
+ if let Some(name) = string(1) {
+ collations.push((name, string(3)));
+ }
+ }
+ _ => {}
+ }
+
+ Ok((columns, collations))
+ },
+ )
+ .await?;
+
+ let columns = columns.ok_or_else(|| {
+ crate::Error::Protocol("expecting column metadata from query but not found".into())
+ })?;
+
+ // Own the column names so the returned metadata is not tied to the
+ // lifetime of the token stream.
+ let columns = columns
+ .into_iter()
+ .map(|c| MetaDataColumn {
+ base: c.base,
+ col_name: std::borrow::Cow::Owned(c.col_name.into_owned()),
+ })
+ .collect();
+
+ Ok((columns, collations))
+ }
+
+ /// Sends a TDS Attention signal to the server (packet type `0x06`,
+ /// MS-TDS section 2.2.1.6) to cancel the request that is currently in
+ /// flight on this connection, and drains the acknowledging token stream so
+ /// the connection can be reused for further queries.
+ ///
+ /// The server responds to the Attention signal by aborting the running
+ /// batch or RPC and returning a `DONE` token with the `DONE_ATTN` status
+ /// bit set. This method waits for that acknowledgement before returning,
+ /// discarding any remaining rows or tokens from the cancelled request.
+ ///
+ /// # Query cancellation and futures
+ ///
+ /// Dropping a [`query`], [`execute`] or [`simple_query`] future (for
+ /// example when a `tokio::time::timeout` elapses or a `select!` branch is
+ /// cancelled) stops the client from polling the stream, but it does *not*
+ /// tell the server to stop working on the request. To actually cancel the
+ /// in-flight work on the server, keep the [`Client`] and call
+ /// `cancel_query` on it. Because `cancel_query` borrows the client
+ /// mutably, it can only be issued once the borrowing result stream has
+ /// been dropped — typically from a separate task holding the client, or
+ /// after a cancelled/timed-out future has released its borrow.
+ ///
+ /// [`query`]: #method.query
+ /// [`execute`]: #method.execute
+ /// [`simple_query`]: #method.simple_query
+ pub async fn cancel_query(&mut self) -> crate::Result<()> {
+ self.connection.cancel_request().await?;
+ Ok(())
+ }
+
/// Closes this database connection explicitly.
pub async fn close(self) -> crate::Result<()> {
self.connection.close().await
}
+ /// Begins a new transaction using a Transaction Manager request
+ /// (`TM_BEGIN_XACT`, MS-TDS 2.2.6.8) instead of a `BEGIN TRAN` T-SQL
+ /// batch.
+ ///
+ /// On success the server replies with a `BeginTransaction` environment
+ /// change token whose descriptor is stored in the connection context and
+ /// automatically attached to subsequent requests, scoping them to the
+ /// transaction. Commit the work with [`commit_transaction`] or discard it
+ /// with [`rollback_transaction`].
+ ///
+ /// The transaction uses the server's default isolation level. Use
+ /// [`begin_transaction_with_isolation`] to request a specific one.
+ ///
+ /// [`commit_transaction`]: #method.commit_transaction
+ /// [`rollback_transaction`]: #method.rollback_transaction
+ /// [`begin_transaction_with_isolation`]: #method.begin_transaction_with_isolation
+ pub async fn begin_transaction(&mut self) -> crate::Result<()> {
+ self.begin_transaction_with_isolation(IsolationLevel::Unspecified)
+ .await
+ }
+
+ /// Begins a new transaction with an explicit isolation level using a
+ /// Transaction Manager request (`TM_BEGIN_XACT`, MS-TDS 2.2.6.8).
+ ///
+ /// See [`begin_transaction`] for details on transaction scoping.
+ ///
+ /// [`begin_transaction`]: #method.begin_transaction
+ pub async fn begin_transaction_with_isolation(
+ &mut self,
+ isolation_level: IsolationLevel,
+ ) -> crate::Result<()> {
+ let req = TransactionManagerRequest::begin(
+ self.connection.context().transaction_descriptor(),
+ isolation_level,
+ "",
+ );
+
+ self.send_transaction_manager_request(req).await
+ }
+
+ /// Commits the active transaction using a Transaction Manager request
+ /// (`TM_COMMIT_XACT`, MS-TDS 2.2.6.8).
+ ///
+ /// After a successful commit the connection is no longer scoped to a
+ /// transaction.
+ pub async fn commit_transaction(&mut self) -> crate::Result<()> {
+ let req = TransactionManagerRequest::commit(
+ self.connection.context().transaction_descriptor(),
+ "",
+ );
+
+ self.send_transaction_manager_request(req).await
+ }
+
+ /// Rolls back the active transaction using a Transaction Manager request
+ /// (`TM_ROLLBACK_XACT`, MS-TDS 2.2.6.8).
+ ///
+ /// After a successful rollback the connection is no longer scoped to a
+ /// transaction.
+ pub async fn rollback_transaction(&mut self) -> crate::Result<()> {
+ let req = TransactionManagerRequest::rollback(
+ self.connection.context().transaction_descriptor(),
+ "",
+ );
+
+ self.send_transaction_manager_request(req).await
+ }
+
+ /// Creates a named savepoint in the active transaction using a Transaction
+ /// Manager request (`TM_SAVE_XACT`, MS-TDS 2.2.6.8).
+ ///
+ /// The savepoint can later be targeted by a T-SQL `ROLLBACK TRANSACTION
+ /// ` to undo work performed after it while keeping the surrounding
+ /// transaction open.
+ pub async fn save_transaction<'a>(
+ &mut self,
+ name: impl Into>,
+ ) -> crate::Result<()> {
+ let req = TransactionManagerRequest::save(
+ self.connection.context().transaction_descriptor(),
+ name,
+ );
+
+ self.send_transaction_manager_request(req).await
+ }
+
+ async fn send_transaction_manager_request(
+ &mut self,
+ req: TransactionManagerRequest<'_>,
+ ) -> crate::Result<()> {
+ self.connection.flush_stream().await?;
+
+ let id = self.connection.context_mut().next_packet_id();
+ self.connection
+ .send(PacketHeader::transaction_manager(id), req)
+ .await?;
+
+ // The server responds with a DONE token (plus an ENVCHANGE token that
+ // the token stream applies to the connection context, updating the
+ // active transaction descriptor).
+ TokenStream::new(&mut self.connection).flush_done().await?;
+
+ Ok(())
+ }
+
pub(crate) fn rpc_params<'a>(query: impl Into>) -> Vec> {
vec![
RpcParam {
name: Cow::Borrowed("stmt"),
flags: BitFlags::empty(),
- value: ColumnData::String(Some(query.into())),
+ value: RpcValue::Scalar(ColumnData::String(Some(query.into()))),
},
RpcParam {
name: Cow::Borrowed("params"),
flags: BitFlags::empty(),
- value: ColumnData::I32(Some(0)),
+ value: RpcValue::Scalar(ColumnData::I32(Some(0))),
},
]
}
@@ -388,12 +911,12 @@ impl Client {
rpc_params.push(RpcParam {
name: Cow::Owned(format!("@P{}", i + 1)),
flags: BitFlags::empty(),
- value: param,
+ value: RpcValue::Scalar(param),
});
}
if let Some(params) = rpc_params.iter_mut().find(|x| x.name == "params") {
- params.value = ColumnData::String(Some(param_str.into()));
+ params.value = RpcValue::Scalar(ColumnData::String(Some(param_str.into())));
}
let req = TokenRpcRequest::new(
@@ -407,4 +930,1089 @@ impl Client {
Ok(())
}
+
+ /// Sends a named-procedure RPC request with the given parameters. The caller
+ /// is responsible for flushing the connection beforehand and for consuming
+ /// the resulting token stream.
+ pub(crate) async fn rpc_run_command<'a, 'b>(
+ &'a mut self,
+ command_name: Cow<'b, str>,
+ rpc_params: Vec>,
+ ) -> crate::Result<()>
+ where
+ 'a: 'b,
+ {
+ let req = TokenRpcRequest::new(
+ command_name,
+ rpc_params,
+ self.connection.context().transaction_descriptor(),
+ );
+
+ let id = self.connection.context_mut().next_packet_id();
+ self.connection.send(PacketHeader::rpc(id), req).await?;
+
+ Ok(())
+ }
+
+ /// Runs a batch query solely to retrieve its column metadata. Used to
+ /// resolve the column layout of a table-valued parameter type.
+ pub(crate) async fn query_run_for_metadata<'b>(
+ &mut self,
+ query: String,
+ ) -> crate::Result