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