bsql-macros 0.7.0

Proc macros for bsql — compile-time safe SQL for Rust
Documentation
//! Shared compile-time PostgreSQL connection for proc macro validation.
//!
//! Uses `LazyLock` to maintain a single connection across all `query!`
//! invocations within one `cargo build`. The first invocation pays ~5ms
//! for the connection. Subsequent invocations reuse it at ~0 cost.

use std::sync::LazyLock;
use tokio::runtime::Runtime;
use tokio_postgres::Client;

/// The shared connection state, initialized once per compilation.
struct MacroConnection {
    runtime: Runtime,
    client: Client,
    // Keep the connection task alive. If this handle is dropped, the
    // connection closes and all queries fail.
    _conn_handle: tokio::task::JoinHandle<()>,
}

/// The global shared connection. Initialized on first access.
static MACRO_CONN: LazyLock<Result<MacroConnection, String>> = LazyLock::new(|| {
    let database_url = std::env::var("BSQL_DATABASE_URL")
        .or_else(|_| std::env::var("DATABASE_URL"))
        .map_err(|_| {
            "bsql: BSQL_DATABASE_URL or DATABASE_URL must be set for compile-time \
             SQL validation. Set one of these environment variables to a PostgreSQL \
             connection URL (e.g. postgres://user:pass@localhost/mydb)."
                .to_string()
        })?;

    let rt = Runtime::new().map_err(|e| format!("bsql: failed to create tokio runtime: {e}"))?;

    let mut pg_config: tokio_postgres::Config = database_url
        .parse()
        .map_err(|e| format!("bsql: invalid DATABASE_URL: {e}"))?;
    pg_config.connect_timeout(std::time::Duration::from_secs(10));

    let (client, connection) = rt
        .block_on(pg_config.connect(tokio_postgres::NoTls))
        .map_err(|e| {
            format!(
                "bsql: failed to connect to PostgreSQL at compile time: {e}. \
                 Check that BSQL_DATABASE_URL or DATABASE_URL is set correctly \
                 and the database is running."
            )
        })?;

    let handle = rt.spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("bsql: compile-time connection error: {e}");
        }
    });

    Ok(MacroConnection {
        runtime: rt,
        client,
        _conn_handle: handle,
    })
});

/// Run an async operation on the shared compile-time connection.
///
/// Returns a `syn::Error` if the connection is not available.
pub fn with_connection<F, T>(f: F) -> Result<T, syn::Error>
where
    F: FnOnce(&Runtime, &Client) -> Result<T, String>,
{
    let conn = MACRO_CONN
        .as_ref()
        .map_err(|msg| syn::Error::new(proc_macro2::Span::call_site(), msg))?;
    f(&conn.runtime, &conn.client)
        .map_err(|msg| syn::Error::new(proc_macro2::Span::call_site(), msg))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn missing_env_var_produces_error() {
        // When neither BSQL_DATABASE_URL nor DATABASE_URL is set,
        // with_connection should return an error.
        // NOTE: this test may pass or fail depending on the test environment.
        // If DATABASE_URL is set in CI, this test succeeds differently.
        // We test the error message format when connection is unavailable.
        let result: Result<(), syn::Error> = with_connection(|_rt, _client| Ok(()));
        // We can't assert Err here because the env var might be set.
        // Instead, verify the function compiles and runs without panic.
        let _ = result;
    }
}