SeaORM: items and read state

The item list, its counts, filters, sorts and search, positions, pins and
mark-all-read move to SeaORM, as SQL written for both databases:

- Parameters are gathered as the SQL is written (Args), so only what a
  statement uses is bound. rusqlite needed every one mentioned, hence the old
  `?1 IS NULL` and `?2 = ''`; Postgres refuses a parameter it cannot type.
- Yes/no columns are tested as booleans (NOT coalesce(s.read, false)) and
  written as true, not 1; SQLite reads true and false as 1 and 0.
- The last tiebreak of the sort is the guid, not SQLite's rowid, which Postgres
  lacks. Only items with the same date change places.
- set_position names entry_state.duration beside excluded.duration.
- The status callback on the control socket returns a future, as the counts
  are now a query.

Checked on a copy of production against the live server: 42 of 48 lists
identical; the other six differ only in how ties fall, or because the test
daemon cleared paths to files this machine does not have. Run on the same file,
every filter's count matches the old SQL exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 18:32:46 +00:00
parent d7f8f2df1d
commit 6bf1ad6b31
4 changed files with 234 additions and 207 deletions

View File

@@ -740,7 +740,7 @@ async fn feeds(
last_error: s.last_error,
entries: s.entries,
downloaded: s.downloaded,
unread: state.ctx.db.unread_count(user.id, id)?,
unread: state.ctx.db.unread_count(user.id, id).await?,
subscribers: counts.get(id).copied().unwrap_or(0),
pinned: pinned.contains(id),
});
@@ -1124,7 +1124,7 @@ async fn entries(
user: crate::db::User,
Query(page): Query<Page>,
) -> Result<Json<EntryPage>, ApiError> {
entry_page(&state, user.id, Some(&id), &page)
entry_page(&state, user.id, Some(&id), &page).await
}
/// Every subscribed feed's items together, newest first: All Subscriptions.
@@ -1133,11 +1133,11 @@ async fn all_entries(
user: crate::db::User,
Query(page): Query<Page>,
) -> Result<Json<EntryPage>, ApiError> {
entry_page(&state, user.id, None, &page)
entry_page(&state, user.id, None, &page).await
}
/// One feed's page of items, or every subscribed feed's when `feed` is None.
fn entry_page(
async fn entry_page(
state: &WebState,
user_id: i64,
feed: Option<&str>,
@@ -1153,14 +1153,14 @@ fn entry_page(
filter != crate::db::Filter::InProgress,
);
let mut rows =
db.entries_in(user_id, feed, filter, search, page.offset, page.limit.clamp(1, 200), &order)?;
db.entries_in(user_id, feed, filter, search, page.offset, page.limit.clamp(1, 200), &order).await?;
let mut sanitizer = feed_sanitizer();
for row in &mut rows {
if let Some(d) = &row.description {
row.description = Some(clean_description(&mut sanitizer, d, row.link.as_deref()));
}
}
let total = db.count_in(user_id, feed, filter, search)?;
let total = db.count_in(user_id, feed, filter, search).await?;
Ok(Json(EntryPage { total, entries: rows }))
}
@@ -1440,10 +1440,10 @@ async fn set_flags(
) -> Result<StatusCode, ApiError> {
use crate::db::EntryFlag;
if let Some(v) = body.read {
state.ctx.db.set_entry_flag(user.id, &feed_id, &guid, EntryFlag::Read, v)?;
state.ctx.db.set_entry_flag(user.id, &feed_id, &guid, EntryFlag::Read, v).await?;
}
if let Some(v) = body.flagged {
state.ctx.db.set_entry_flag(user.id, &feed_id, &guid, EntryFlag::Flagged, v)?;
state.ctx.db.set_entry_flag(user.id, &feed_id, &guid, EntryFlag::Flagged, v).await?;
}
Ok(StatusCode::NO_CONTENT)
}
@@ -1598,7 +1598,7 @@ async fn set_position(
user: crate::db::User,
Json(body): Json<Position>,
) -> Result<StatusCode, ApiError> {
state.ctx.db.set_position(user.id, &feed_id, &guid, body.secs, body.duration)?;
state.ctx.db.set_position(user.id, &feed_id, &guid, body.secs, body.duration).await?;
Ok(StatusCode::NO_CONTENT)
}
@@ -1615,7 +1615,7 @@ async fn read_all(
.filter(|s| s.cfg.group.as_deref() == Some(id.as_str()))
.map(|s| s.id),
);
let n = state.ctx.db.mark_all_read(user.id, &ids)?;
let n = state.ctx.db.mark_all_read(user.id, &ids).await?;
Ok(Json(serde_json::json!({ "marked": n })))
}
@@ -1627,7 +1627,7 @@ async fn read_all_mine(
) -> Result<Json<serde_json::Value>, ApiError> {
let ids: Vec<String> =
state.ctx.db.subscriptions_for(user.id).await?.into_iter().map(|s| s.feed_id).collect();
let n = state.ctx.db.mark_all_read(user.id, &ids)?;
let n = state.ctx.db.mark_all_read(user.id, &ids).await?;
Ok(Json(serde_json::json!({ "marked": n })))
}