SQLite
SQLite is the default Rango backend — ideal for development, testing, and small applications.
Setup
# Cargo.toml
[dependencies]
rango-orm = { version = "0.2", default-features = false, features = ["sqlite"] }
# rango.toml
[database]
backend = "sqlite"
export DATABASE_URL=sqlite://db.sqlite3
# In-memory (for tests):
export DATABASE_URL=sqlite::memory:
Connecting
#![allow(unused)] fn main() { use rango::prelude::*; let pool = connect_sqlite("sqlite://db.sqlite3").await?; // or let pool = connect_sqlite("sqlite::memory:").await?; }
SQLite-specific features
Import SqliteQueryExt to unlock SQLite-only filter methods:
#![allow(unused)] fn main() { use rango::prelude::*; // SqliteQueryExt is included // json_extract — filter by JSON field value Post::filter(&pool) .json_extract_eq("metadata", "$.status", "published") .all() .await? // json_extract with LIKE Post::filter(&pool) .json_extract_like("metadata", "$.tags", "%rust%") .all() .await? // Check if JSON key exists Post::filter(&pool) .json_extract_exists("metadata", "$.featured_image") .all() .await? // FTS5 full-text search (requires a virtual FTS5 table) Article::filter(&pool) .fts5("articles_fts", "rust async") .all() .await? }
Type mapping
SQLite has fewer native types than Postgres. Rango maps transparently:
| Rango type | SQLite column |
|---|---|
FieldUuid | TEXT (UUID string) |
FieldBool | INTEGER (0/1) |
FieldDateTime | TEXT (ISO 8601) |
FieldJson | TEXT (serialized JSON) |
FieldVarchar | TEXT |
Limitations vs Postgres
| Feature | Postgres | SQLite |
|---|---|---|
| Native UUID | ✅ | ❌ (stored as TEXT) |
| JSONB operators | ✅ | ❌ (use json_extract) |
ALTER COLUMN TYPE | ✅ | ❌ |
SET/DROP NOT NULL | ✅ | ❌ |
| Full-text search | tsvector | FTS5 (virtual table) |
RETURNING * | ✅ | ✅ (SQLite ≥ 3.35) |
Errors
On SQLite a unique violation, including one on a partial unique index, reports the column names (user.email), not the index name, and a foreign key violation carries no constraint name. See Errors.
Testing with in-memory SQLite
Create the schema from the migration files rango makemigrations generated. Each file is plain SQL, and raw_execute runs a string with several statements on SQLite (give it no parameters).
#![allow(unused)] fn main() { use rango::prelude::*; const MIGRATIONS: &[&str] = &[ include_str!("../migrations/0001_user.sql"), include_str!("../migrations/0002_post.sql"), ]; async fn test_pool() -> SqlitePool { let pool = connect_sqlite("sqlite::memory:").await.unwrap(); for sql in MIGRATIONS { raw_execute(&pool, sql, vec![]).await.unwrap(); } pool } #[tokio::test] async fn my_test() { let pool = test_pool().await; // Test your logic... } }
List the files in order. A new migration means a new line here. No Docker, no external process: fast and isolated.
Production on PostgreSQL/MySQL, tests on SQLite
rango.toml fixes one backend, and makemigrations writes SQL for that backend only. A migration folder made for PostgreSQL does not run on SQLite (types, ALTER, index syntax differ). The supported setup is a second set of migrations:
- Keep your main
rango.toml(backend = "postgres") andmigrations/. - Add a folder, for example
tests-sqlite/, with its ownrango.tomlthat saysbackend = "sqlite". - From that folder, generate the SQLite files from the same models:
cd tests-sqlite && rango makemigrations ../src --output migrations. Commit them, and re-run after every model change. - Load those files in the test helper above (
include_str!("../tests-sqlite/migrations/0001_user.sql")).
Limits:
- You maintain two migration sets.
rango makemigrations --checkin each folder tells you when one is stale. - SQLite is not PostgreSQL. Tests do not see native UUID,
JSONBoperators,DECIMALscale,PgQueryExtmethods, or the locking and error details of the real server. - Decimal aggregates, partial unique indexes and constraint names behave differently (see above and Field types).
- Run a final test pass against the production engine before release.