Conectar ao PostgreSQL em Rust

Aprenda como conectar ao PostgreSQL em Rust com sqlx. PgPool, variáveis de ambiente, query_as, migrações e operações CRUD completas.

Conectar ao PostgreSQL em Rust

A crate sqlx é a biblioteca mais popular para acesso a bancos de dados em Rust. Ela é totalmente assíncrona, suporta verificação de queries em tempo de compilação e funciona com PostgreSQL, MySQL e SQLite.

Dependências

Cargo.toml:

[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "macros"] }
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"
serde = { version = "1", features = ["derive"] }

Configuração com .env

Crie um arquivo .env na raiz do projeto:

DATABASE_URL=postgres://usuario:senha@localhost:5432/meu_banco

Conexão básica com PgPool

Estabeleça um pool de conexões (recomendado para produção):

use sqlx::postgres::PgPoolOptions;
use std::env;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    // Carregar variáveis do .env
    dotenvy::dotenv().ok();

    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL deve estar definida no .env");

    // Criar pool de conexões
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&database_url)
        .await?;

    // Testar a conexão
    let row: (i64,) = sqlx::query_as("SELECT $1::BIGINT")
        .bind(150_i64)
        .fetch_one(&pool)
        .await?;

    println!("Conexão OK! Resultado do teste: {}", row.0);

    Ok(())
}

Saída:

Conexão OK! Resultado do teste: 150

Criar tabela

Execute DDL para criar a estrutura do banco:

use sqlx::postgres::PgPoolOptions;
use std::env;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    dotenvy::dotenv().ok();
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&env::var("DATABASE_URL").unwrap())
        .await?;

    // Criar tabela
    sqlx::query(
        r#"
        CREATE TABLE IF NOT EXISTS usuarios (
            id SERIAL PRIMARY KEY,
            nome VARCHAR(100) NOT NULL,
            email VARCHAR(150) UNIQUE NOT NULL,
            idade INTEGER,
            ativo BOOLEAN DEFAULT true,
            criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        "#,
    )
    .execute(&pool)
    .await?;

    println!("Tabela 'usuarios' criada com sucesso!");

    Ok(())
}

Saída:

Tabela 'usuarios' criada com sucesso!

CRUD completo — Create, Read, Update, Delete

Um exemplo completo com todas as operações CRUD:

use serde::Serialize;
use sqlx::postgres::PgPoolOptions;
use sqlx::FromRow;
use std::env;

#[derive(Debug, FromRow, Serialize)]
struct Usuario {
    id: i32,
    nome: String,
    email: String,
    idade: Option<i32>,
    ativo: Option<bool>,
}

// CREATE — inserir novo usuário
async fn criar_usuario(
    pool: &sqlx::PgPool,
    nome: &str,
    email: &str,
    idade: i32,
) -> Result<Usuario, sqlx::Error> {
    let usuario = sqlx::query_as::<_, Usuario>(
        r#"
        INSERT INTO usuarios (nome, email, idade)
        VALUES ($1, $2, $3)
        RETURNING id, nome, email, idade, ativo
        "#,
    )
    .bind(nome)
    .bind(email)
    .bind(idade)
    .fetch_one(pool)
    .await?;

    Ok(usuario)
}

// READ — buscar todos os usuários
async fn listar_usuarios(pool: &sqlx::PgPool) -> Result<Vec<Usuario>, sqlx::Error> {
    let usuarios = sqlx::query_as::<_, Usuario>(
        "SELECT id, nome, email, idade, ativo FROM usuarios ORDER BY id",
    )
    .fetch_all(pool)
    .await?;

    Ok(usuarios)
}

// READ — buscar por ID
async fn buscar_por_id(
    pool: &sqlx::PgPool,
    id: i32,
) -> Result<Option<Usuario>, sqlx::Error> {
    let usuario = sqlx::query_as::<_, Usuario>(
        "SELECT id, nome, email, idade, ativo FROM usuarios WHERE id = $1",
    )
    .bind(id)
    .fetch_optional(pool)
    .await?;

    Ok(usuario)
}

// UPDATE — atualizar usuário
async fn atualizar_email(
    pool: &sqlx::PgPool,
    id: i32,
    novo_email: &str,
) -> Result<bool, sqlx::Error> {
    let resultado = sqlx::query(
        "UPDATE usuarios SET email = $1 WHERE id = $2",
    )
    .bind(novo_email)
    .bind(id)
    .execute(pool)
    .await?;

    Ok(resultado.rows_affected() > 0)
}

// DELETE — remover usuário
async fn remover_usuario(
    pool: &sqlx::PgPool,
    id: i32,
) -> Result<bool, sqlx::Error> {
    let resultado = sqlx::query("DELETE FROM usuarios WHERE id = $1")
        .bind(id)
        .execute(pool)
        .await?;

    Ok(resultado.rows_affected() > 0)
}

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    dotenvy::dotenv().ok();

    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&env::var("DATABASE_URL").unwrap())
        .await?;

    // Criar tabela
    sqlx::query(
        r#"
        CREATE TABLE IF NOT EXISTS usuarios (
            id SERIAL PRIMARY KEY,
            nome VARCHAR(100) NOT NULL,
            email VARCHAR(150) UNIQUE NOT NULL,
            idade INTEGER,
            ativo BOOLEAN DEFAULT true,
            criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        "#,
    )
    .execute(&pool)
    .await?;

    // CREATE
    println!("=== Criando usuários ===");
    let u1 = criar_usuario(&pool, "Maria Silva", "maria@exemplo.com", 28).await?;
    println!("Criado: {:?}", u1);

    let u2 = criar_usuario(&pool, "João Santos", "joao@exemplo.com", 35).await?;
    println!("Criado: {:?}", u2);

    let u3 = criar_usuario(&pool, "Ana Costa", "ana@exemplo.com", 22).await?;
    println!("Criado: {:?}", u3);

    // READ (listar todos)
    println!("\n=== Listando todos ===");
    let todos = listar_usuarios(&pool).await?;
    for u in &todos {
        println!("  #{}: {} ({}) - idade: {:?}", u.id, u.nome, u.email, u.idade);
    }

    // READ (buscar por ID)
    println!("\n=== Buscar por ID ===");
    if let Some(usuario) = buscar_por_id(&pool, u1.id).await? {
        println!("Encontrado: {} ({})", usuario.nome, usuario.email);
    }

    // UPDATE
    println!("\n=== Atualizando email ===");
    let atualizado = atualizar_email(&pool, u1.id, "maria.nova@exemplo.com").await?;
    println!("Atualizado: {}", atualizado);

    if let Some(usuario) = buscar_por_id(&pool, u1.id).await? {
        println!("Email atualizado: {}", usuario.email);
    }

    // DELETE
    println!("\n=== Removendo usuário ===");
    let removido = remover_usuario(&pool, u3.id).await?;
    println!("Removido: {}", removido);

    // Listar final
    println!("\n=== Lista final ===");
    let restantes = listar_usuarios(&pool).await?;
    for u in &restantes {
        println!("  #{}: {} ({})", u.id, u.nome, u.email);
    }

    Ok(())
}

Saída:

=== Criando usuários ===
Criado: Usuario { id: 1, nome: "Maria Silva", email: "maria@exemplo.com", idade: Some(28), ativo: Some(true) }
Criado: Usuario { id: 2, nome: "João Santos", email: "joao@exemplo.com", idade: Some(35), ativo: Some(true) }
Criado: Usuario { id: 3, nome: "Ana Costa", email: "ana@exemplo.com", idade: Some(22), ativo: Some(true) }

=== Listando todos ===
  #1: Maria Silva (maria@exemplo.com) - idade: Some(28)
  #2: João Santos (joao@exemplo.com) - idade: Some(35)
  #3: Ana Costa (ana@exemplo.com) - idade: Some(22)

=== Buscar por ID ===
Encontrado: Maria Silva (maria@exemplo.com)

=== Atualizando email ===
Atualizado: true
Email atualizado: maria.nova@exemplo.com

=== Removendo usuário ===
Removido: true

=== Lista final ===
  #1: Maria Silva (maria.nova@exemplo.com)
  #2: João Santos (joao@exemplo.com)

Queries com filtros e busca

Exemplos de queries mais avançadas:

use sqlx::FromRow;
use sqlx::postgres::PgPoolOptions;
use std::env;

#[derive(Debug, FromRow)]
struct Usuario {
    id: i32,
    nome: String,
    email: String,
    idade: Option<i32>,
    ativo: Option<bool>,
}

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    dotenvy::dotenv().ok();
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&env::var("DATABASE_URL").unwrap())
        .await?;

    // Buscar com filtro
    let adultos = sqlx::query_as::<_, Usuario>(
        "SELECT id, nome, email, idade, ativo FROM usuarios WHERE idade >= $1 AND ativo = true ORDER BY nome",
    )
    .bind(18)
    .fetch_all(&pool)
    .await?;

    println!("Usuários adultos ativos:");
    for u in &adultos {
        println!("  {} - {} anos", u.nome, u.idade.unwrap_or(0));
    }

    // Busca por texto (LIKE)
    let busca = "%silva%";
    let resultados = sqlx::query_as::<_, Usuario>(
        "SELECT id, nome, email, idade, ativo FROM usuarios WHERE LOWER(nome) LIKE LOWER($1)",
    )
    .bind(busca)
    .fetch_all(&pool)
    .await?;

    println!("\nBusca por 'silva': {} resultados", resultados.len());

    // Contagem
    let (total,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM usuarios")
        .fetch_one(&pool)
        .await?;

    println!("Total de usuários: {}", total);

    // Paginação
    let pagina = 1;
    let por_pagina = 10;
    let offset = (pagina - 1) * por_pagina;

    let pagina_usuarios = sqlx::query_as::<_, Usuario>(
        "SELECT id, nome, email, idade, ativo FROM usuarios ORDER BY id LIMIT $1 OFFSET $2",
    )
    .bind(por_pagina)
    .bind(offset)
    .fetch_all(&pool)
    .await?;

    println!(
        "\nPágina {} ({} de {} total):",
        pagina,
        pagina_usuarios.len(),
        total
    );
    for u in &pagina_usuarios {
        println!("  #{}: {}", u.id, u.nome);
    }

    Ok(())
}

Saída:

Usuários adultos ativos:
  João Santos - 35 anos
  Maria Silva - 28 anos

Busca por 'silva': 1 resultados
Total de usuários: 2

Página 1 (2 de 2 total):
  #1: Maria Silva
  #2: João Santos

Transações

Agrupe operações que devem ser atômicas:

use sqlx::postgres::PgPoolOptions;
use std::env;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    dotenvy::dotenv().ok();
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&env::var("DATABASE_URL").unwrap())
        .await?;

    // Iniciar transação
    let mut tx = pool.begin().await?;

    // Todas as operações dentro da transação
    sqlx::query("INSERT INTO usuarios (nome, email, idade) VALUES ($1, $2, $3)")
        .bind("Pedro Lima")
        .bind("pedro@exemplo.com")
        .bind(30)
        .execute(&mut *tx)
        .await?;

    sqlx::query("UPDATE usuarios SET ativo = false WHERE idade < $1")
        .bind(25)
        .execute(&mut *tx)
        .await?;

    // Commit — todas as operações são aplicadas de uma vez
    tx.commit().await?;

    println!("Transação concluída com sucesso!");

    // Se qualquer operação falhasse, nenhuma seria aplicada
    // O drop automático do tx faz rollback se commit não foi chamado

    Ok(())
}

Saída:

Transação concluída com sucesso!

Veja também