use super::{PgPool, Uuid}; /// Track a thread (upsert). #[tracing::instrument(skip_all)] pub async fn track_thread( pool: &PgPool, user_id: Uuid, thread_id: Uuid, ) -> Result<(), sqlx::Error> { sqlx::query!( "INSERT INTO tracked_threads (user_id, thread_id) VALUES ($1, $2) ON CONFLICT (user_id, thread_id) DO NOTHING", user_id, thread_id, ) .execute(pool) .await?; Ok(()) } /// Untrack a thread. #[tracing::instrument(skip_all)] pub async fn untrack_thread( pool: &PgPool, user_id: Uuid, thread_id: Uuid, ) -> Result<(), sqlx::Error> { sqlx::query!( "DELETE FROM tracked_threads WHERE user_id = $1 AND thread_id = $2", user_id, thread_id, ) .execute(pool) .await?; Ok(()) } /// Stop tracking all threads for a user. #[tracing::instrument(skip_all)] pub async fn untrack_all(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM tracked_threads WHERE user_id = $1", user_id) .execute(pool) .await?; Ok(()) } /// Advance the read position for a tracked thread to `last_post_id`. /// /// Monotonic: the position only ever moves forward in thread order. The caller /// (`routes::forum::thread`) bumps to the last post *on the page being viewed*, /// so an unguarded write would drag the position backward whenever a reader /// revisits an earlier page, and `list_tracked_threads` would then re-count /// already-read posts as unread. The two writes are also spawned off the /// response path, so concurrent views of different pages race; keeping the /// comparison in SQL means Postgres re-evaluates it against the winning row /// under read-committed rather than letting the last writer win. /// /// A `last_post_id` that names no row leaves the position untouched: the /// scalar subquery yields NULL, the comparison is not true, and the guard /// fails closed. #[tracing::instrument(skip_all)] pub async fn update_read_position( pool: &PgPool, user_id: Uuid, thread_id: Uuid, last_post_id: Uuid, ) -> Result<(), sqlx::Error> { sqlx::query!( "UPDATE tracked_threads SET last_read_post_id = $3 WHERE user_id = $1 AND thread_id = $2 AND (last_read_post_id IS NULL OR (SELECT np.created_at FROM posts np WHERE np.id = $3) > (SELECT cp.created_at FROM posts cp WHERE cp.id = last_read_post_id))", user_id, thread_id, last_post_id, ) .execute(pool) .await?; Ok(()) }