Files
GoblinPay/crates/gp-core/src/db.rs
T
2ro bd67bfc92e GoblinPay: receive-only Grin payment server
A self-hostable Grin payment server for shops, creators, and sites: show a
code, Grin lands in your wallet, with a verifiable Grin payment proof on
receive. Workspace crates (gp-core / gp-nostr / gp-server / gp-wallet /
gp-goblin-sender), a WooCommerce connector, a hosted /pay/<token> checkout,
and NIP-44 v3 gift-wrapped payment DMs carried over the Nym mixnet. All
secrets are read from the environment; none are committed.
2026-07-02 04:29:54 -04:00

74 lines
2.6 KiB
Rust

//! SQLite persistence via raw `sqlx` (no ORM). One database file, zero-ops,
//! trivial backup. DB access stays behind this thin module so a later
//! Postgres swap is contained.
use sqlx::migrate::Migrator;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
/// Embedded migrations from the workspace-level `migrations/` directory.
pub static MIGRATOR: Migrator = sqlx::migrate!("../../migrations");
/// Open (creating if missing) the SQLite database at `db_path` and bring the
/// schema up to date. Called once at startup.
///
/// A single pooled connection: SQLite serializes writers anyway, and one
/// connection keeps the migrator (and every write) free of "database is
/// locked" contention. Fine for a low-traffic receive-only till; a later
/// Postgres swap for the multi-store backend lifts the ceiling.
pub async fn init(db_path: &str) -> Result<SqlitePool, sqlx::Error> {
let options = SqliteConnectOptions::new()
.filename(db_path)
.create_if_missing(true)
.busy_timeout(std::time::Duration::from_secs(10))
.foreign_keys(true);
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(options)
.await?;
MIGRATOR.run(&pool).await?;
Ok(pool)
}
/// A migrated in-memory database on a single shared connection, for tests.
#[cfg(test)]
pub(crate) async fn test_pool() -> SqlitePool {
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.expect("open in-memory sqlite");
MIGRATOR.run(&pool).await.expect("run migrations");
pool
}
#[cfg(test)]
mod tests {
use super::*;
async fn table_names(pool: &SqlitePool) -> Vec<String> {
sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
.fetch_all(pool)
.await
.unwrap()
}
#[tokio::test]
async fn init_creates_db_and_applies_migrations() {
let path = std::env::temp_dir().join(format!("gp-db-test-{}.db", std::process::id()));
let _ = std::fs::remove_file(&path);
let pool = init(path.to_str().unwrap()).await.unwrap();
let tables = table_names(&pool).await;
assert!(tables.contains(&"payment".to_string()), "{tables:?}");
assert!(tables.contains(&"invoice".to_string()), "{tables:?}");
pool.close().await;
// Re-opening an existing database re-runs the migrator harmlessly.
let pool = init(path.to_str().unwrap()).await.unwrap();
assert!(table_names(&pool).await.contains(&"payment".to_string()));
pool.close().await;
let _ = std::fs::remove_file(&path);
}
}