medoo_rs
🇪🇸 Solo hablo español — la documentación, los comentarios del código y los mensajes de error están en español. Sorry / lo siento / sumimasen. PRs en inglés son bienvenidos igualmente.
Query builder dinámico inspirado en Medoo (PHP).
API plana, ergonómica, multi-backend (Postgres / MySQL / SQLite).
Núcleo sin dependencias: construye (SQL, Vec<Value>) parametrizado.
La capa async (pool, transacciones, streaming) se monta encima como
feature opcional.
Cheat sheet rápida → MANUAL.md
Tabla de contenidos
- Filosofía
- Instalación
- Features opcionales
- Quick start (núcleo)
- Quick start (pool async)
- Conceptos
- SELECT
- INSERT / UPSERT / RETURNING
- UPDATE / DELETE
- Macros
- JSON
- DDL
- Migraciones
- Logging
- Pool async — referencia completa
- FromRow (mapeo a struct)
- chrono (fechas tipadas)
- Seguridad
- Tests
Filosofía
let mut q = db.select;
if let Some = filter.status
if let Some = filter.min_age
if filter.recent
if let Some = filter.limit
let = q.to_sql?;
Cada if agrega o no agrega. Sin clones, sin builders verbosos.
Como Medoo, pero rápido y seguro.
Instalación
[]
= "0.1"
Compila con Rust estable. Núcleo zero deps.
Linux / WSL: sudo apt install build-essential.
Windows: Visual Studio Build Tools (Desktop development with C++).
Features opcionales
Todas son opt-in. Sin activar ninguna, el núcleo permanece zero-deps
y solo emite (sql, params) — vos lo ejecutás contra el runtime que
prefieras.
| Feature | Activa |
|---|---|
runtime-mysql |
Pool sqlx MySQL / MariaDB |
runtime-postgres |
Pool sqlx Postgres |
runtime-sqlite |
Pool sqlx SQLite |
derive |
#[derive(FromRow)] (proc-macro) |
chrono |
IntoValue + accesores chrono para fechas tipadas |
Los runtime-* arrastran sqlx + tokio + futures. Necesitás
tokio en tu app al activarlos.
= { = "0.1", = ["runtime-postgres", "derive", "chrono"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Quick start (núcleo)
use ;
let db = new
.with_logger;
let q = db.select
.where_cond
.order_desc
.limit;
let = db.build?;
let ins = db.insert.set;
let = db.build?;
db.build(&q) corre to_sql() y emite log. Para SQL puro sin logger
usá q.to_sql() directo.
Quick start (pool async)
[]
= { = "0.1", = ["runtime-postgres"] }
= { = "1", = ["macros", "rt-multi-thread"] }
use ;
use ;
async
Pool con opciones, transacciones, savepoints, retry, streaming y
EXPLAIN: ver Pool async — referencia completa.
Conceptos
Db
Punto de entrada del builder. No mantiene pool — solo emite SQL.
let db = new; // o Backend::MySql / Backend::Sqlite
Value
IntoValue cubre bool, i8..i64, u8..u64, f32/f64, &str,
String, Option<T>. Se infiere automáticamente.
Backends y placeholders
| Backend | Placeholder | Quote ident |
|---|---|---|
| Postgres | $1, $2… |
"col" |
| MySQL | ? |
`col` |
| SQLite | ? |
"col" |
Errores (QueryError)
InvalidIdentifier— ident con caracteres fuera de[A-Za-z0-9_.]InvalidOperator— operador desconocidoEmptyInList—where_in(..., [])EmptyRecord— INSERT sin columnasMissingWhere— UPDATE/DELETE sin WHERE (guard anti-foot-gun)BindMismatch—?yparamsdesalineados enwhere_rawDriver(String)— error del driver async (featureruntime-*)
SELECT
let = db.select
.columns
.distinct
.left_join
.where_eq
.where_op // operador como string
.where_op // formato Medoo equivalente
.where_in
.where_between
.where_not_null
.or_where
.group_by
.having
.order_desc
.limit
.offset
.to_sql?;
Operadores: = (==) · <> (!, !=) · > < >= <= ·
~/LIKE · !~/NOT LIKE · ~*/ILIKE. Formatos >= y [>=]
ambos válidos. Case-insensitive en palabras.
NULL automático: .where_eq("col", None::<&str>) → col IS NULL.
Solo = y <> aceptan NULL; otros operadores → InvalidOperator.
Atajos LIKE: .where_starts_with, .where_ends_with,
.where_contains, .where_ilike (auto-escapan % y _).
BETWEEN: .where_between("c", lo, hi) (valores) ·
.where_between_cols("c", "lo_col", "hi_col") (columnas) ·
.where_value_in_range(v, "lo", "hi") (valor contra rango).
Subqueries: .where_in_subquery, .where_not_in_subquery,
.where_exists, .where_not_exists, .where_scalar.
CTE: .with("name", sub) · .with_recursive_flag().
JOINs: inner_join · left_join · right_join · cross_join.
LATERAL (PG / MySQL 8+): inner_join_lateral · left_join_lateral ·
cross_join_lateral.
Raw fragments:
db.select
.where_eq
.where_raw;
Los ? se traducen al placeholder del backend.
Non-panic: try_where_op / try_where_eq para input dinámico
(retornan Result en vez de paniquear con operador inválido).
INSERT / UPSERT / RETURNING
db.insert
.set
.set // multi-row
.to_sql?;
// UPSERT: ON CONFLICT (PG/SQLite) / ON DUPLICATE KEY UPDATE (MySQL)
db.insert
.set
.on_conflict
.do_update; // o .do_nothing()
// RETURNING: PG, SQLite 3.35+, MariaDB 10.5+ (no MySQL clásico)
db.insert.set.returning;
db.insert.set.returning;
UPDATE / DELETE
Por defecto sin WHERE → QueryError::MissingWhere. Opt-in con
.allow_full_table().
db.update
.set
.set
.where_eq
.returning // PG / SQLite 3.35+ / MariaDB
.to_sql?;
db.delete
.where_op
.to_sql?;
Macros
record!
where_!
JSON
Path validado: keys [A-Za-z_][A-Za-z0-9_]*, índices [N].
db.select
.where_json
.where_json
.where_json; // IS NULL
let needle = json;
db.select.where_json_contains;
Render por backend:
- MySQL:
JSON_UNQUOTE(JSON_EXTRACT(...))·JSON_CONTAINS(...) - Postgres:
"col" #>> '{path}'·"col" @> $n::jsonb - SQLite:
json_extract(...)· contains →InvalidOperator
DDL
use ;
db.create_table
.if_not_exists
.col
.col
.col
.col
.col
.to_sql?;
db.drop_table.if_exists.cascade.to_sql?;
let sqls: = db.alter_table
.add_column
.drop_column
.rename_column
.rename_table
.to_sql?;
ColType cubre enteros (TinyInt..BigInt), Decimal(p,s), Bool,
flotantes, textos (Text / TinyText / MediumText / LongText /
Char(n) / Varchar(n)), Uuid, binarios (Bytes / Binary(n) /
VarBinary(n) / blobs), Json, fechas (Timestamp con TZ / DateTime
naive / Date / Time / TimeTz / Year), Enum/Set, Raw("...").
Cada uno se traduce al tipo nativo del backend — tabla completa en
MANUAL.md.
Charset / Collation (por columna o tabla, MySQL principal):
new
.charset
.collation;
db.create_table
.col
.engine
.default_charset
.default_collation;
CREATE / DROP DATABASE, vistas (incl. MATERIALIZED en PG), triggers (MySQL inline / SQLite con WHEN / PG con FUNCTION) y events (MySQL/MariaDB) — ver MANUAL.md.
Migraciones
Modelo + planificador. La ejecución la hace tu runtime async o el
pool (runtime-*).
use ;
let migrator = new
.add
.add;
let create = tracking_table_sql?;
let pending = migrator.pending?;
let rollback = migrator.rollback_plan?;
Detecta duplicados, ordena ascendente, soporta rollback hasta versión target.
Logging
Sink configurable + filtro por categoría (bitflags).
use ;
stdout
stderr
file?
buffer // -> (Logger, Arc<Mutex<Vec<u8>>>) para tests
READ | WRITE // INSERT|UPDATE|DELETE + READ
DDL | RAW
ALL | NONE
let db = new
.with_logger;
db.build?; // SELECT/INSERT/UPDATE/DELETE: auto
db.log_ddl; // DDL: manual
db.log_raw; // SQL crudo
Formato: [<unix_secs>s] [<CATEGORY>] <SQL> -- params: <Vec<Value>>.
q.to_sql() es puro (no loguea). db.build(&q) loguea como READ/INSERT/UPDATE/DELETE automáticamente.
Pool async — referencia completa
Activá una runtime-* y obtenés un Pool unificado sobre sqlx.
Conexión
use ;
// Defaults razonables
let pool = connect_mysql.await?;
let pool = connect_postgres.await?;
let pool = connect_sqlite.await?;
// Configurado
use Duration;
let opts = PoolOptions ;
let pool = connect_postgres_with.await?;
// Con retry inicial (boot resiliente)
let pool = connect_mysql_retry.await?;
// Logger sobre el pool
let pool = pool.with_logger;
Ejecución
// Builders re-expuestos en el Pool
let q = pool.select.where_eq;
let n = pool.execute.await?; // -> u64 (rows affected)
let rows = pool.fetch_all.await?; // Vec<HashMap<String, Value>>
let row = pool.fetch_one.await?; // o Driver("0 filas")
let opt = pool.fetch_optional.await?;
// SQL crudo
pool.execute_raw.await?;
pool.fetch_all_raw.await?;
// Accesores tipados sobre Row
let nombre: = row.get_str;
let edad: = row.get_i64;
let activo: = row.get_bool;
Transacciones
// Manual
let mut tx = pool.begin.await?;
tx.execute.await?;
tx.commit.await?; // o tx.rollback().await?
// drop sin commit -> rollback automático
// Closure (commit si Ok, rollback si Err)
pool.transaction.await?;
// Savepoints
tx.savepoint.await?;
tx.execute.await?;
tx.rollback_to_savepoint.await?; // o release_savepoint
Bulk
pool.execute_many.await?; // atómico en transacción
pool.execute_batch.await?; // Vec<u64>, sin tx (más rápido)
pool.execute_batch_raw.await?;
Streaming (datasets grandes)
pool.for_each_row.await?;
use StreamExt;
let s = pool.fetch_stream?;
let primeros: = s.take.collect.await;
// Tipado con FromRow
let s = pool.?;
Health + EXPLAIN
pool.ping.await?; // SELECT 1
let plan = pool.explain.await?;
let plan = pool.explain_analyze.await?; // PG: ANALYZE · SQLite: QUERY PLAN
Retry con backoff exponencial
pool.execute_retry.await?;
pool.fetch_all_retry.await?;
pool.execute_raw_retry.await?;
Reintenta solo transitorios (conexión caída, deadlock, serialización, timeout). Backoff: 50ms, 100, 200, 400... cap 5s. Errores de SQL/constraint salen al toque.
FromRow (mapeo a struct)
Manual — sin features extra:
use ;
let xs: = pool.fetch_all_as.await?;
let one: User = pool.fetch_one_as.await?;
let opt: = pool.fetch_optional_as.await?;
Derive — feature derive:
= { .., features = ["runtime-postgres", "derive"] }
use FromRow;
Tipos soportados: i8..i64, u8..u64, f32, f64, bool, String,
Vec<u8>, Option<T> de cualquiera.
chrono (fechas tipadas)
Feature chrono — agrega IntoValue para tipos chrono e accesores
tipados sobre Row.
= { .., features = ["runtime-postgres", "chrono"] }
use ;
use RowExtChrono;
// Insert: cualquier tipo chrono va por IntoValue (ISO 8601)
db.insert.set;
// Read: accesores tipados
let dt: = row.get_datetime_utc;
let nd: = row.get_naive_datetime;
let fecha: = row.get_date;
let hora: = row.get_time;
Sin la feature, el pool decodifica fechas como Value::Text (ISO 8601)
para no atar la lib a un crate de tiempo concreto.
Seguridad
| Punto de entrada | Defensa |
|---|---|
| Tabla / columna | Whitelist [A-Za-z_][A-Za-z0-9_]*, ≤64 chars |
| Operador (string) | Parser cerrado, error explícito |
| Valor de usuario | Siempre placeholder, jamás inline |
JOIN ... ON |
Solo ident = ident (con tabla.col) |
| Path JSON | Whitelist + índices numéricos, segmentos vacíos no |
default_raw |
Rechaza ; y -- |
Columnas con (...) |
Aceptadas como expr, rechazan ; y -- |
where_raw |
Cuenta ? vs params.len() → BindMismatch |
Tests de inyección por cada punto en tests/security.rs.
Tests
209 tests verde cubriendo builder, JSON, DDL, vistas, triggers, events, migraciones, logging, security, pool (sqlite real), derive y chrono.
Licencia
Licenciado bajo Apache License, Version 2.0.
Las contribuciones aportadas intencionalmente para inclusión en este proyecto, según se define en la licencia Apache-2.0, serán licenciadas bajo los mismos términos, sin ningún término o condición adicional.