qbrs-sqlx 0.3.0

sqlx-based execution integration for qbrs (Postgres).
Documentation
//! `prepare!{}` executed against a real Postgres: one rendered query,
//! reused across multiple `.load(executor, params)` calls with different values.
//! See `postgres_integration.rs` for the DB-setup rationale (in-process
//! WASM Postgres vs. an external `DATABASE_URL`).

mod common;

use qbrs::dialect::Postgres;
use qbrs::expr::{ExprMethods, Text};
use qbrs::select::select;
use qbrs::statement::Statement;
use qbrs::{Table, prepare};
use qbrs_sqlx::{LoadExt, PreparedExt, StreamExt as _};

#[derive(Table)]
#[table(name = "users_prepare_test")]
#[allow(dead_code)]
struct Users {
    #[column(primary_key, generated)]
    id: i64,
    email: String,
}

prepare! {
    struct ByEmail { email: Text }
}

#[tokio::test]
async fn prepared_query_reused_across_different_params() {
    let (pool, guard) = common::test_pool("qbrs_test_prepare").await;

    sqlx::query("DROP TABLE IF EXISTS users_prepare_test")
        .execute(&pool)
        .await
        .expect("drop table");
    sqlx::query(
        "CREATE TABLE users_prepare_test (
            id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
            email TEXT NOT NULL
        )",
    )
    .execute(&pool)
    .await
    .expect("create table");

    let ids: Vec<i64> = qbrs::insert::insert(users::Table)
        .values(UsersInsert::builder().email("ada@example.com").build())
        .values(UsersInsert::builder().email("dan@example.com").build())
        .returning(users::id)
        .load(&pool)
        .await
        .expect("seed users");
    assert_eq!(ids.len(), 2);

    let query = select(users::id)
        .from(users::Table)
        .filter(users::email.eq(ByEmail::email()))
        .prepare::<ByEmail, _>(Postgres);

    let ada: Vec<i64> = query
        .load(
            &pool,
            ByEmail {
                email: "ada@example.com".to_string(),
            },
        )
        .await
        .expect("load for ada");
    assert_eq!(ada, vec![ids[0]]);

    // Same `query` value, no re-render — just a different `params`.
    let dan: Vec<i64> = query
        .load(
            &pool,
            ByEmail {
                email: "dan@example.com".to_string(),
            },
        )
        .await
        .expect("load for dan");
    assert_eq!(dan, vec![ids[1]]);

    // A placeholder named from two clauses is one parameter in the rendered
    // statement, so `resolve` has one slot to fill where the query has two
    // occurrences. Postgres says whether the numbering and the substituted
    // list still line up.
    let twice = select(users::id)
        .from(users::Table)
        .filter(users::email.eq(ByEmail::email()))
        .filter(users::email.gte(ByEmail::email()))
        .prepare::<ByEmail, _>(Postgres);
    let (sql, params) = twice
        .resolve(ByEmail {
            email: "ada@example.com".to_string(),
        })
        .expect("every placeholder resolved");
    assert!(
        sql.ends_with(
            r#"WHERE ("users_prepare_test"."email" = $1) AND ("users_prepare_test"."email" >= $1)"#
        ),
        "{sql}"
    );
    assert_eq!(params.len(), 1);

    let ada_again: Vec<i64> = twice
        .load(
            &pool,
            ByEmail {
                email: "ada@example.com".to_string(),
            },
        )
        .await
        .expect("load through a placeholder named twice");
    assert_eq!(ada_again, vec![ids[0]]);

    // A prepared query streams with the params that arrive at the call —
    // which is the shape a reusable export has.
    let mut streamed = query
        .stream(
            &pool,
            ByEmail {
                email: "dan@example.com".to_string(),
            },
        )
        .expect("open the prepared stream");
    let mut streamed_ids = Vec::new();
    while let Some(id) = streamed.next().await {
        streamed_ids.push(id.expect("decode a streamed row"));
    }
    drop(streamed);
    assert_eq!(streamed_ids, vec![ids[1]]);

    sqlx::query("DROP TABLE users_prepare_test")
        .execute(&pool)
        .await
        .expect("cleanup");
    common::shutdown(pool, guard).await;
}