SQLx: The Rust SQL Toolkit

repository·main·Indexed 12 days ago

https://github.com/launchbadge/sqlx

An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. It supports PostgreSQL, MySQL, MariaDB, and SQLite, and is designed to be runtime and TLS agnostic. Includes the sqlx-cli for database creation and migration management.

Tokens
44.5K
Snippets
139
Records
196
Agent score
92%

What's inside SQLx

  1. Understand SQLx's Minimum Supported Rust Version (MSRV)

    main

    SQLx's MSRV is defined as the second-to-latest stable release at the beginning of the current release cycle (0.x.0). This ensures compatibility with Rust versions that are at least six weeks old, allowing time for packaging systems to catch up.

    For example, if the latest stable Rust version is 1.79.0 during the 0.8.0 release cycle, the MSRV for 0.8.x is 1.78.0.

  2. Enable Automatic Test Database Management

    main

    When the migrate feature is enabled, #[sqlx::test] can automatically create isolated test databases for every annotated function. This ensures tests are isolated from one another.

    To activate this, change your test function signature to one of the following:

    • async fn(Pool<DB>) -> Ret (Pools share a single connection limit)
    • async fn(PoolConnection<DB>) -> Ret
    • async fn(PoolOptions<DB>, impl ConnectOptions<DB>) -> Ret (Use this if you need to customize the pool, e.g., setting max_connections)

    Database Requirements:

    • Postgres/MySQL: Requires a DATABASE_URL environment variable (or .env file) providing a superuser connection to manage the test databases.
    • SQLite: Does not require DATABASE_URL. Test databases are stored in target/sqlx/test-dbs/<path>.sqlite, where <path> is the function path.

    Cleanup:

    • Successful tests are automatically cleaned up.
    • Failed tests (including panics) leave databases in place for debugging.
    • Previous test databases are deleted the next time a test binary using #[sqlx::test] is run to save disk space.
    use sqlx::{pool::PoolOptions, postgres::PgConnectOptions, Postgres};
    
    #[sqlx::test]
    async fn basic_test(
        pool_options: PoolOptions<Postgres>,
        connect_options: PgConnectOptions,
    ) -> sqlx::Result<()> {
        let pool = pool_options
            .max_connections(1)
            .connect_with(connect_options)
            .await?;
    
        sqlx::query("SELECT 1").execute(&pool).await?;
    
        Ok(())
    }
  3. Configure multiple databases using `sqlx.toml`

    main

    In complex projects, you can manage multiple schemas or databases by using separate sqlx.toml files in different subcrates. This allows each crate to own its own schema and its own set of migrations.

    In this pattern:

    • A main crate can own the public schema and manage its migrations in a specific directory (e.g., using the migrate.migrations-dir config key).
    • Subcrates (like accounts or payments) can own specific schemas (e.g., accounts or payments) and manage their own migrations independently.

    Important: Use Schema-Qualified Names When working with multiple schemas, avoid relying on changing the connection's search_path to eliminate schema prefixes. Relying on search_path can lead to errors if names conflict or if the migrator attempts to reference a table unqualified when the _sqlx_migrations table exists in multiple schemas. It is best practice to use fully qualified names (e.g., schema_name.table_name) for clarity and reliability.

  4. Querying with prepared and unprepared statements

    main

    SQLx distinguishes between two types of queries:

    1. Prepared (Parameterized) Queries: Use sqlx::query or sqlx::query_as. These are cached, use binary communication for speed, and protect against SQL injection via parameters. In SQLx, a Query or QueryAs struct represents a prepared query.
    2. Unprepared (Simple) Queries: Use a &str directly with an executor. These are intended for database commands that do not support prepared statements, such as PRAGMA, SET, or BEGIN.

    Query Finalizers

    Use high-level finalizers to execute queries against a connection (&mut conn) or a pool (&pool):

    • .execute(): Returns the number of affected rows.
    • .fetch(): Returns a stream of rows.
    • .fetch_one(): Returns exactly one row (errors if zero or multiple found).
    • .fetch_optional(): Returns an Option<Row> (useful when zero rows is not an error).
    • .fetch_all(): Returns a Vec<Row>.
  5. How compile-time checked queries work in SQLx

    main

    SQLx is not an ORM. It does not provide a DSL (Domain Specific Language) for building queries. Instead, it uses the macros feature to provide query*! macros that take raw SQL strings as input.

    How it works: At compile time, SQLx connects to your development database. The database itself verifies the SQL syntax and returns information about the query (such as expected types). This allows SQLx to ensure your queries are valid and type-safe without needing to implement its own SQL parser.

    Implications:

    • Database-Specific Syntax: Since the database performs the verification, you can use any syntax supported by your specific database (including extensions).
    • Verification Depth: The level of verification depends on how much information your specific database engine provides about its queries.
    • Requirement: You must have a running development database available during the compilation process.
  6. Understand Default Host behavior

    main

    If the connection URL does not contain a hostname and PGHOST is not set, SQLx attempts to find an open Unix domain socket in standard locations based on the port:

    • /var/run/postgresql/.s.PGSQL.{port} (Debian)
    • /private/tmp/.s.PGSQL.{port} (macOS via Homebrew)
    • /tmp/.s.PGSQL.{port} (Default)

    Important: If your Postgres instance uses a non-default port, you must set the port parameter correctly for this lookup to work. If no socket is found in these locations, localhost is assumed as the fallback.

  7. Configure SSL and Root Certificates

    main

    SQLx supports SSL configuration via connection URLs or environment variables.

    Root Certificates

    If sslrootcert is not explicitly set, the default root certificates depend on your enabled Cargo features:

    • tls-native-tls: Uses system root certificates.
    • tls-rustls-ring-native-roots: Uses system root certificates.
    • Otherwise: Uses the webpki-roots crate.

    Environment Variable Flexibility

    Unlike libpq, the following environment variables can accept either a file path or a PEM-encoded string directly:

    • PGSSLROOTCERT
    • PGSSLCERT
    • PGSSLKEY

    If the string starts with -----BEGIN <CERTIFICATE | PRIVATE KEY>----- and ends with the corresponding -----END ...----- footer, it is parsed as the certificate/key content. Note that passing private keys via environment variables may pose a security risk.

  8. Compile-time SQL verification with `query!` and `query_as!`

    main

    SQLx provides macros for compile-time syntactic and semantic verification of your SQL queries. This ensures your queries are valid and that the types match your Rust code.

    sqlx::query!

    Returns an anonymous record type where each SQL column is a field. Because the type is anonymous, you cannot name it in a function signature.

    sqlx::query_as!

    Identical to query!, but allows you to map the results into a named struct.

    Requirements for Compile-time Verification

    1. DATABASE_URL: This environment variable must be set at build time. It should point to a database with the same schema as your production database so SQLx can prepare the queries against it.
    2. Offline Mode: To avoid requiring a live database during CI/CD or builds, you can use the sqlx-cli to enable "offline mode," which caches query analysis results in a JSON file.

    Performance Tip

    To speed up incremental builds (like cargo check), add the following to your Cargo.toml to optimize the macro expansion:

    [profile.dev.package.sqlx-macros]
    opt-level = 3
    // query! returns an anonymous record
    let countries = sqlx::query!(
        "SELECT country, COUNT(*) as count FROM users GROUP BY country WHERE organization = ?",
        organization
    )
    .fetch_all(&pool)
    .await?;
    // countries[0].country
    
    // query_as! maps to a named struct
    struct Country { country: String, count: i64 }
    
    let countries = sqlx::query_as!(Country, 
        "SELECT country, COUNT(*) as count FROM users GROUP BY country WHERE organization = ?",
        organization
    )
    .fetch_all(&pool)
    .await?;
  9. Quickstart with SQLx

    main

    To get started with SQLx, you need to create a connection pool using the options specific to your database (e.g., PgPoolOptions for Postgres, MySqlPoolOptions for MySQL, or SqlitePoolOptions for SQLite). You can then use sqlx::query_as to execute a query and map the results to a type.

    Note: For MySQL/MariaDB, use the ? placeholder instead of $1 for parameters.

    use sqlx::postgres::PgPoolOptions;
    
    #[tokio::main]
    async fn main() -> Result<(), sqlx::Error> { 
        // Create a connection pool
        let pool = PgPoolOptions::new()
            .max_connections(5)
            .connect("postgres://postgres:password@localhost/test").await?;
    
        // Make a simple query to return the given parameter
        let row: (i64,) = sqlx::query_as("SELECT $1")
            .bind(150_i64)
            .fetch_one(&pool).await?;
    
        assert_eq!(row.0, 150);
    
        Ok(())
    }
  10. Mapping rows to domain types

    main

    There are three main ways to map database rows to Rust types:

    1. Manual mapping with Row::get or Row::try_get

    Access columns by name or ordinal index using the sqlx::Row trait.

    2. Using the map closure on a stream

    You can transform rows as they are fetched using the .map() method on a fetch() stream.

    3. Using sqlx::FromRow and query_as

    Derive sqlx::FromRow on your struct and use sqlx::query_as to automatically map columns to fields.

    #[derive(sqlx::FromRow)]
    struct User { name: String, id: i64 }
    
    let users = sqlx::query_as::<_, User>("SELECT name, id FROM users")
        .fetch_all(&pool)
        .await?;
    // Using map on a stream
    use futures_util::TryStreamExt;
    use sqlx::Row;
    
    let mut rows = sqlx::query("SELECT * FROM users WHERE email = ?")
        .bind("user@example.com")
        .fetch(&mut conn);
    
    while let Some(row) = rows.try_next().await? {
        let email: &str = row.try_get("email")?;
    }
    
    // Using FromRow
    #[derive(sqlx::FromRow)]
    struct User { name: String, id: i64 }
    
    let mut stream = sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = ? OR name = ?")
        .bind("user@example.com")
        .bind("example_username")
        .fetch(&mut conn);
  11. Enable SQLx offline mode on docs.rs

    main

    Since docs.rs cannot access your database, you must instruct it to use offline mode. To do this, run cargo sqlx prepare locally to generate a .sqlx directory, then add a build.rs file to your project root to detect the DOCS_RS environment variable and set SQLX_OFFLINE=true.

    // build.rs
    fn main() {
        // When building in docs.rs, we want to set SQLX_OFFLINE mode to true
        if std::env::var_os("DOCS_RS").is_some() {
            println!("cargo:rustc-env=SQLX_OFFLINE=true");
        }
    }
  12. Install SQLx with a specific runtime and TLS backend

    main

    SQLx is runtime-agnostic and supports multiple TLS backends. When adding sqlx to your Cargo.toml, you must select a feature combination that includes both a runtime and a tls backend.

    For forward compatibility, it is recommended to use the separate runtime and TLS features rather than the combined legacy features.

    Supported runtimes include tokio (which is also compatible with actix-web) and async-std. Supported TLS backends include native-tls and rustls (with various configurations like ring-webpki or aws-lc-rs).

    # Example: tokio + rustls with ring and WebPKI CA certificates
    [dependencies]
    sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-rustls-ring-webpki" ] }