2026-09-03 11:32:19 +00:00
|
|
|
mod affinities;
|
|
|
|
|
mod articles;
|
|
|
|
|
mod feeds;
|
2026-09-03 19:23:34 +00:00
|
|
|
mod models;
|
2026-09-03 11:32:19 +00:00
|
|
|
mod reading_events;
|
2026-09-03 19:23:34 +00:00
|
|
|
|
|
|
|
|
pub use models::RankedArticleRow;
|
2026-09-03 11:32:19 +00:00
|
|
|
pub(crate) mod schema;
|
2026-08-21 09:04:38 +00:00
|
|
|
|
2026-08-20 15:04:14 +00:00
|
|
|
use anyhow::Result;
|
2026-08-21 09:04:38 +00:00
|
|
|
use diesel::prelude::*;
|
|
|
|
|
use diesel::sqlite::SqliteConnection;
|
|
|
|
|
use diesel_async::pooled_connection::bb8::Pool;
|
|
|
|
|
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
|
|
|
|
|
use diesel_async::sync_connection_wrapper::SyncConnectionWrapper;
|
|
|
|
|
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
2026-08-20 15:04:14 +00:00
|
|
|
|
2026-08-21 09:04:38 +00:00
|
|
|
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
|
|
|
|
|
|
|
|
|
|
/// SQLite has no native async driver, so `diesel-async` wraps a blocking
|
|
|
|
|
/// `SqliteConnection` and runs it on a blocking thread under the hood.
|
|
|
|
|
type AsyncSqliteConnection = SyncConnectionWrapper<SqliteConnection>;
|
|
|
|
|
|
2026-08-20 15:04:14 +00:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct Db {
|
2026-08-21 09:04:38 +00:00
|
|
|
pool: Pool<AsyncSqliteConnection>,
|
2026-08-20 15:04:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Db {
|
2026-08-21 09:04:38 +00:00
|
|
|
/// `database_url` is a plain SQLite file path (e.g. "feedsignal.db"),
|
|
|
|
|
/// created if it doesn't exist. Everything lives in one file, no
|
|
|
|
|
/// separate database server to run.
|
|
|
|
|
pub async fn connect(database_url: &str) -> Result<Self> {
|
|
|
|
|
// `MigrationHarness` only works on a synchronous `Connection`, and
|
|
|
|
|
// this only runs once at boot, so open a throwaway sync connection
|
|
|
|
|
// just for it rather than pulling migrations through the async pool.
|
|
|
|
|
// Each pending migration runs in its own transaction (SQLite
|
|
|
|
|
// supports transactional DDL), and `down.sql` gives each one a
|
|
|
|
|
// matching revert.
|
|
|
|
|
let url = database_url.to_string();
|
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<()> {
|
|
|
|
|
let mut conn = SqliteConnection::establish(&url)?;
|
2026-08-21 11:10:08 +00:00
|
|
|
conn.run_pending_migrations(MIGRATIONS)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!(e))?;
|
2026-08-21 09:04:38 +00:00
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await??;
|
|
|
|
|
|
|
|
|
|
let manager = AsyncDieselConnectionManager::<AsyncSqliteConnection>::new(database_url);
|
|
|
|
|
let pool = Pool::builder().max_size(5).build(manager).await?;
|
2026-08-20 15:04:14 +00:00
|
|
|
Ok(Self { pool })
|
|
|
|
|
}
|
|
|
|
|
}
|