FEAT Can initialize the database and fail to retrieve a page.
In the great tradition of TPP, this is a win. We've gone through the test driven development, and there is so much *learning* here: - tokio::test NEEDS the threaded_schedular feature to report errors correctly - thiserror can't do enum variants the way I expected - Different error types for different returns is not kosher - Serde's configuration NEEDS a type, such as JSON, to work, - Rust has include_str!(), to embed text in a Rust program from an external source - SQLX is still a pain, but it's managable.
This commit is contained in:
		
							parent
							
								
									2bd7c0aaad
								
							
						
					
					
						commit
						8ee71c76a3
					
				| 
						 | 
					@ -6,11 +6,19 @@ edition = "2018"
 | 
				
			||||||
description = "Datastore interface layer for notesmachine."
 | 
					description = "Datastore interface layer for notesmachine."
 | 
				
			||||||
readme = "./README.org"
 | 
					readme = "./README.org"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[features]
 | 
					[features]
 | 
				
			||||||
cli = ["nm-store-cli"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
nm-store-cli = { path: "../nm-store-cli" }
 | 
					thiserror = "1.0.20"
 | 
				
			||||||
 | 
					tokio = { version = "0.2.22", features = ["rt-threaded"] }
 | 
				
			||||||
 | 
					serde = { version = "1.0.116", features = ["derive"] }
 | 
				
			||||||
 | 
					serde_json = "1.0.56"
 | 
				
			||||||
 | 
					sqlx = { version = "0.4.0-beta.1", default-features = false, features = [
 | 
				
			||||||
 | 
					  "runtime-tokio",
 | 
				
			||||||
 | 
					  "sqlite",
 | 
				
			||||||
 | 
					  "macros",
 | 
				
			||||||
 | 
					] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					use sqlx;
 | 
				
			||||||
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// All the ways looking up objects can fail
 | 
				
			||||||
 | 
					#[derive(Error, Debug)]
 | 
				
			||||||
 | 
					pub enum NoteStoreError {
 | 
				
			||||||
 | 
					    /// When building a new note for the back-end, it failed to parse
 | 
				
			||||||
 | 
					    /// in some critical way.
 | 
				
			||||||
 | 
					    #[error("Invalid Note Structure")]
 | 
				
			||||||
 | 
					    InvalidNoteStructure(String),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// All other errors from the database.
 | 
				
			||||||
 | 
					    #[error(transparent)]
 | 
				
			||||||
 | 
					    DBError(#[from] sqlx::Error),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,23 @@
 | 
				
			||||||
 | 
					mod errors;
 | 
				
			||||||
 | 
					mod store;
 | 
				
			||||||
 | 
					mod structs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use crate::store::NoteStore;
 | 
				
			||||||
 | 
					pub use crate::errors::NoteStoreError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(test)]
 | 
					#[cfg(test)]
 | 
				
			||||||
mod tests {
 | 
					mod tests {
 | 
				
			||||||
    #[test]
 | 
						use super::*;
 | 
				
			||||||
    fn it_works() {
 | 
						use tokio;
 | 
				
			||||||
        assert_eq!(2 + 2, 4);
 | 
					
 | 
				
			||||||
 | 
						#[tokio::test(threaded_scheduler)]
 | 
				
			||||||
 | 
					    async fn it_works() {
 | 
				
			||||||
 | 
							let storagepool = NoteStore::new("sqlite://:memory:").await;
 | 
				
			||||||
 | 
							assert!(storagepool.is_ok());
 | 
				
			||||||
 | 
							let storagepool = storagepool.unwrap();
 | 
				
			||||||
 | 
							assert!(storagepool.reset_database().await.is_ok());
 | 
				
			||||||
 | 
							let unfoundpage = storagepool.fetch_page("nonexistent-page").await;
 | 
				
			||||||
 | 
							assert!(unfoundpage.is_err());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS notes;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS note_relationships;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS pages;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS favorites;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS page_relationships;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE notes (
 | 
				
			||||||
 | 
					    id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					    uuid TEXT NOT NULL UNIQUE,
 | 
				
			||||||
 | 
					    content TEXT NULL,
 | 
				
			||||||
 | 
					    notetype TEXT
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX notes_uuids ON notes (uuid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE pages (
 | 
				
			||||||
 | 
					    id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					    title text NOT NULL UNIQUE,
 | 
				
			||||||
 | 
					    slug text NOT NULL UNIQUE,
 | 
				
			||||||
 | 
					    note_id INTEGER,
 | 
				
			||||||
 | 
					    FOREIGN KEY (note_id) REFERENCES notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX pages_slugs ON pages (slug);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE favorites (
 | 
				
			||||||
 | 
					    id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					    position INTEGER NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE note_relationships (
 | 
				
			||||||
 | 
					    note_id INTEGER NOT NULL,
 | 
				
			||||||
 | 
					    parent_id INTEGER NOT NULL,
 | 
				
			||||||
 | 
					    position INTEGER NOT NULL,
 | 
				
			||||||
 | 
					    nature TEXT NOT NULL,
 | 
				
			||||||
 | 
					    FOREIGN KEY (note_id) REFERENCES notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION,
 | 
				
			||||||
 | 
					    FOREIGN KEY (parent_id) REFERENCES notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE page_relationships (
 | 
				
			||||||
 | 
					    note_id INTEGER NOT NULL,
 | 
				
			||||||
 | 
						page_id INTEGER NOT NULL,
 | 
				
			||||||
 | 
						FOREIGN KEY (note_id) references notes (id) ON DELETE NO ACTION ON UPDATE NO ACTION,
 | 
				
			||||||
 | 
						FOREIGN KEY (page_id) references pages (id) ON DELETE NO ACTION ON UPDATE NO ACTION
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					SELECT id, title, slug, note_id FROM pages WHERE slug=?;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					use sqlx;
 | 
				
			||||||
 | 
					use sqlx::sqlite::SqlitePool;
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					use crate::errors::NoteStoreError;
 | 
				
			||||||
 | 
					use crate::structs::RawPage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A handle to our Sqlite database.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct NoteStore(Arc<SqlitePool>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type NoteResult<T> = core::result::Result<T, NoteStoreError>;
 | 
				
			||||||
 | 
					type SqlResult<T> = sqlx::Result<T>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl NoteStore {
 | 
				
			||||||
 | 
						pub async fn new(url: &str) -> NoteResult<Self> {
 | 
				
			||||||
 | 
							let pool = SqlitePool::connect(url).await?;
 | 
				
			||||||
 | 
							Ok(NoteStore(Arc::new(pool)))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// This will erase all the data in the database.  Only use this
 | 
				
			||||||
 | 
						/// if you're sure that's what you want.
 | 
				
			||||||
 | 
						pub async fn reset_database(&self) -> NoteResult<()> {
 | 
				
			||||||
 | 
							let initialize_sql = include_str!("sql/initialize_database.sql");
 | 
				
			||||||
 | 
							sqlx::query(initialize_sql)
 | 
				
			||||||
 | 
								.execute(&*self.0)
 | 
				
			||||||
 | 
								.await?;
 | 
				
			||||||
 | 
							Ok(())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pub async fn fetch_page(&self, id: &str) -> SqlResult<RawPage> {
 | 
				
			||||||
 | 
							let select_one_page_sql = include_str!("sql/select_one_page.sql");
 | 
				
			||||||
 | 
							sqlx::query_as(select_one_page_sql)
 | 
				
			||||||
 | 
					            .bind(&id)
 | 
				
			||||||
 | 
					            .fetch_one(&*self.0)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use sqlx::{self, FromRow};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Serialize, Deserialize, Debug, FromRow)]
 | 
				
			||||||
 | 
					pub struct RawPage {
 | 
				
			||||||
 | 
					    id: i64,
 | 
				
			||||||
 | 
					    slug: String,
 | 
				
			||||||
 | 
					    title: String,
 | 
				
			||||||
 | 
					    note_id: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue