Homebrew RSS reader server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

db: use sqlx FromRow derive for row mapping

* removes manual row.get() calls in favor of derive macros
* uses #[sqlx(rename)] for columns with different struct field names
* uses #[sqlx(skip)] for constant fields like is_spark and is_saved
* removes unused sqlx::Row import

This reduces boilerplate and makes the mapping less error-prone since
field names are checked at compile time.

+31 -71
+31 -71
src/db.rs
··· 1 1 use anyhow::{Context, Result}; 2 2 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; 3 - use sqlx::{Row, SqlitePool}; 3 + use sqlx::SqlitePool; 4 4 use std::str::FromStr; 5 5 6 6 use crate::config::Config; ··· 87 87 88 88 // -- Fever API query types -- 89 89 90 - #[derive(Debug, serde::Serialize)] 90 + #[derive(Debug, serde::Serialize, sqlx::FromRow)] 91 91 pub struct Group { 92 92 pub id: i64, 93 + #[sqlx(rename = "name")] 93 94 pub title: String, 94 95 } 95 96 96 - #[derive(Debug, serde::Serialize)] 97 + #[derive(Debug, serde::Serialize, sqlx::FromRow)] 97 98 pub struct FeedGroup { 98 99 pub group_id: i64, 99 100 pub feed_ids: String, 100 101 } 101 102 102 - #[derive(Debug, serde::Serialize)] 103 + #[derive(Debug, serde::Serialize, sqlx::FromRow)] 103 104 pub struct Feed { 104 105 pub id: i64, 105 106 pub favicon_id: i64, 106 107 pub title: String, 107 108 pub url: String, 108 109 pub site_url: String, 110 + #[sqlx(skip)] 109 111 pub is_spark: i32, 112 + #[sqlx(rename = "last_fetched_at")] 110 113 pub last_updated_on_time: i64, 111 114 } 112 115 113 - #[derive(Debug, serde::Serialize)] 116 + #[derive(Debug, serde::Serialize, sqlx::FromRow)] 114 117 pub struct Item { 115 118 pub id: i64, 116 119 pub feed_id: i64, 117 120 pub title: String, 118 121 pub author: String, 122 + #[sqlx(rename = "content")] 119 123 pub html: String, 120 124 pub url: String, 125 + #[sqlx(skip)] 121 126 pub is_saved: i32, 122 127 pub is_read: i32, 128 + #[sqlx(rename = "ts")] 123 129 pub created_on_time: i64, 124 130 } 125 131 126 - #[derive(Debug, serde::Serialize)] 132 + #[derive(Debug, serde::Serialize, sqlx::FromRow)] 127 133 pub struct Favicon { 128 134 pub id: i64, 129 135 pub data: String, 130 136 } 131 137 132 138 pub async fn get_groups(pool: &SqlitePool) -> Result<Vec<Group>> { 133 - let rows: Vec<(i64, String)> = sqlx::query_as("SELECT id, name FROM groups") 139 + let groups = sqlx::query_as::<_, Group>("SELECT id, name FROM groups") 134 140 .fetch_all(pool) 135 141 .await?; 136 - Ok(rows 137 - .into_iter() 138 - .map(|(id, name)| Group { id, title: name }) 139 - .collect()) 142 + Ok(groups) 140 143 } 141 144 142 145 pub async fn get_feeds_groups(pool: &SqlitePool) -> Result<Vec<FeedGroup>> { 143 - let rows = sqlx::query( 146 + let feed_groups = sqlx::query_as::<_, FeedGroup>( 144 147 "SELECT group_id, GROUP_CONCAT(id) as feed_ids FROM feeds GROUP BY group_id", 145 148 ) 146 149 .fetch_all(pool) 147 150 .await?; 148 - 149 - Ok(rows 150 - .iter() 151 - .map(|row| FeedGroup { 152 - group_id: row.get("group_id"), 153 - feed_ids: row.get::<String, _>("feed_ids"), 154 - }) 155 - .collect()) 151 + Ok(feed_groups) 156 152 } 157 153 158 154 pub async fn get_feeds(pool: &SqlitePool) -> Result<Vec<Feed>> { 159 - let rows = sqlx::query( 155 + let feeds = sqlx::query_as::<_, Feed>( 160 156 "SELECT id, COALESCE(favicon_id, 0) as favicon_id, title, url, site_url, \ 161 157 COALESCE(last_fetched_at, 0) as last_fetched_at FROM feeds", 162 158 ) 163 159 .fetch_all(pool) 164 160 .await?; 165 - 166 - Ok(rows 167 - .iter() 168 - .map(|row| Feed { 169 - id: row.get("id"), 170 - favicon_id: row.get("favicon_id"), 171 - title: row.get("title"), 172 - url: row.get("url"), 173 - site_url: row.get("site_url"), 174 - is_spark: 0, 175 - last_updated_on_time: row.get("last_fetched_at"), 176 - }) 177 - .collect()) 161 + Ok(feeds) 178 162 } 179 163 180 164 pub async fn get_items( ··· 183 167 max_id: Option<i64>, 184 168 with_ids: Option<&[i64]>, 185 169 ) -> Result<Vec<Item>> { 186 - let rows = if let Some(ids) = with_ids { 170 + let items = if let Some(ids) = with_ids { 187 171 let placeholders: Vec<&str> = ids.iter().map(|_| "?").collect(); 188 172 let sql = format!( 189 173 "SELECT id, feed_id, title, author, content, url, is_read, \ ··· 191 175 FROM items WHERE id IN ({}) ORDER BY id DESC", 192 176 placeholders.join(",") 193 177 ); 194 - let mut query = sqlx::query(&sql); 178 + let mut query = sqlx::query_as::<_, Item>(&sql); 195 179 for id in ids { 196 180 query = query.bind(id); 197 181 } 198 182 query.fetch_all(pool).await? 199 183 } else if let Some(since) = since_id { 200 - sqlx::query( 184 + sqlx::query_as::<_, Item>( 201 185 "SELECT id, feed_id, title, author, content, url, is_read, \ 202 186 COALESCE(published_at, created_at) as ts \ 203 187 FROM items WHERE id > ? ORDER BY id ASC LIMIT 50", ··· 206 190 .fetch_all(pool) 207 191 .await? 208 192 } else if let Some(max) = max_id { 209 - sqlx::query( 193 + sqlx::query_as::<_, Item>( 210 194 "SELECT id, feed_id, title, author, content, url, is_read, \ 211 195 COALESCE(published_at, created_at) as ts \ 212 196 FROM items WHERE id < ? ORDER BY id DESC LIMIT 50", ··· 215 199 .fetch_all(pool) 216 200 .await? 217 201 } else { 218 - sqlx::query( 202 + sqlx::query_as::<_, Item>( 219 203 "SELECT id, feed_id, title, author, content, url, is_read, \ 220 204 COALESCE(published_at, created_at) as ts \ 221 205 FROM items ORDER BY id DESC LIMIT 50", ··· 223 207 .fetch_all(pool) 224 208 .await? 225 209 }; 226 - 227 - Ok(rows 228 - .iter() 229 - .map(|row| Item { 230 - id: row.get("id"), 231 - feed_id: row.get("feed_id"), 232 - title: row.get("title"), 233 - author: row.get("author"), 234 - html: row.get("content"), 235 - url: row.get("url"), 236 - is_saved: 0, 237 - is_read: row.get("is_read"), 238 - created_on_time: row.get("ts"), 239 - }) 240 - .collect()) 210 + Ok(items) 241 211 } 242 212 243 213 pub async fn get_unread_item_ids(pool: &SqlitePool) -> Result<String> { ··· 250 220 } 251 221 252 222 pub async fn get_favicons(pool: &SqlitePool) -> Result<Vec<Favicon>> { 253 - let rows: Vec<(i64, String)> = sqlx::query_as("SELECT id, data FROM favicons") 223 + let favicons = sqlx::query_as::<_, Favicon>("SELECT id, data FROM favicons") 254 224 .fetch_all(pool) 255 225 .await?; 256 - Ok(rows 257 - .into_iter() 258 - .map(|(id, data)| Favicon { id, data }) 259 - .collect()) 226 + Ok(favicons) 260 227 } 261 228 262 229 // -- Fetcher queries -- 263 230 231 + #[derive(sqlx::FromRow)] 264 232 pub struct FeedRow { 265 233 pub id: i64, 266 234 pub url: String, ··· 268 236 } 269 237 270 238 pub async fn get_all_feeds(pool: &SqlitePool) -> Result<Vec<FeedRow>> { 271 - let rows: Vec<(i64, String, Option<i64>)> = 272 - sqlx::query_as("SELECT id, url, favicon_id FROM feeds") 273 - .fetch_all(pool) 274 - .await?; 275 - Ok(rows 276 - .into_iter() 277 - .map(|(id, url, favicon_id)| FeedRow { 278 - id, 279 - url, 280 - favicon_id, 281 - }) 282 - .collect()) 239 + let feeds = sqlx::query_as::<_, FeedRow>("SELECT id, url, favicon_id FROM feeds") 240 + .fetch_all(pool) 241 + .await?; 242 + Ok(feeds) 283 243 } 284 244 285 245 pub async fn upsert_feed_metadata(