mssql-client 0.20.0

High-level async SQL Server client with type-state connection management
Documentation

mssql-client

High-level async SQL Server client with type-state connection management.

Overview

This is the primary public API surface for the rust-mssql-driver project. It provides a type-safe, ergonomic interface for working with SQL Server databases using modern async Rust patterns.

Features

  • Type-state pattern: Compile-time enforcement of connection states
  • Async/await: Built on Tokio for efficient async I/O
  • Parameterized queries: Server-side plan reuse via sp_executesql (a client-side statement cache is planned)
  • Transactions: Full transaction support with savepoints
  • Azure support: Automatic routing and failover handling
  • Lazy row decoding: Rows decode on demand from the buffered response (true streaming from the socket is planned)
  • Bulk insert: Bulk data loading via BCP
  • Table-valued parameters: Pass collections to stored procedures

Type-State Connection Management

The client uses a compile-time type-state pattern that ensures invalid operations are caught at compile time:

Disconnected -> Ready (via connect())
Ready -> InTransaction (via begin_transaction())
InTransaction -> Ready (via commit() or rollback())

Usage

use mssql_client::{Client, Config};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::from_connection_string(
        "Server=localhost;Database=test;User Id=sa;Password=Password123",
    )?;

    let mut client = Client::connect(config).await?;

    // Execute a query with parameters
    let rows = client
        .query("SELECT * FROM users WHERE id = @p1", &[&1i32])
        .await?;

    // QueryStream yields Result<Row, Error>
    for result in rows {
        let row = result?;
        let name: String = row.get(0)?;
        println!("User: {}", name);
    }

    Ok(())
}

Transactions with Savepoints

use mssql_client::{Client, Config};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::from_connection_string(
        "Server=localhost;Database=test;User Id=sa;Password=Password123",
    )?;
    let client = Client::connect(config).await?;

    // begin_transaction() consumes the client and returns Client<InTransaction>
    let mut tx = client.begin_transaction().await?;
    tx.execute("INSERT INTO users (name) VALUES (@p1)", &[&"Alice"]).await?;

    // Create a savepoint for partial rollback
    let sp = tx.save_point("before_update").await?;
    tx.execute("UPDATE users SET active = 1", &[]).await?;

    // Rollback to savepoint if needed
    tx.rollback_to(&sp).await?;

    tx.commit().await?;
    Ok(())
}

Large Result Sets

use mssql_client::{Client, Config};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::from_connection_string(
        "Server=localhost;Database=test;User Id=sa;Password=Password123",
    )?;
    let mut client = Client::connect(config).await?;

    // The response is buffered, but rows decode lazily as they are consumed —
    // only one decoded Row is alive at a time.
    let stream = client.query("SELECT * FROM large_table", &[]).await?;

    for result in stream {
        let row = result?;
        let _ = row; // Process each row as it decodes
    }
    Ok(())
}

Bulk Insert

use mssql_client::{BulkColumn, BulkInsertBuilder, Client, Config, SqlValue};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::from_connection_string(
        "Server=localhost;Database=test;User Id=sa;Password=Password123",
    )?;
    let mut client = Client::connect(config).await?;

    let builder = BulkInsertBuilder::new("dbo.users").with_typed_columns(vec![
        BulkColumn::new("id", "INT", 0)?,
        BulkColumn::new("name", "NVARCHAR(100)", 1)?,
    ]);

    let mut writer = client.bulk_insert(&builder).await?;
    writer.send_row_values(&[SqlValue::Int(1), SqlValue::String("Alice".into())])?;
    writer.send_row_values(&[SqlValue::Int(2), SqlValue::String("Bob".into())])?;

    let result = writer.finish().await?;
    println!("Inserted {} rows", result.rows_affected);
    Ok(())
}

Feature Flags

Flag Default Description
chrono Yes Date/time type support via chrono
uuid Yes UUID type support
decimal Yes Decimal type support via rust_decimal
encoding Yes Collation-aware VARCHAR decoding
json No JSON type support via serde_json
otel No OpenTelemetry instrumentation
zeroize No Secure credential wiping
always-encrypted No Client-side encryption with key providers

Modules

Module Description
client Main Client type and connection management
config Connection configuration and parsing
query Query building and execution
row Row and column access
stream Result streaming types
transaction Transaction and savepoint handling
bulk Bulk insert operations
tvp Table-valued parameters
from_row Row-to-struct mapping trait
to_params Struct-to-parameters trait
instrumentation OpenTelemetry integration

Examples

See the examples/ directory for complete examples:

  • basic.rs - Simple queries and parameter binding
  • transactions.rs - Transaction handling with savepoints
  • bulk_insert.rs - Bulk loading
  • derive_macros.rs - Using #[derive(FromRow)] and #[derive(ToParams)]
  • streaming.rs - Iterating large result sets (lazy row decoding)

Development

Built with heavy AI assistance, with a human maintainer reviewing and accountable for every change. The protocol layer has unit and property tests, and an integration suite runs against a real SQL Server in CI; known gaps are tracked in LIMITATIONS.md.

License

MIT OR Apache-2.0