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:
Elf M. Sternberg 2020-09-28 17:33:43 -07:00
parent 2bd7c0aaad
commit 8ee71c76a3
7 changed files with 137 additions and 5 deletions

View File

@ -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",
] }

View File

@ -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),
}

View File

@ -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());
}
}

View File

@ -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
);

View File

@ -0,0 +1 @@
SELECT id, title, slug, note_id FROM pages WHERE slug=?;

View File

@ -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
}
}

View File

@ -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,
}