//! SQLite pool initialization and projection-cursor persistence. //! //! Two durable stores back the server: //! 1. Hydrant's fjall store (`HYDRANT_DATABASE_PATH`) for raw ATProto records. //! 2. This SQLite projection database (`DATABASE_URL`) for app-specific derived //! views. The pool here owns the second. use std::str::FromStr; use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; use sqlx::{Sqlite, SqlitePool, Transaction}; use super::config::ServerConfig; /// Open the SQLite pool, enable WAL mode, run migrations, and return the pool. /// /// WAL mode is set on the connection options (not in a migration) because SQLite /// silently ignores `journal_mode` changes inside the transaction sqlx uses to /// run migrations. pub async fn init_db(cfg: &ServerConfig) -> anyhow::Result { ensure_data_dir(&cfg.database_url)?; let options = SqliteConnectOptions::from_str(&cfg.database_url)? .create_if_missing(true) .journal_mode(SqliteJournalMode::Wal); let pool = SqlitePoolOptions::new() .max_connections(5) .connect_with(options) .await?; sqlx::migrate!("./migrations").run(&pool).await?; tracing::info!("sqlite migrations applied"); Ok(pool) } fn ensure_data_dir(db_url: &str) -> anyhow::Result<()> { if let Some(path) = db_url.strip_prefix("sqlite:") && let Some(parent) = std::path::Path::new(path).parent() { std::fs::create_dir_all(parent)?; } Ok(()) } struct CursorRow { value: String, } /// Load the last persisted projection cursor (the Hydrant event id), if any. pub async fn load_cursor(db: &SqlitePool) -> anyhow::Result> { let row: Option = sqlx::query_as!( CursorRow, "SELECT value FROM projection_state WHERE key = ?", "cursor" ) .fetch_optional(db) .await?; Ok(row.and_then(|r| r.value.parse().ok())) } /// Persist the projection cursor outside a projection transaction. /// /// Used for events that are acknowledged but not projected (e.g. account status /// events), so the durable cursor advances past them and a reconnect/resume does /// not replay them indefinitely. pub async fn advance_cursor(db: &SqlitePool, cursor: u64) -> anyhow::Result<()> { let mut tx = db.begin().await?; save_cursor(&mut tx, cursor).await?; tx.commit().await?; Ok(()) } /// Persist the projection cursor inside an in-flight projection transaction. pub async fn save_cursor(tx: &mut Transaction<'_, Sqlite>, cursor: u64) -> anyhow::Result<()> { let now = chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"); sqlx::query!( "INSERT INTO projection_state (key, value, updated_at) VALUES ('cursor', ?, ?) \ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at", cursor.to_string(), now ) .execute(&mut **tx) .await?; Ok(()) }