SQLx es el toolkit de base de datos async para Rust que verifica tus queries SQL contra la base de datos real en tiempo de compilación, antes de ejecutar el código. Si la query tiene un error de sintaxis, referencia una columna que no existe o devuelve un tipo incompatible con el struct de destino, obtienes un error de compilación, no un panic en producción.
Conexión con PgPool y configuración básica
SQLx trabaja con pools de conexiones. PgPool gestiona un conjunto de conexiones a PostgreSQL y las reutiliza entre peticiones. El pool es Clone y Send + Sync, por lo que puede compartirse fácilmente con Axum o Tokio.
// [dependencies]
// sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
// tokio = { version = "1", features = ["full"] }
use sqlx::PgPool;
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://user:pass@localhost/midb".into());
let pool = PgPool::connect(&database_url).await?;
// Verificar conexión
let row: (i32,) = sqlx::query_as("SELECT 1").fetch_one(&pool).await?;
println!("Conexión OK: {}", row.0);
// Configurar pool con opciones avanzadas
let pool_config = sqlx::postgres::PgPoolOptions::new()
.max_connections(20)
.min_connections(2)
.acquire_timeout(std::time::Duration::from_secs(5))
.connect(&database_url)
.await?;
println!("Pool configurado con {} conexiones máximas", pool_config.size());
Ok(())
}
query_as! y fetch_all: mapeo a structs
La macro query_as! compila la query y verifica que los tipos del struct destino coinciden con los de la query. Para que la verificación funcione en compilación, SQLx necesita acceso a la base de datos durante cargo build (o usa archivos de metadatos generados con cargo sqlx prepare para CI offline).
use sqlx::{PgPool, FromRow};
#[derive(Debug, FromRow)]
struct Usuario {
id: i64,
nombre: String,
email: String,
activo: bool,
}
async fn listar_usuarios(pool: &PgPool) -> Result<Vec<Usuario>, sqlx::Error> {
// query_as! verifica la SQL contra la DB en compilación
sqlx::query_as!(Usuario,
"SELECT id, nombre, email, activo FROM usuarios WHERE activo = true ORDER BY nombre"
)
.fetch_all(pool)
.await
}
async fn buscar_usuario(pool: &PgPool, id: i64) -> Result<Option<Usuario>, sqlx::Error> {
sqlx::query_as!(Usuario,
"SELECT id, nombre, email, activo FROM usuarios WHERE id = $1",
id
)
.fetch_optional(pool)
.await
}
async fn crear_usuario(pool: &PgPool, nombre: &str, email: &str) -> Result<i64, sqlx::Error> {
let row = sqlx::query!(
"INSERT INTO usuarios (nombre, email, activo) VALUES ($1, $2, true) RETURNING id",
nombre, email
)
.fetch_one(pool)
.await?;
Ok(row.id)
}
Transacciones
SQLx ofrece transacciones con soporte completo de RAII: si el Transaction sale del scope sin que se haya llamado a commit(), hace rollback automáticamente. Esto garantiza que los errores nunca dejan la base de datos en estado inconsistente.
use sqlx::PgPool;
async fn transferir_saldo(
pool: &PgPool,
origen_id: i64,
destino_id: i64,
importe: f64,
) -> Result<(), sqlx::Error> {
let mut tx = pool.begin().await?;
// Verificar saldo suficiente
let saldo: (f64,) = sqlx::query_as(
"SELECT saldo FROM cuentas WHERE id = $1 FOR UPDATE"
)
.bind(origen_id)
.fetch_one(&mut *tx)
.await?;
if saldo.0 < importe {
return Err(sqlx::Error::RowNotFound); // forzar rollback
}
// Restar del origen
sqlx::query!("UPDATE cuentas SET saldo = saldo - $1 WHERE id = $2", importe, origen_id)
.execute(&mut *tx)
.await?;
// Sumar al destino
sqlx::query!("UPDATE cuentas SET saldo = saldo + $1 WHERE id = $2", importe, destino_id)
.execute(&mut *tx)
.await?;
tx.commit().await?; // sin esto, el Drop haría rollback
Ok(())
}
Migraciones con sqlx migrate
SQLx incluye un sistema de migraciones integrado. Los archivos de migración son SQL numerado en el directorio migrations/ y se aplican programáticamente al iniciar la aplicación o con el CLI de sqlx.
// Estructura de archivos:
// migrations/
// 0001_crear_usuarios.sql
// 0002_agregar_perfil.sql
// 0003_indices.sql
// migrations/0001_crear_usuarios.sql
-- CREATE TABLE usuarios (
-- id SERIAL PRIMARY KEY,
-- nombre VARCHAR(100) NOT NULL,
-- email VARCHAR(255) UNIQUE NOT NULL,
-- activo BOOLEAN NOT NULL DEFAULT true,
-- creado_en TIMESTAMP WITH TIME ZONE DEFAULT NOW()
-- );
// Aplicar migraciones al iniciar la aplicación:
use sqlx::PgPool;
async fn inicializar_db(pool: &PgPool) -> Result<(), sqlx::Error> {
sqlx::migrate!("./migrations")
.run(pool)
.await?;
println!("Migraciones aplicadas");
Ok(())
}
// Comandos CLI de sqlx:
// cargo install sqlx-cli
// sqlx migrate add crear_usuarios -- crear nuevo archivo de migración
// sqlx migrate run -- aplicar pendientes
// sqlx migrate revert -- revertir la última
// cargo sqlx prepare -- generar metadatos offline para CI
La verificación en compilación de SQLx elimina toda una categoría de bugs que en otros lenguajes solo aparecen en producción. El coste es que necesitas acceso a la base de datos durante el desarrollo (o usar los metadatos offline), pero en la práctica ese acceso ya existe en cualquier flujo de desarrollo serio.
