Skip to main content

max / balanced_breakfast

3.4 KB · 105 lines History Blame Raw
1 //! BalancedBreakfast Database Layer
2
3 pub mod id_types;
4 pub mod models;
5 pub mod repository;
6
7 pub use id_types::*;
8 pub use models::*;
9 pub use repository::*;
10
11 use sqlx::sqlite::SqlitePoolOptions;
12 use sqlx::SqlitePool;
13
14 /// SQLite timestamp format used for all datetime columns.
15 pub const TIMESTAMP_FMT: &str = "%Y-%m-%d %H:%M:%S";
16
17 #[derive(Clone)]
18 /// SQLite database connection pool
19 pub struct Database {
20 pool: SqlitePool,
21 }
22
23 impl Database {
24 /// Create a new database connection
25 #[tracing::instrument(skip_all)]
26 pub async fn connect(database_url: &str) -> Result<Self, sqlx::Error> {
27 let pool = SqlitePoolOptions::new()
28 .max_connections(16)
29 .after_connect(|conn, _meta| {
30 Box::pin(async move {
31 sqlx::query("PRAGMA foreign_keys = ON")
32 .execute(&mut *conn)
33 .await?;
34 Ok(())
35 })
36 })
37 .connect(database_url)
38 .await?;
39
40 Ok(Self { pool })
41 }
42
43 /// Run migrations
44 #[tracing::instrument(skip_all)]
45 pub async fn migrate(&self) -> Result<(), sqlx::migrate::MigrateError> {
46 sqlx::migrate!("../../migrations/sqlite").run(&self.pool).await
47 }
48
49 /// Get the connection pool
50 #[tracing::instrument(skip_all)]
51 pub fn pool(&self) -> &SqlitePool {
52 &self.pool
53 }
54
55 /// Get a repository for feed subscription CRUD (create, enable/disable, delete,
56 /// list by busser, update last_fetch timestamp).
57 #[tracing::instrument(skip_all)]
58 pub fn feeds(&self) -> FeedsRepository {
59 FeedsRepository::new(self.pool.clone())
60 }
61
62 /// Get a repository for feed item operations: upsert, read/star toggling,
63 /// paginated listing (by busser, by feed, unread, starred), and counts.
64 #[tracing::instrument(skip_all)]
65 pub fn items(&self) -> ItemsRepository {
66 ItemsRepository::new(self.pool.clone())
67 }
68
69 /// Get a repository for feed tag CRUD (per-feed tag assignment,
70 /// distinct tag listing, bulk feed-tag pairs for sidebar).
71 #[tracing::instrument(skip_all)]
72 pub fn tags(&self) -> TagsRepository {
73 TagsRepository::new(self.pool.clone())
74 }
75
76 /// Get a repository for busser key-value state (cursors, tokens, pagination
77 /// markers). Each entry is keyed by `(busser_id, key)`.
78 #[tracing::instrument(skip_all)]
79 pub fn state(&self) -> StateRepository {
80 StateRepository::new(self.pool.clone())
81 }
82
83 /// Get a repository for user_config key-value pairs (theme, welcome flag,
84 /// etc.). Synced via sync_changelog triggers (migration 007).
85 #[tracing::instrument(skip_all)]
86 pub fn config(&self) -> ConfigRepository {
87 ConfigRepository::new(self.pool.clone())
88 }
89
90 /// Get a repository for query feed CRUD (saved filter rules that act as
91 /// virtual sources). Synced via sync_changelog triggers (migration 009).
92 #[tracing::instrument(skip_all)]
93 pub fn query_feeds(&self) -> QueryFeedsRepository {
94 QueryFeedsRepository::new(self.pool.clone())
95 }
96
97 /// Get a repository for bookmark (reading list) CRUD. Bookmarks are
98 /// permanent, user-owned references to URLs. Synced via sync_changelog
99 /// triggers (migration 012).
100 #[tracing::instrument(skip_all)]
101 pub fn bookmarks(&self) -> BookmarksRepository {
102 BookmarksRepository::new(self.pool.clone())
103 }
104 }
105