1use std::{path::PathBuf, str::FromStr};
2
3use self::sqlite_helper::EntryIntermediate;
4
5use super::*;
6use anyhow::{Context, anyhow};
7use path_absolutize::Absolutize;
8use sqlx::{
9 Row, Sqlite, SqlitePool,
10 migrate::MigrateDatabase,
11 sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
12};
13
14mod sqlite_helper;
15
16pub struct SqliteDataProvide {
17 pool: SqlitePool,
18}
19
20impl SqliteDataProvide {
21 pub async fn from_file(file_path: PathBuf) -> anyhow::Result<Self> {
22 let file_full_path = file_path
23 .absolutize()
24 .with_context(|| format!("Failed to resolve database path: {}", file_path.display()))?;
25 if !file_path.exists()
26 && let Some(parent) = file_path.parent()
27 {
28 tokio::fs::create_dir_all(parent).await.with_context(|| {
29 format!("Failed to create database directory: {}", parent.display())
30 })?;
31 }
32
33 let db_url = format!("sqlite://{}", file_full_path.to_string_lossy());
34
35 SqliteDataProvide::create(&db_url).await
36 }
37
38 pub async fn create(db_url: &str) -> anyhow::Result<Self> {
39 if !Sqlite::database_exists(db_url)
40 .await
41 .with_context(|| format!("Failed to check database existence: {db_url}"))?
42 {
43 log::trace!("Creating Database with the URL '{db_url}'");
44 Sqlite::create_database(db_url)
45 .await
46 .with_context(|| format!("Failed to create database: {db_url}"))?;
47 }
48
49 let options = SqliteConnectOptions::from_str(db_url)
53 .with_context(|| format!("Failed to parse database URL: {db_url}"))?
54 .journal_mode(SqliteJournalMode::Off)
55 .synchronous(SqliteSynchronous::Off);
56
57 let pool = SqlitePoolOptions::new()
58 .connect_with(options)
59 .await
60 .with_context(|| format!("Failed to connect to database: {db_url}"))?;
61
62 sqlx::migrate!("backend/src/sqlite/migrations")
63 .run(&pool)
64 .await
65 .map_err(|err| match err {
66 sqlx::migrate::MigrateError::VersionMissing(id) => anyhow!("Database version mismatch: migration {id} was previously applied but is missing in the resolved migrations"),
67 err => anyhow!(err),
68 })
69 .with_context(|| format!("Failed to apply migrations on database: {db_url}"))?;
70
71 Ok(Self { pool })
72 }
73
74 async fn insert_tags(&self, entry_id: u32, tags: &[String]) -> Result<(), ModifyEntryError> {
75 for tag in tags {
76 sqlx::query(
77 r"INSERT INTO tags (entry_id, tag)
78 VALUES($1, $2)",
79 )
80 .bind(entry_id)
81 .bind(tag)
82 .execute(&self.pool)
83 .await
84 .with_context(|| format!("Failed to add tag '{tag}' to entry {entry_id}"))?;
85 }
86
87 Ok(())
88 }
89}
90
91impl DataProvider for SqliteDataProvide {
92 async fn load_all_entries(&self) -> anyhow::Result<Vec<Entry>> {
93 let entries: Vec<EntryIntermediate> = sqlx::query_as(
94 r"SELECT entries.id, entries.title, entries.date, entries.content, entries.priority, GROUP_CONCAT(tags.tag) AS tags
95 FROM entries
96 LEFT JOIN tags ON entries.id = tags.entry_id
97 GROUP BY entries.id
98 ORDER BY date DESC",
99 )
100 .fetch_all(&self.pool)
101 .await
102 .context("Failed to load entries from database")?;
103
104 let entries: Vec<Entry> = entries.into_iter().map(Entry::from).collect();
105
106 Ok(entries)
107 }
108
109 async fn add_entry(&self, entry: EntryDraft) -> Result<Entry, ModifyEntryError> {
110 let row = sqlx::query(
111 r"INSERT INTO entries (title, date, content, priority)
112 VALUES($1, $2, $3, $4)
113 RETURNING id",
114 )
115 .bind(&entry.title)
116 .bind(entry.date)
117 .bind(&entry.content)
118 .bind(entry.priority)
119 .fetch_one(&self.pool)
120 .await
121 .with_context(|| format!("Failed to add entry: {}", entry.title))?;
122
123 let id = row.get::<u32, _>(0);
124
125 self.insert_tags(id, &entry.tags).await?;
126
127 Ok(Entry::from_draft(id, entry))
128 }
129
130 async fn restore_entry(&self, entry: Entry) -> Result<Entry, ModifyEntryError> {
131 sqlx::query(
132 r"INSERT INTO entries (id, title, date, content, priority)
133 VALUES($1, $2, $3, $4, $5)",
134 )
135 .bind(entry.id)
136 .bind(&entry.title)
137 .bind(entry.date)
138 .bind(&entry.content)
139 .bind(entry.priority)
140 .execute(&self.pool)
141 .await
142 .with_context(|| format!("Failed to restore entry {}", entry.id))?;
143
144 self.insert_tags(entry.id, &entry.tags).await?;
145
146 Ok(entry)
147 }
148
149 async fn remove_entry(&self, entry_id: u32) -> anyhow::Result<()> {
150 sqlx::query(r"DELETE FROM entries WHERE id=$1")
151 .bind(entry_id)
152 .execute(&self.pool)
153 .await
154 .with_context(|| format!("Failed to delete entry {entry_id}"))?;
155
156 Ok(())
157 }
158
159 async fn update_entry(&self, entry: Entry) -> Result<Entry, ModifyEntryError> {
160 sqlx::query(
161 r"UPDATE entries
162 Set title = $1,
163 date = $2,
164 content = $3,
165 priority = $4
166 WHERE id = $5",
167 )
168 .bind(&entry.title)
169 .bind(entry.date)
170 .bind(&entry.content)
171 .bind(entry.priority)
172 .bind(entry.id)
173 .execute(&self.pool)
174 .await
175 .with_context(|| format!("Failed to update entry {}", entry.id))?;
176
177 let existing_tags: Vec<String> = sqlx::query_scalar(
178 r"SELECT tag FROM tags
179 WHERE entry_id = $1",
180 )
181 .bind(entry.id)
182 .fetch_all(&self.pool)
183 .await
184 .with_context(|| format!("Failed to load tags for entry {}", entry.id))?;
185
186 for tag_to_remove in existing_tags.iter().filter(|tag| !entry.tags.contains(tag)) {
188 sqlx::query(r"DELETE FROM tags Where entry_id = $1 AND tag = $2")
189 .bind(entry.id)
190 .bind(tag_to_remove)
191 .execute(&self.pool)
192 .await
193 .with_context(|| {
194 format!(
195 "Failed to remove tag '{tag_to_remove}' from entry {}",
196 entry.id
197 )
198 })?;
199 }
200
201 for tag_to_insert in entry.tags.iter().filter(|tag| !existing_tags.contains(tag)) {
203 sqlx::query(
204 r"INSERT INTO tags (entry_id, tag)
205 VALUES ($1, $2)",
206 )
207 .bind(entry.id)
208 .bind(tag_to_insert)
209 .execute(&self.pool)
210 .await
211 .with_context(|| {
212 format!("Failed to add tag '{tag_to_insert}' to entry {}", entry.id)
213 })?;
214 }
215
216 Ok(entry)
217 }
218
219 async fn get_export_object(&self, entries_ids: &[u32]) -> anyhow::Result<EntriesDTO> {
220 let ids_text = entries_ids
221 .iter()
222 .map(|id| id.to_string())
223 .collect::<Vec<String>>()
224 .join(", ");
225
226 let sql = format!(
227 r"SELECT entries.id, entries.title, entries.date, entries.content, entries.priority, GROUP_CONCAT(tags.tag) AS tags
228 FROM entries
229 LEFT JOIN tags ON entries.id = tags.entry_id
230 WHERE entries.id IN ({ids_text})
231 GROUP BY entries.id
232 ORDER BY date DESC"
233 );
234
235 let entries: Vec<EntryIntermediate> = sqlx::query_as(sql.as_str())
236 .fetch_all(&self.pool)
237 .await
238 .with_context(|| format!("Failed to load entries for export: {ids_text}"))?;
239
240 let entry_drafts = entries
241 .into_iter()
242 .map(Entry::from)
243 .map(EntryDraft::from_entry)
244 .collect();
245
246 Ok(EntriesDTO::new(entry_drafts))
247 }
248
249 async fn assign_priority_to_entries(&self, priority: u32) -> anyhow::Result<()> {
250 let sql = format!(
251 r"UPDATE entries
252 SET priority = '{priority}'
253 WHERE priority IS NULL;"
254 );
255
256 sqlx::query(sql.as_str())
257 .execute(&self.pool)
258 .await
259 .with_context(|| format!("Failed to assign priority {priority} to entries"))?;
260
261 Ok(())
262 }
263}