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."
 | 
			
		||||
readme = "./README.org"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
cli = ["nm-store-cli"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[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)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn it_works() {
 | 
			
		||||
        assert_eq!(2 + 2, 4);
 | 
			
		||||
	use super::*;
 | 
			
		||||
	use tokio;
 | 
			
		||||
 | 
			
		||||
	#[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