Chapter 16 — Every feature on every backend
"Tri-dialect everywhere": every framework surface runs on PostgreSQL, MySQL 8+, and SQLite out of the box. Concretely:
16.220 — Multi-tenant runserver on any backend
What: Cli::tenancy().run().await and server::Builder boot
the operator console + tenant admin + host-based dispatch on PG,
MySQL, or SQLite.
Recipe:
// Same code on every backend — only DATABASE_URL changes.
#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
rustango::manage::Cli::new()
.tenancy()
.api(my_app::urls::router())
.run().await
}
On PG: DATABASE_URL=postgres://…. On MySQL: DATABASE_URL=mysql://….
On SQLite: DATABASE_URL=sqlite:./var/registry.db?mode=rwc.
server::Builder<DB> is generic over the registry backend.
Builder::from_env() is the PG-default constructor;
Builder::<DB>::from_pool(pool, url, apex) is the explicit-backend
constructor for non-PG.
16.221 — Storage modes — pick the right one
| Mode | Backends | Use when |
|---|---|---|
database (default) | PG, MySQL, SQLite | Enterprise B2B; compliance; geographic sharding; small-to-medium N |
schema | Postgres only | High-N SaaS on PG (500+ tenants); shared connection pool matters |
On MySQL/SQLite, Org.storage_mode = "schema" returns a clear
runtime validation error pointing the user at database (semantics
equivalent on those backends — one DB / file per tenant).
16.222 — Jobs queue, per-backend pickup
PgJobQueue (name kept for back-compat) now runs on PG / MySQL 8+ /
SQLite. PG + MySQL 8+ use FOR UPDATE SKIP LOCKED for atomic
multi-worker pickup; SQLite uses a transaction-bounded
UPDATE … WHERE id = (SELECT id … LIMIT 1) RETURNING … (SQLite
serializes writers globally so the pickup is implicitly mutually-
exclusive).
let pool = rustango::sql::Pool::connect("sqlite:./var/jobs.db?mode=rwc").await?;
rustango::jobs::pg::PgJobQueue::ensure_table_pool(&pool).await?;
let queue = std::sync::Arc::new(
rustango::jobs::pg::PgJobQueue::with_workers_pool(pool, 1)
);
queue.register::<SendWelcomeEmail>().await;
queue.start().await;
queue.dispatch(&SendWelcomeEmail { user_id: 42 }).await?;
Cargo.toml: rustango = { features = ["sqlite", "jobs-postgres"] }.
The feature name is preserved for back-compat — the queue itself is
no longer PG-only.
16.223 — manage inspectdb on any backend
PG/MySQL use information_schema; SQLite uses PRAGMA table_info
sqlite_master. Emits per-dialect type-mapped#[derive(Model)]source.
# Postgres
cargo run -- inspectdb --schema public
# MySQL — `--schema` is the database name (DATABASE() default)
cargo run -- inspectdb
# SQLite — `--schema` is ignored
cargo run -- inspectdb --table users
16.224 — Media on any backend
The media Cargo feature no longer requires postgres. Every
MediaManager method dispatches per-dialect; PG-specific SQL idioms
(ANY($1), NOW() - INTERVAL, DELETE … USING, ON CONFLICT DO UPDATE, INSERT … RETURNING) translated to portable equivalents.
# Tri-dialect media
rustango = { version = "0.38", default-features = false, features = ["sqlite", "media", "storage"] }
let pool = rustango::sql::Pool::connect("sqlite:./var/app.db?mode=rwc").await?;
rustango::media::ensure_all_tables_pool(&pool).await?;
let manager = MediaManager::new_pool(pool, registry);
let m = manager.save_bytes(opts).await?;
let m = manager.get(m.id.get().copied().unwrap()).await?.unwrap();
16.225 — Permissions facade, fixtures, auth
The top-level rustango::permissions::*_for_model_pool<T> typed
helpers, tenancy::auth::authenticate_user_pool, and fixtures:: load_all_pool / Fixture::load_into_pool all run on any backend.
Tests covering this chapter
- PG live tests — every existing suite still green (1386 lib tests on PG; 22 PG media live; 4 PG jobs live; 3 PG inspectdb live).
- SQLite live tests:
media_sqlite_live— save → get → delete → purge round-trip; collection CRUD; tag lifecycle (ON CONFLICT, IGNORE, subquery DELETE, popular_tags aggregate).jobs_sqlite_live— dispatch persists + drains; pending_count sweep.inspectdb_sqlite_live—Auto<i64>PK + max_length + FK +Option<String>nullable;--tablefiltering.