diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 390041d2c..88668400d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,7 +38,65 @@ jobs:
- 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 install -y openssl libkrb5-dev
+ - name: Check on MSRV
+ run: cargo check --features all
+
+ 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 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:
@@ -109,7 +167,73 @@ jobs:
- name: Run tests
run: cargo test ${{matrix.features}}
- cargo-test-windows:
+ integration-linux-next:
+ name: linux (SQL 2025, experimental)
+ if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/qa')
+ runs-on: ubuntu-latest
+ # Newest server; kept non-blocking so a not-yet-published image or a
+ # server-side behaviour change on the bleeding edge cannot red-wall qa.
+ continue-on-error: true
+ 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
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+ - name: Start SQL Server 2025
+ run: DOCKER_BUILDKIT=1 docker compose -f docker-compose.yml up -d mssql-2025
+ - name: Install dependencies
+ run: sudo apt-get install -y openssl libkrb5-dev
+ - name: Wait for SQL Server
+ # Authenticated readiness (not just an open port) — see the smoke job.
+ 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-2025 || true
+ exit 1
+ - name: Run tests
+ run: cargo test --features=all
+
+ integration-macos:
+ name: macos build + unit tests
+ if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/qa')
+ runs-on: macos-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ features:
+ - "--no-default-features --features=rustls,chrono,time,tds73,sql-browser-tokio,sql-browser-smol,rust_decimal,bigdecimal"
+ - "--no-default-features --features=vendored-openssl"
+ 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
+ # No live SQL Server on hosted macOS runners; this verifies the crate
+ # builds and its unit tests pass on macOS/Apple targets.
+ - name: Build
+ run: cargo build ${{ matrix.features }}
+ - name: Run library unit tests
+ run: cargo test --lib ${{ 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:
@@ -216,42 +340,3 @@ jobs:
- name: Run normal tests
shell: powershell
run: cargo test ${{matrix.features}}
-
- cargo-test-macos:
- runs-on: macos-26-intel
-
- strategy:
- fail-fast: false
- matrix:
- 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"
-
- 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
- shell: bash
- 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 }}
-
- # macOS runners can't host the Linux SQL Server container, so this lane is
- # compile + library unit tests only (no server-dependent integration tests,
- # which run on the Linux and Windows lanes).
- - name: Build
- run: cargo build ${{matrix.features}}
-
- - name: Run library unit tests
- run: cargo test --lib ${{matrix.features}}
diff --git a/Cargo.toml b/Cargo.toml
index 173d75522..33b8ccb32 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,20 +13,15 @@ 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"]
-
[[test]]
path = "tests/named-instance-tokio.rs"
name = "named-instance-tokio"
@@ -37,16 +32,20 @@ path = "tests/named-instance-smol.rs"
name = "named-instance-smol"
required-features = ["sql-browser-smol"]
+[[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"
@@ -54,14 +53,15 @@ uuid = "1.0"
zeroize = "1.8.2"
[target.'cfg(windows)'.dependencies]
-winauth = { version = "0.0.4", optional = true }
+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]
@@ -74,11 +74,6 @@ version = "0.7"
features = ["compat"]
optional = true
-[dependencies.async-std]
-version = "1"
-optional = true
-features = ["attributes"]
-
[dependencies.chrono]
version = "0.4"
optional = true
@@ -102,20 +97,25 @@ 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]
@@ -157,45 +157,59 @@ 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.11"
-azure_identity = "0.20.0"
-url = "2.2.2"
-reqwest = "0.12"
+azure_identity = "1.0"
+azure_core = "1"
+url = "2.5"
+reqwest = "0.13"
paste = "1.0"
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"]
@@ -203,3 +217,10 @@ bigdecimal = ["bigdecimal_"]
rustls = ["tokio-rustls", "tokio-util", "rustls-native-certs", "rustls-webpki"]
native-tls = ["async-native-tls"]
vendored-openssl = ["opentls"]
+# 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/deny.toml b/deny.toml
index d384d581a..cefa0e485 100644
--- a/deny.toml
+++ b/deny.toml
@@ -6,19 +6,11 @@
[advisories]
yanked = "deny"
ignore = [
- # async-std is discontinued upstream. In tiberius it is reachable ONLY through
- # the opt-in `sql-browser-async-std` feature (and dev-deps); it is not part of
- # the default build. Tracked for migration to smol/tokio.
- { id = "RUSTSEC-2025-0052", reason = "async-std: opt-in `sql-browser-async-std` feature + dev-deps only; not in the default shipped graph" },
-
# 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-0384", reason = "instant: transitive dev-dependency only; not shipped" },
{ id = "RUSTSEC-2024-0436", reason = "paste: dev/test only (tests/bulk.rs + azure_identity example); not shipped" },
- { id = "RUSTSEC-2026-0174", reason = "http-types: dev-only via azure_identity in the aad-auth example; not shipped" },
- { id = "RUSTSEC-2026-0275", reason = "azure_core: dev-only via azure_identity in the aad-auth example; not shipped (the crate never handles AAD tokens itself)" },
]
[bans]
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
new file mode 100644
index 000000000..ecc73940c
--- /dev/null
+++ b/docs/GUIDE.md
@@ -0,0 +1,314 @@
+# 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::TableValueRow;
+
+#[derive(TableValueRow)]
+struct Item {
+ #[colname = "Id"] id: i32,
+ #[colname = "Name"] name: String,
+}
+```
+
+## 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 e280a8b94..413f593bc 100644
--- a/examples/aad-auth.rs
+++ b/examples/aad-auth.rs
@@ -8,8 +8,9 @@
//! - 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 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;
@@ -21,23 +22,18 @@ async fn main() -> anyhow::Result<()> {
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());
- 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_owned(),
- ));
+ config.authentication(AuthMethod::AADToken(token.token.secret().to_owned()));
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/client.rs b/src/client.rs
index c31ff2c38..244a3e22e 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -14,6 +14,7 @@ pub use auth::*;
pub use config::*;
pub(crate) use connection::*;
+use crate::tds::codec::RpcValue;
use crate::tds::stream::ReceivedToken;
use crate::{
result::ExecuteResult,
@@ -21,9 +22,12 @@ use crate::{
codec::{self, IteratorJoin},
stream::{QueryStream, TokenStream},
},
- BulkLoadRequest, ColumnFlag, SqlReadBytes, ToSql,
+ BulkLoadRequest, ColumnFlag, 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 +61,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 +85,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 {
@@ -357,15 +379,68 @@ impl Client {
table: &'a str,
columns: &'a [&'a str],
) -> crate::Result> {
- // Start the bulk request
+ // Retrieve column metadata from the server, keeping only the updateable
+ // columns as bulk targets (identity/computed columns are skipped).
+ let columns: Vec<_> = self
+ .column_metadata(table, columns)
+ .await?
+ .into_iter()
+ .filter(|column| column.base.flags.contains(ColumnFlag::Updateable))
+ .collect();
+
+ // 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 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 ts = TokenStream::new(&mut self.connection);
+ ts.flush_done().await?;
+
+ 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.
+ ///
+ /// ```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>> {
self.connection.flush_stream().await?;
- // retrieve column metadata from server
+ // Ask the server for the column layout without returning any rows.
let columns = columns.join(", ");
let query = format!("SELECT TOP 0 {columns} FROM {table}");
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?;
@@ -381,33 +456,159 @@ impl Client {
})
.await?;
- // now start bulk upload
- let columns: Vec<_> = columns
- .ok_or_else(|| {
- crate::Error::Protocol("expecting column metadata from query but not found".into())
- })?
+ 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.
+ Ok(columns
.into_iter()
- .filter(|column| column.base.flags.contains(ColumnFlag::Updateable))
- .collect();
+ .map(|c| MetaDataColumn {
+ base: c.base,
+ col_name: std::borrow::Cow::Owned(c.col_name.into_owned()),
+ })
+ .collect())
+ }
- self.connection.flush_stream().await?;
- let col_data = columns.iter().map(|c| format!("{}", c)).join(", ");
- let query = format!("INSERT BULK {} ({})", table, col_data);
+ /// 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(())
+ }
- let req = BatchRequest::new(query, self.connection.context().transaction_descriptor());
- let id = self.connection.context_mut().next_packet_id();
+ /// Closes this database connection explicitly.
+ pub async fn close(self) -> crate::Result<()> {
+ self.connection.close().await
+ }
- self.connection.send(PacketHeader::batch(id), req).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
+ }
- let ts = TokenStream::new(&mut self.connection);
- ts.flush_done().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,
+ "",
+ );
- BulkLoadRequest::new(&mut self.connection, columns)
+ self.send_transaction_manager_request(req).await
}
- /// Closes this database connection explicitly.
- pub async fn close(self) -> crate::Result<()> {
- self.connection.close().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> {
@@ -415,12 +616,12 @@ impl Client {
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))),
},
]
}
@@ -446,12 +647,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(
@@ -465,4 +666,55 @@ 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