aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFederico Igne <git@federicoigne.com>2022-08-05 13:44:36 +0100
committerFederico Igne <git@federicoigne.com>2022-08-05 13:44:36 +0100
commita922aa28908d83c8ecead51e6d63c7c549568b5c (patch)
treeae2636d41d65c5d33d566c23cc8bb9a0f5adfae4
parentf5611c0bfdc73f76ef5330a17a49d4b5407ae395 (diff)
downloadjoyce-a922aa28908d83c8ecead51e6d63c7c549568b5c.tar.gz
joyce-a922aa28908d83c8ecead51e6d63c7c549568b5c.zip
feat(sqlite): move from file to SQLite backend
-rw-r--r--README.md249
1 files changed, 192 insertions, 57 deletions
diff --git a/README.md b/README.md
index 64de347..2d6b444 100644
--- a/README.md
+++ b/README.md
@@ -34,48 +34,60 @@ mod note;
34 34
35<<note_request_struct>> 35<<note_request_struct>>
36 36
37<<req_get_notes>> 37<<note_request_impl>>
38 38
39<<req_get_tags>> 39<<req_get_notes>>
40 40
41<<req_post_notes>> 41<<req_post_notes>>
42``` 42```
43 43
44## Anatomy of a note 44## Anatomy of a note
45 45
46A *note* is a simple structure recording a unique UUID and a timestamp, both assigned at creation, a list of tags, and the body of the note. 46A *note* is a simple structure recording a unique ID and a timestamp, both assigned at creation, a list of tags, and the body of the note.
47A set of notes is just a `Vec` of `Note`s.
48 47
49```{#note_struct .rust} 48```{#note_struct .rust}
50#[derive(Debug, Deserialize, Serialize)] 49#[derive(Debug, Deserialize, Serialize)]
51pub struct Note { 50pub struct Note {
52 uuid: Uuid, 51 id: i64,
53 timestamp: DateTime<Utc>, 52 timestamp: DateTime<Utc>,
54 tags: Vec<String>, 53 tags: Vec<String>,
55 body: String, 54 body: String,
56} 55}
57
58type Notes = Vec<Note>;
59``` 56```
60 57
61A `Note` can be automatically created from a `NoteRequest`.
62
63```{#note_impl .rust} 58```{#note_impl .rust}
64impl Note { 59impl Note {
65 fn new(body: String, tags: Vec<String>) -> Self { 60 pub fn new(id: i64, timestamp: DateTime<Utc>, tags: Vec<String>, body: String) -> Self {
66 Self { uuid: Uuid::new_v4(), timestamp: Utc::now(), tags, body } 61 Self { id, timestamp, tags, body }
67 } 62 }
68} 63}
69 64
70impl From<NoteRequest> for Note { 65```
71 fn from(req: NoteRequest) -> Self { 66
72 Self::new(req.body, req.tags) 67A `Note` can also be create from a `Row`, result of a query over the database.
73 } 68
69```{#note_uses .rust}
70use rusqlite::Row;
71```
72
73```{#note_impl .rust}
74impl From<&Row<'_>> for Note {
75 fn from(row: &Row<'_>) -> Note {
76 Note::new(
77 row.get(0).expect("Failed to read column 1"), // id
78 row.get(1).expect("Failed to read column 2"), // timestamp
79 serde_json::from_value(row.get::<_,serde_json::Value>(2).expect("Failed to read column 3")).expect("Failed to parse JSON"), // tags
80 row.get(3).expect("Failed to read column 4")) // body
81 }
74} 82}
75``` 83```
76 84
77Similarly, a `NoteRequest` is what a client would request at note creation. 85Similarly, a `NoteRequest` is what a client would request at note creation.
78It contains the same information as a `Note` without the information assigned at creation by the server. 86It contains the same information as a `Note` without the information assigned at creation by the server and the database.
87
88```{#note_uses .rust}
89use serde_json::Value;
90```
79 91
80```{#note_request_struct .rust} 92```{#note_request_struct .rust}
81#[derive(Debug, Deserialize, Serialize)] 93#[derive(Debug, Deserialize, Serialize)]
@@ -83,8 +95,18 @@ pub struct NoteRequest {
83 tags: Vec<String>, 95 tags: Vec<String>,
84 body: String, 96 body: String,
85} 97}
98```
99
100The stucture can also be converted into a tuple of parameters to pass a SQL query (see [`Params`](https://docs.rs/rusqlite/latest/rusqlite/trait.Params.html) from `rusqlite`).
86 101
87type NoteRequests = Vec<NoteRequest>; 102```{#note_request_impl .rust}
103pub type NoteParams = (DateTime<Utc>, Value, String);
104
105impl From<NoteRequest> for NoteParams {
106 fn from(req: NoteRequest) -> NoteParams {
107 (Utc::now(), Value::from(req.tags), req.body)
108 }
109}
88``` 110```
89 111
90### (De)serialization 112### (De)serialization
@@ -99,18 +121,9 @@ serde = { version = "1.0", features = ["derive"] }
99use serde::{Serialize, Deserialize}; 121use serde::{Serialize, Deserialize};
100``` 122```
101 123
102### UUIDs 124### IDs
103 125
104UUIDs are unique 128-bit identifiers, stored as 16 octets, and formatted as a hex string in five groups, e.g., `67e55044-10b1-426f-9247-bb680e5fe0c8`. 126We are using a `i64` as unique identifier because the field is automatically handled by SQLite, and `i64` is the most convenient type to convert from/to SQLite's `INTEGER` (according to [`rusqlite`](https://docs.rs/rusqlite/latest/rusqlite/types/index.html#)).
105Have a look at the [Wikipedia entry](http://en.wikipedia.org/wiki/Universally_unique_identifier) for more information.
106
107```{#dependencies .toml}
108uuid = { version = "1.1", features = ["v4","fast-rng","serde"] }
109```
110
111```{#note_uses .rust}
112use uuid::Uuid;
113```
114 127
115### Timestamps 128### Timestamps
116 129
@@ -135,10 +148,21 @@ actix-web = "4.1"
135```{#note_uses .rust} 148```{#note_uses .rust}
136use actix_web::{HttpResponse,Responder,web,get,post}; 149use actix_web::{HttpResponse,Responder,web,get,post};
137use actix_web::http::header::ContentType; 150use actix_web::http::header::ContentType;
151use super::db::Pool;
138``` 152```
139 153
140Each request handlers is an *async* function that accepts zero or more parameters, extracted from a request (see [`FromRequest`](https://docs.rs/actix-web/latest/actix_web/trait.FromRequest.html) trait), and returns an [`HttpResponse`](https://docs.rs/actix-web/latest/actix_web/struct.HttpResponse.html). 154Each request handlers is an *async* function that accepts zero or more parameters, extracted from a request (see [`FromRequest`](https://docs.rs/actix-web/latest/actix_web/trait.FromRequest.html) trait), and returns an [`HttpResponse`](https://docs.rs/actix-web/latest/actix_web/struct.HttpResponse.html).
141 155
156Internally requests will be carried out by querying the underlying SQLite database.
157
158```{#main_mods .rust}
159mod db;
160```
161
162```{#note_uses .rust}
163use super::db;
164```
165
142## Resources 166## Resources
143 167
144- [Tutorial](https://web.archive.org/web/20220710213947/https://hub.qovery.com/guides/tutorial/create-a-blazingly-fast-api-in-rust-part-1/) 168- [Tutorial](https://web.archive.org/web/20220710213947/https://hub.qovery.com/guides/tutorial/create-a-blazingly-fast-api-in-rust-part-1/)
@@ -151,11 +175,12 @@ The function takes 0 parameters and returns a JSON object.
151 175
152```{#req_get_notes .rust} 176```{#req_get_notes .rust}
153#[get("/notes")] 177#[get("/notes")]
154pub async fn list() -> HttpResponse { 178pub async fn get_notes(pool: web::Data<Pool>) -> HttpResponse {
155 let notes: Notes; 179 let mut conn = web::block(move || pool.get())
156 180 .await
157 <<notes_retrieve>> 181 .expect("Blocking error")
158 182 .expect("Error getting from connection pool");
183 let notes: Vec<Note> = db::get_notes(&mut conn);
159 HttpResponse::Ok() 184 HttpResponse::Ok()
160 .content_type(ContentType::json()) 185 .content_type(ContentType::json())
161 .json(notes) 186 .json(notes)
@@ -175,18 +200,13 @@ New notes can be added by POSTing a JSON array of `NoteRequest`s of the form
175 200
176```{#req_post_notes .rust} 201```{#req_post_notes .rust}
177#[post("/notes")] 202#[post("/notes")]
178pub async fn add(new_notes: web::Json<NoteRequests>) -> impl Responder { 203pub async fn post_notes(pool: web::Data<Pool>, req: web::Json<Vec<NoteRequest>>) -> impl Responder {
179 let mut new_notes: Notes = 204 let mut conn = web::block(move || pool.get())
180 new_notes 205 .await
181 .into_inner() 206 .expect("Blocking error")
182 .into_iter() 207 .expect("Error getting from connection pool");
183 .map(|n| n.into()) 208 let res = db::post_notes(&mut conn, req.into_inner());
184 .collect(); 209 format!("Successfully added {res} note(s)")
185 let count = new_notes.len();
186
187 <<notes_add>>
188
189 format!("Successfully added {} note(s)", count)
190} 210}
191``` 211```
192 212
@@ -200,7 +220,7 @@ One or more tags separated by `+` can be passed to the request.
200pub async fn get_tags(tags: web::Path<String>) -> HttpResponse { 220pub async fn get_tags(tags: web::Path<String>) -> HttpResponse {
201 let tags = tags.split('+').map(|t| t.to_string()).collect::<Vec<_>>(); 221 let tags = tags.split('+').map(|t| t.to_string()).collect::<Vec<_>>();
202 222
203 let notes: Notes; 223 let notes: Vec<Note>;
204 <<notes_retrieve>> 224 <<notes_retrieve>>
205 let tagged = notes.into_iter().filter(|n| tags.iter().all(|t| n.tags.contains(t))).collect::<Vec<_>>(); 225 let tagged = notes.into_iter().filter(|n| tags.iter().all(|t| n.tags.contains(t))).collect::<Vec<_>>();
206 226
@@ -227,7 +247,7 @@ The main program is structured as follows
227The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. 247The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080.
228 248
229```{#main_uses .rust} 249```{#main_uses .rust}
230use actix_web::{App, HttpServer}; 250use actix_web::{App, HttpServer, web};
231``` 251```
232 252
233The `App` will register all request handlers defined above. 253The `App` will register all request handlers defined above.
@@ -235,11 +255,12 @@ The `App` will register all request handlers defined above.
235```{#main_service .rust} 255```{#main_service .rust}
236#[actix_web::main] 256#[actix_web::main]
237async fn main() -> std::io::Result<()> { 257async fn main() -> std::io::Result<()> {
238 HttpServer::new(|| { 258 let db_pool = db::get_connection_pool("notes.db");
259 HttpServer::new(move || {
239 App::new() 260 App::new()
240 .service(note::list) 261 .app_data(web::Data::new(db_pool.clone()))
241 .service(note::add) 262 .service(note::get_notes)
242 .service(note::get_tags) 263 .service(note::post_notes)
243 }) 264 })
244 .bind(("127.0.0.1", 8080))? 265 .bind(("127.0.0.1", 8080))?
245 .run() 266 .run()
@@ -247,6 +268,119 @@ async fn main() -> std::io::Result<()> {
247} 268}
248``` 269```
249 270
271# SQLite backend
272
273`Note`s are saved into a [SQLite](https://sqlite.org/) database.
274The `notes` database contains a single table mirroring the `Note`'s structure.
275
276```{#notes.sql .sql}
277CREATE TABLE notes (
278 id INTEGER PRIMARY KEY AUTOINCREMENT,
279 timestamp TEXT NOT NULL,
280 tags TEXT NOT NULL,
281 body TEXT NOT NULL
282);
283```
284
285Note that, apart from [standard SQLite types](https://docs.rs/rusqlite/latest/rusqlite/types/index.html#), `DateTime<Utc>` is converted to/from `TEXT`, `Vec<String>` is first wrapped in a `Value` (from the `serde_json` crate) and then converted from/to `TEXT`.
286
287`id`s are handled automatically by SQLite and are not set on the Rust side.
288
289A new database can be bootstraped in the current directory as follows:
290
291```sh
292sqlite3 ./notes.db < notes.sql
293```
294
295## Interfacing with SQLite from Rust
296
297To interface with the underlying database we use [`rusqlite`](https://github.com/rusqlite/rusqlite) along with [`r2d2`](https://github.com/sfackler/r2d2) to create a connection pool.
298
299```{#dependencies .toml}
300r2d2 = "0.8"
301r2d2_sqlite = "0.21"
302rusqlite = { version = "0.28", features = ["chrono","serde_json"] }
303```
304
305The following code sets up a connection pool to the SQLite database.
306
307```{#db_uses .rust}
308use r2d2_sqlite::SqliteConnectionManager;
309```
310
311```{#db_types .rust}
312pub type Pool = r2d2::Pool<SqliteConnectionManager>;
313```
314
315```{#db_connection_pool .rust}
316pub fn get_connection_pool(db: &str) -> Pool {
317 let manager = SqliteConnectionManager::file(db);
318 r2d2::Pool::new(manager).expect("Unable to connect to 'notes.db'")
319}
320```
321
322For the sake of convenience, all operations on the database are stored on a separate file.
323
324```{#db.rs .rust path="src/"}
325<<db_uses>>
326
327<<db_types>>
328
329<<db_connection_pool>>
330
331<<db_operations>>
332```
333
334### Retrieving notes
335
336We first build the query (caching it for later) and then execute it to retrieve all requested notes.
337
338We return a `Vec<Note>` built from the query result.
339
340```{#db_uses .rust}
341use rusqlite::Connection;
342use super::note::Note;
343```
344
345```{#db_operations .rust}
346pub fn get_notes(conn: &mut Connection) -> Vec<Note> {
347 let mut query = conn.prepare_cached("SELECT * FROM notes")
348 .expect("Failed to prepare SELECT query");
349 query
350 .query_map([], |row| Ok(Note::from(row)))
351 .and_then(Iterator::collect)
352 .expect("Failed to collect query results")
353}
354
355```
356
357### Creating notes
358
359When inserting new `Note`s in the database, we loop over the requested notes, attaching a timestamp and executing an `INSERT` SQL query.
360
361SQLite will take care of attaching an ID to the new entry.
362
363Operations are executed into a single transaction [to achieve better performances](https://github.com/rusqlite/rusqlite/issues/262#issuecomment-294895051).
364
365```{#db_uses .rust}
366use super::note::{NoteParams,NoteRequest};
367```
368
369```{#db_operations .rust}
370pub fn post_notes(conn: &mut Connection, reqs: Vec<NoteRequest>) -> usize {
371 let tx = conn.transaction().expect("Failed to start transaction");
372 {
373 let mut stmt = tx.prepare_cached(
374 "INSERT INTO notes (timestamp, tags, body) VALUES (?, ?, ?)"
375 ).expect("Failed to prepare INSERT query");
376 reqs.into_iter().for_each(|req| { stmt.execute::<NoteParams>(req.into()).expect("Failed to execute INSERT query"); });
377 }
378 tx.commit().expect("Commit failed");
379 0
380}
381
382```
383
250# Testing 384# Testing
251 385
252## Using a file as a backend 386## Using a file as a backend
@@ -257,10 +391,6 @@ This is a temporary solution until an interface to a database (most likely SQLit
257serde_json = "1.0" 391serde_json = "1.0"
258``` 392```
259 393
260```{#note_uses .rust}
261use std::fs::File;
262```
263
264### Retrieving notes 394### Retrieving notes
265 395
266```{#notes_retrieve .rust} 396```{#notes_retrieve .rust}
@@ -273,7 +403,7 @@ notes = {
273### Adding notes 403### Adding notes
274 404
275```{#notes_add .rust} 405```{#notes_add .rust}
276let mut notes: Notes; 406let mut notes: Vec<Note>;
277<<notes_retrieve>> 407<<notes_retrieve>>
278notes.append(&mut new_notes); 408notes.append(&mut new_notes);
279 409
@@ -281,11 +411,16 @@ let db = File::create("notes.db").expect("Unable to create/open 'notes.db'");
281serde_json::to_writer(&db,&notes).expect("Unable to write to 'notes.db'"); 411serde_json::to_writer(&db,&notes).expect("Unable to write to 'notes.db'");
282``` 412```
283 413
284# Open questions 414# TODOs
415
416- Better error handling
417
418## Open questions
285 419
286- Should one be able to delete notes? Or mark them as read/processed? 420- Should one be able to delete notes? Or mark them as read/processed?
287- Authentication method? 421- Authentication method?
288- Custom filters on retrieval. 422- Custom filters on retrieval.
423- `rusqlite` ships with SQLite bundled if necessary (useful to bootstrap the db?)
289 424
290# Credits 425# Credits
291 426