sqlx-sqlite
Injects an sqlx::SqlitePool into services using injectable. Use when setting up a SQLite database connection pool, running migrations in post_construct, or sharing a pool across multiple services.
SKILL.mdskills/sqlx-sqlite/SKILL.md
GitHubSQLite with sqlx
Use #[injectable(inject(use_factory_async = self::make_db_pool))] when a
service needs a sqlx pool created asynchronously from injectable config.
Pool factory
use injectable::prelude::*;
use sqlx::{Pool, Sqlite};
#[injectable(factory)]
async fn make_db_pool(cfg: Inject<AppConfig>) -> Result<Pool<Sqlite>, sqlx::Error> {
sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(5)
.connect(&cfg.database_url)
.await
}
Purpose:
- Open the database pool with async I/O before the service is used.
- Keep connection details in one place instead of duplicating setup logic across services.
- Let the service keep a plain
Pool<Sqlite>field while DI handles creation.
Service with pool + migration
#[injectable]
struct Database {
#[injectable(inject(use_factory_async = self::make_db_pool))]
pool: Pool<Sqlite>,
}
#[injectable]
impl Database {
#[injectable(post_construct)]
async fn migrate(&self) -> Result<(), sqlx::Error> {
sqlx::query(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)",
)
.execute(&self.pool)
.await?;
Ok(())
}
#[injectable(pre_destruct)]
async fn close(&self) {
self.pool.close().await;
}
}
Implementation steps:
- Add a
#[injectable(factory)] async fn make_db_pool(...). - Read
Inject<AppConfig>or other injectable dependencies in the factory. - Annotate the field or constructor parameter with
#[injectable(inject(use_factory_async = self::make_db_pool))]. - Use
#[injectable(post_construct)]for migrations or warm-up queries. - Use
#[injectable(pre_destruct)]to close the pool cleanly on shutdown.
Sharing pool across services
// If multiple services each use use_factory_async directly, each service gets
// its own Pool instance. For sqlite::memory:, that means separate in-memory
// databases. Use a single-connection pool if you intentionally keep one pool
// per service.
#[injectable(factory)]
async fn make_shared_pool(cfg: Inject<AppConfig>) -> Result<Pool<Sqlite>, sqlx::Error> {
sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1) // required for sqlite::memory:
.idle_timeout(None)
.max_lifetime(None)
.connect(&cfg.database_url)
.await
}
#[injectable(inject(use_factory_async = make_db_pool))] is not a cross-service
singleton by itself. The factory runs once per construction of the owning
service type. Since services are singleton by default, that usually means one
pool per service, not one pool for the entire application.
One pool shared by many services
Wrap the external pool in your own singleton #[injectable] type, then inject
that wrapper everywhere else:
use injectable::prelude::*;
use sqlx::{Pool, Sqlite};
#[injectable(factory)]
async fn make_db_pool(cfg: Inject<AppConfig>) -> Result<Pool<Sqlite>, sqlx::Error> {
println!(" [DB] Connecting to {}", cfg.database_url);
sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.idle_timeout(None)
.max_lifetime(None)
.connect(&cfg.database_url)
.await
}
#[injectable]
pub struct DbPool {
#[injectable(inject(use_factory_async = make_db_pool))]
pub pool: Pool<Sqlite>,
}
#[injectable]
pub struct AuthService {
db: Inject<DbPool>,
}
#[injectable]
pub struct UserService {
db: Inject<DbPool>,
}
Why this works:
DbPoolis singleton by default.- Its
poolfield is built once whenDbPoolis first constructed. AuthServiceandUserServiceboth resolve the sameArc<DbPool>.make_db_pooltherefore runs exactly once for the shared wrapper.
Inject<DbPool> vs Arc<DbPool>
If you prefer Arc<DbPool> instead of Inject<DbPool>, that works too:
#[injectable]
pub struct AuthService {
#[injectable(inject)]
db: Arc<DbPool>,
}
#[injectable]
pub struct UserService {
#[injectable(inject)]
db: Arc<DbPool>,
}
These two forms use the same singleton cache path:
Inject<DbPool>auto-injects and is the most idiomatic injectable field type.Arc<DbPool>requires#[injectable(inject)].- Both point to the same shared singleton instance of
DbPool.
Choose Inject<DbPool> when you want the standard injectable wrapper API.
Choose Arc<DbPool> when the service should store a plain Arc field.
Config
#[injectable]
impl AppConfig {
#[injectable(ctor)]
fn new() -> Self {
Self {
database_url: std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite::memory:".into()),
}
}
}
Querying
pub async fn find_user(&self, id: i64) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>("SELECT id, email FROM users WHERE id = ?")
.bind(id)
.fetch_one(&self.pool)
.await
}
Related async-init macros to audit alongside this pattern:
#[injectable(factory)]for async pool creation#[injectable(inject(use_factory_async = ...))]for injection#[injectable(post_construct)]for migrations and warm-up