Para una base de datos en Rust, rusqlite con SQLite es la opción con menos fricción: no hace falta levantar ningún servidor, la base es un archivo o incluso solo memoria, y la característica bundled compila SQLite dentro del programa, así que tampoco hay que instalar nada en el sistema.
También te puede interesar
La dependencia
cargo add rusqlite --features bundled
[dependencies]
rusqlite = { version = "0.40.2", features = ["bundled"] }
La característica bundled compila el SQLite en C junto con tu programa. La primera compilación tarda algo más, pero a cambio el binario no depende de ninguna biblioteca instalada en la máquina.
Crear, insertar y consultar
use rusqlite::{params, Connection};
#[derive(Debug)]
struct Articulo {
id: i64,
nombre: String,
unidades: u32,
}
fn main() -> rusqlite::Result<()> {
let con = Connection::open_in_memory()?;
con.execute(
"CREATE TABLE articulo (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL,
unidades INTEGER NOT NULL
)",
(),
)?;
for (nombre, unidades) in [("tornillos", 120), ("tuercas", 80), ("arandelas", 45)] {
con.execute(
"INSERT INTO articulo (nombre, unidades) VALUES (?1, ?2)",
params![nombre, unidades],
)?;
}
println!("último id insertado: {}", con.last_insert_rowid());
let mut consulta = con.prepare("SELECT id, nombre, unidades FROM articulo WHERE unidades > ?1")?;
let filas = consulta.query_map([50], |f| {
Ok(Articulo { id: f.get(0)?, nombre: f.get(1)?, unidades: f.get(2)? })
})?;
println!("con más de 50 unidades:");
for a in filas {
let a = a?;
println!(" {} {:<10} {}", a.id, a.nombre, a.unidades);
}
let total: u32 = con.query_row("SELECT SUM(unidades) FROM articulo", [], |f| f.get(0))?;
println!("total de unidades: {total}");
Ok(())
}
salidaúltimo id insertado: 3 con más de 50 unidades: 1 tornillos 120 2 tuercas 80 total de unidades: 245
Lo importante del ejemplo está en los ?1 de las consultas. Nunca se construye el SQL pegando cadenas: los valores van como parámetros y SQLite los trata como datos, no como código. Es lo que impide una inyección de SQL, y en Rust además te da la conversión de tipos hecha.
El cierre tampoco hay que escribirlo. La conexión se cierra sola cuando con sale de ámbito, igual que los archivos: es la misma regla de propiedad de la guía del curso aplicada a un recurso del sistema.
execute para lo que no devuelve filas y da cuántas cambiaron; query_row cuando esperas exactamente una, como en la suma del final; y query_map cuando esperas varias, que entrega un iterador donde cada elemento es un Result porque una fila concreta puede fallar al convertirse.Lo que se usa a diario
| Qué quieres | Cómo |
|---|---|
| Base en un archivo | Connection::open("datos.db")? |
| Base en memoria, para pruebas | Connection::open_in_memory()? |
| Una orden sin resultado | con.execute(sql, params![...]) |
| Una sola fila | con.query_row(...) |
| Varias filas | prepare(...) y query_map(...) |
| Un campo que puede ser NULL | leerlo como Option<T> |
| Varias operaciones atómicas | con.transaction()? |
sqlx, que trabaja con los tres motores y comprueba las consultas en tiempo de compilación contra la base real. A cambio es asíncrono, así que arrastra tokio y async.El código de esta página se compiló y ejecutó con rustc 1.96.1 y rusqlite 0.40.2 antes de publicarla; la salida está copiada de la ejecución real. Documentación oficial: rusqlite en docs.rs.