tiberius

repository·main·Indexed 19 days ago

https://github.com/prisma/tiberius

A native asynchronous Microsoft SQL Server (TDS) driver for Rust. It provides a protocol-agnostic implementation of the TDS protocol, supporting asynchronous IO and various TLS implementations including native-tls and rustls. The library focuses on the TDS protocol and is not a query builder or ORM. Key features include support for bulk inserts, Azure connection redirect handling, and configurable TLS encryption levels.

Tokens
12.5K
Snippets
34
Records
52
Agent score
65%

What's inside tiberius

  1. What is Tiberius?

    main

    Tiberius is a native Microsoft SQL Server (TDS) client for Rust. It provides an asynchronous implementation of the TDS protocol and is designed to be independent of the underlying network protocol.

    Key Characteristics:

    • Asynchronous IO: Built for non-blocking network operations.
    • Protocol Agnostic: The Client accepts any socket that implements the AsyncRead and AsyncWrite traits from the futures-rs crate.
    • Not a Query Builder or ORM: Tiberius focuses on the TDS protocol. For connection pooling, use crates like bb8, mobc, or deadpool. For query building or ORM functionality, you will need additional libraries.
  2. Configure TLS encryption settings

    main

    Tiberius supports three encryption levels that can be set during connection:

    • Required: All traffic is encrypted (Default).
    • Off: Only the login procedure is encrypted.
    • NotSupported: No traffic is encrypted.

    TLS Implementation Options:

    • native-tls (Default): Links to OS libraries (OpenSSL on Linux, Schannel on Windows, Security Framework on macOS). Recommended for security updates via system patches.
    • rustls: Uses a pure Rust TLS implementation. Recommended for Apple platforms (macOS/iOS) as the Security Framework may not work correctly with SQL Server TLS settings.
    • vendored-openssl: Statically links against OpenSSL.

    Note: You cannot enable both native-tls and rustls at the same time.

  3. Generate self-signed certificates for Tiberius

    main

    To prepare the necessary self-signed certificates for use with Tiberius, you must first create a custom Certificate Authority (CA) and then use it to sign specific server certificates.

    1. Run ./generate-ca.sh to create a new signing certificate (the custom CA).
    2. Run ./generate-signed-cert.sh <name> to create new certificates with the specified <name>, signed by the customCA.crt created in the first step.
    ./generate-ca.sh
    ./generate-signed-cert.sh server
  4. Handle Azure connection redirects

    main

    When connecting to certain Azure SQL instances, the server may return an Error::Routing { host, port }. This indicates that you must establish a new connection to the redirected address provided in the error.

    To handle this, catch the Error::Routing variant, update your Config with the new host and port, and attempt the connection again. You should typically only expect one redirect.

    use tiberius::{Client, Config, AuthMethod, error::Error};
    use tokio_util::compat::TokioAsyncWriteCompatExt;
    use tokio::net::TcpStream;
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let mut config = Config::new();
    
        config.host("0.0.0.0");
        config.port(1433);
        config.authentication(AuthMethod::sql_server("SA", "<Mys3cureP4ssW0rD>"));
    
        let tcp = TcpStream::connect(config.get_addr()).await?;
        tcp.set_nodelay(true)?;
    
        let client = match Client::connect(config, tcp.compat_write()).await {
            // Connection successful.
            Ok(client) => client,
            // The server wants us to redirect to a different address
            Err(Error::Routing { host, port }) => {
                let mut config = Config::new();
    
                config.host(&host);
                config.port(port);
                config.authentication(AuthMethod::sql_server("SA", "<Mys3cureP4ssW0rD>"));
    
                let tcp = TcpStream::connect(config.get_addr()).await?;
                tcp.set_nodelay(true)?;
    
                // we should not have more than one redirect, so we'll short-circuit here.
                Client::connect(config, tcp.compat_write()).await?
            }
            Err(e) => Err(e)?,
        };
    
        Ok(())
    }
  5. How authentication works in Tiberius

    main

    Tiberius supports several authentication methods:

    • SQL Server authentication: Uses standard database credentials.
    • Windows Authentication: Uses the currently logged-in user or specified Windows credentials (on Windows).
    • Kerberos: If the integrated-auth-gssapi feature is enabled, you can use active Kerberos credentials.
    • AAD (Azure Active Directory): Supports AAD tokens. It is recommended to use the azure_identity crate to retrieve a token and then configure Tiberius with that token.
  6. Convert Rust types to SQL parameters using ToSql and IntoSql

    main

    To use Rust values as parameters in Client#query or Client#execute methods, the types must implement the ToSql or IntoSql traits. These traits convert Rust types into ColumnData, which is the format understood by the SQL Server TDS protocol.

    Supported Type Mappings

    Rust typeSQL Server type
    u8tinyint
    i16smallint
    i32int
    i64bigint
    f32float(24)
    f64float(53)
    boolbit
    String / &str (< 4000 chars)nvarchar(4000)
    String / &strnvarchar(max)
    Vec<u8> / &[u8] (< 8000 bytes)varbinary(8000)
    Vec<u8> / &[u8]varbinary(max)
    Uuiduniqueidentifier
    Numericnumeric / decimal
    Decimal (requires rust_decimal feature)numeric / decimal
    BigDecimal (requires bigdecimal feature)numeric / decimal
    XmlDataxml
    NaiveDate (requires chrono feature, TDS 7.3+)date
    NaiveTime (requires chrono feature, TDS 7.3+)time
    DateTime (requires chrono feature, TDS 7.3+)datetimeoffset
    NaiveDateTime (requires chrono feature)datetime2 (TDS 7.3+) or datetime (TDS 7.2)

    Type Flexibility

    • Strings: Can be used with ntext, text, varchar, nchar, and char columns.
    • Binary: Can be used with binary and image columns.
    • Dates: On TDS 7.3+ (SQL Server 2008+), NaiveDateTime can also be used for datetime and smalldatetime columns.
  7. Configure Tiberius feature flags

    main

    Tiberius uses feature flags to enable specific functionalities like TLS implementations, date/time types, and decimal support.

    FlagDescription
    tds73Support for new date and time types in TDS version 7.3. Disable if using version 7.2. (Default: enabled)
    native-tlsUse operating system's TLS libraries for traffic encryption. (Default: enabled)
    rustlsUse the built-in TLS implementation from rustls. (Default: disabled)
    vendored-opensslStatically link against OpenSSL. (Default: disabled)
    chronoRead/write date and time using chrono types. (Default: disabled)
    timeRead/write date and time using time crate types. (Default: disabled)
    rust_decimalRead/write numeric/decimal using rust_decimal::Decimal. (Default: disabled)
    bigdecimalRead/write numeric/decimal using bigdecimal::BigDecimal. (Default: disabled)
    sql-browser-tokioSQL Browser implementation for Tokio TcpStream.
    sql-browser-async-stdSQL Browser implementation for async-std TcpStream.
    sql-browser-smolSQL Browser implementation for smol TcpStream.
    integrated-auth-gssapiSupport for Integrated Auth via GSSAPI.
  8. Mapping SQL Server date and time types to Rust

    main

    Tiberius maps SQL Server date and time types to specific Rust types depending on the enabled feature flags and the SQL Server version being used.

    With tds73 feature flag (SQL Server 2008 or later):

    • Time -> time::Time
    • Date -> time::Date
    • DateTime -> time::PrimitiveDateTime
    • DateTime2 -> time::PrimitiveDateTime
    • SmallDateTime -> time::PrimitiveDateTime
    • DateTimeOffset -> time::OffsetDateTime

    Without tds73 feature flag (SQL Server 2005):

    • DateTime -> time::PrimitiveDateTime
    • SmallDateTime -> time::PrimitiveDateTime

    Note: It is highly recommended to use the types provided by the time or chrono features (e.g., PrimitiveDateTime, Date, OffsetDateTime) rather than the raw server-side presentation types (like DateTime, SmallDateTime, or DateTime2) for application logic.

  9. Accessing data from a Row

    main

    A Row represents a single record from a query result set. You can access its data in two primary ways:

    1. By-reference (Copying): Use .get(idx) or .try_get(idx) to retrieve a value. The index idx can be a zero-indexed usize or a &str representing the column name.

      • .get(idx): Panics if the type conversion fails or the index is out of bounds. Use this when you are certain of the schema.
      • .try_get(idx): Returns a Result<Option<R>>. Use this for safer error handling.
    2. By-value (Moving): Implement IntoIterator for Row to consume the row and iterate over its values as ColumnData items.

    Metadata such as column names and types can be accessed via .columns().

    // By-reference using name
    let val: Option<i32> = row.get("col1");
    
    // By-reference using index
    let val: Option<i32> = row.get(0);
    
    // By-value iteration
    for val in row.into_iter() {
        // val is ColumnData
    }
  10. How QueryStream works and handles multiple result sets

    main

    A QueryStream is an asynchronous stream of QueryItem values returned by a query. It can contain both Metadata (describing the columns of the upcoming rows) and Row data.

    Key behaviors:

    • Metadata First: Every result set in the stream begins with a Metadata item describing the column structure.
    • Multiple Results: If a single query (or batch) produces multiple result sets (e.g., SELECT 1; SELECT 2), the stream will yield multiple Metadata items interleaved with their respective rows.
    • Performance Warning: You must poll the QueryStream until it is empty before sending another query via the same Client. Failing to do so causes an undeterministic flush that slows down subsequent queries.
    • Result Indexing: You can use result_index() on Metadata or Row to determine which result set the item belongs to (starting from 0).
    // Example of iterating over a stream with multiple result sets
    while let Some(item) = stream.try_next().await? {
        match item {
            // Metadata for the first result set
            QueryItem::Metadata(meta) if meta.result_index() == 0 => {
                // handle metadata
            }
            // A row from the first result set
            QueryItem::Row(row) if row.result_index() == 0 => {
                assert_eq!(Some(1), row.get(0));
            }
            // Metadata for the second result set
            QueryItem::Metadata(meta) => {
                // handle metadata
            }
            // A row from the second result set
            QueryItem::Row(row) => {
                assert_eq!(Some(2), row.get(0));
            }
        }
    }
  11. Connect to a named instance using SQL Browser

    main

    On Windows, connecting to a named instance may require the SQL Browser service to resolve the correct port. To use this, enable the sql-browser-async-std or sql-browser-tokio feature flag. You must provide the instance_name and set the port to the SQL Browser port (default 1434). Use the TcpStream::connect_named(&config) method provided by the SqlBrowser trait.

    #[cfg(any(feature = "sql-browser-async-std", feature = "sql-browser-tokio"))]
    use tiberius::{Client, Config, AuthMethod, SqlBrowser};
    use async_std::net::TcpStream;
    
    #[async_std::main]
    async fn main() -> anyhow::Result<()> {
        let mut config = Config::new();
        config.authentication(AuthMethod::sql_server("SA", "<password>"));
        config.host("localhost");
        config.port(1434); // SQL Browser port
        config.instance_name("INSTANCE");
        config.trust_cert();
    
        // connect_named is provided by the SqlBrowser trait
        let tcp = TcpStream::connect_named(&config).await?;
    
        let mut client = Client::connect(config, tcp).await?;
        Ok(())
    }
  12. Run Microsoft SQL Server via Docker Compose

    main

    The repository provides a docker-compose.yml file to spin up various versions of Microsoft SQL Server for local development and testing with Tiberius. You can choose between different SQL Server versions or Azure SQL Edge by selecting the corresponding service in the compose file.

    To use these services, ensure you have Docker and Docker Compose installed, then run:

    docker-compose up
    version: "3"
    services:
      mssql-2022:
        build:
          context: docker/
          dockerfile: docker-mssql-2022.dockerfile
        restart: always
        environment:
          ACCEPT_EULA: "Y"
          SA_PASSWORD: "<YourStrong@Passw0rd>"
        ports:
          - "1433:1433"