SeaORM beside rusqlite: entities, schema sync, a second connection
The first step of moving to SeaORM (#18, phase 1). Nothing a user sees changes. - src/entity.rs: the seven tables as SeaORM entities, matching the SQLite schema. Strings are Text, as the columns are; yes/no columns are bool, which is BOOLEAN on Postgres and stays INTEGER in the existing SQLite file (sync notes the difference and leaves it alone). - Db holds a SeaORM connection to the same SQLite file beside the rusqlite one; functions move to it one at a time, and rusqlite goes with the last of them. - db::sync creates what a database is missing from the entities (SeaORM's schema-sync, experimental, so sea-orm is pinned to ~2.0), plus the two indexes an entity cannot express. Checked against a copy of production: it added the lower(name) index and changed nothing else. - Test databases are now built from the entities alone, in a temporary file (two connections to one ":memory:" are two databases), so every test also checks that the entities describe what the queries need. That caught the one difference: finding a user by name relied on COLLATE NOCASE, which Postgres lacks; it now compares lower() on both sides. - rusqlite steps back to 0.39: 0.40's libsqlite3-sys is newer than sqlx accepts, and only one may link SQLite. It goes away at the end of this phase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
29
src/main.rs
29
src/main.rs
@@ -1,6 +1,7 @@
|
||||
mod auth;
|
||||
mod config;
|
||||
mod db;
|
||||
mod entity;
|
||||
mod download;
|
||||
mod feed;
|
||||
mod ipc;
|
||||
@@ -179,7 +180,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
let config_path = cli.config.clone().unwrap_or_else(config::config_path);
|
||||
let cfg = config::Config::load(&config_path)?;
|
||||
let db = db::Db::open(&config::data_dir().join("state.db"))?;
|
||||
let db = db::Db::open(&config::data_dir().join("state.db")).await?;
|
||||
|
||||
// A daemon owns the state; don't have two processes downloading the same thing.
|
||||
let wire_cmd = match &cli.command {
|
||||
@@ -1690,8 +1691,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shared_feed_is_fetched_for_whoever_wants_the_most() {
|
||||
#[tokio::test]
|
||||
async fn a_shared_feed_is_fetched_for_whoever_wants_the_most() {
|
||||
// Nobody subscribed: the feed's own settings stand, as in a single-user install.
|
||||
let p = merge_policy(&[], &feed(), 3);
|
||||
assert!(p.auto_download);
|
||||
@@ -1721,10 +1722,10 @@ mod tests {
|
||||
assert!(p.auto_download);
|
||||
}
|
||||
|
||||
fn test_ctx(cfg: config::Config) -> Ctx {
|
||||
async fn test_ctx(cfg: config::Config) -> Ctx {
|
||||
Ctx {
|
||||
cfg: std::sync::RwLock::new(Arc::new(cfg)),
|
||||
db: db::Db::memory().unwrap(),
|
||||
db: db::Db::memory().await.unwrap(),
|
||||
client: reqwest::Client::new(),
|
||||
out: Emitter::terminal(),
|
||||
torrents: tokio::sync::OnceCell::new(),
|
||||
@@ -1734,11 +1735,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_derived_feed_is_not_scanned_once_its_opml_leaves_config() {
|
||||
#[tokio::test]
|
||||
async fn a_derived_feed_is_not_scanned_once_its_opml_leaves_config() {
|
||||
// davewiner: the OPML subscription left config.toml, but its 922 derived rows
|
||||
// stayed in the database and kept being scanned under the no-parent fallback.
|
||||
let ctx = test_ctx(config::Config::default());
|
||||
let ctx = test_ctx(config::Config::default()).await;
|
||||
ctx.db.upsert_managed("child", "http://x/child.xml", "Child", "gone-opml").unwrap();
|
||||
assert!(
|
||||
subscriptions(&ctx).unwrap().iter().all(|s| s.id != "child"),
|
||||
@@ -1746,9 +1747,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retiring_a_group_drops_what_was_never_downloaded_and_orphans_the_rest() {
|
||||
let ctx = test_ctx(config::Config::default());
|
||||
#[tokio::test]
|
||||
async fn retiring_a_group_drops_what_was_never_downloaded_and_orphans_the_rest() {
|
||||
let ctx = test_ctx(config::Config::default()).await;
|
||||
ctx.db.upsert_managed("empty", "http://x/empty.xml", "Empty", "parent").unwrap();
|
||||
ctx.db.upsert_managed("has-file", "http://x/has-file.xml", "Has File", "parent").unwrap();
|
||||
let enc = feed::Enclosure { url: "http://x/ep.mp3".into(), mime: None, length: None };
|
||||
@@ -1763,14 +1764,14 @@ mod tests {
|
||||
assert!(ctx.db.feed_summary("has-file").unwrap().orphaned, "and flagged as orphaned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stranded_group_is_retired_but_a_promoted_feed_keeps_its_entries() {
|
||||
#[tokio::test]
|
||||
async fn a_stranded_group_is_retired_but_a_promoted_feed_keeps_its_entries() {
|
||||
// davewiner: the OPML left config before retire_group existed, and 11 of its feeds
|
||||
// promoted to config since still said managed = 1.
|
||||
let mut cfg = config::Config::default();
|
||||
cfg.feeds.insert("promoted".into(), feed());
|
||||
cfg.feeds.insert("live-opml".into(), feed());
|
||||
let ctx = test_ctx(cfg);
|
||||
let ctx = test_ctx(cfg).await;
|
||||
ctx.db.upsert_managed("promoted", "http://x/p.xml", "Promoted", "gone-opml").unwrap();
|
||||
ctx.db.upsert_managed("empty", "http://x/e.xml", "Empty", "gone-opml").unwrap();
|
||||
ctx.db.upsert_managed("listed", "http://x/l.xml", "Listed", "live-opml").unwrap();
|
||||
|
||||
Reference in New Issue
Block a user