From cf6f906fa4bd05d1d0a37ddb40f4993ebfabde1a Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Tue, 10 Nov 2020 20:08:26 -0800 Subject: [PATCH] It has the secret for nested transactions. --- server/nm-store/src/store/private.rs | 595 ++++++++++++++------------- server/nm-store/src/store/store.rs | 372 +++++++++-------- 2 files changed, 521 insertions(+), 446 deletions(-) diff --git a/server/nm-store/src/store/private.rs b/server/nm-store/src/store/private.rs index ce057fe..6b8713a 100644 --- a/server/nm-store/src/store/private.rs +++ b/server/nm-store/src/store/private.rs @@ -2,7 +2,7 @@ use crate::structs::*; use lazy_static::lazy_static; use regex::Regex; use slug::slugify; -use sqlx::{sqlite::Sqlite, Done, Executor}; +use sqlx::{sqlite::Sqlite, Acquire, Done, Executor, Transaction}; use std::collections::HashSet; type SqlResult = sqlx::Result; @@ -19,24 +19,24 @@ type SqlResult = sqlx::Result; // of the SQL queries. lazy_static! { - static ref SELECT_KASTEN_BY_TITLE_SQL: String = str::replace( - include_str!("sql/select_notes_by_parameter.sql"), - "QUERYPARAMETER", - "notes.content" - ); + static ref SELECT_KASTEN_BY_TITLE_SQL: String = str::replace( + include_str!("sql/select_notes_by_parameter.sql"), + "QUERYPARAMETER", + "notes.content" + ); } lazy_static! { - static ref SELECT_KASTEN_BY_ID_SQL: String = str::replace( - include_str!("sql/select_notes_by_parameter.sql"), - "QUERYPARAMETER", - "notes.id" - ); + static ref SELECT_KASTEN_BY_ID_SQL: String = str::replace( + include_str!("sql/select_notes_by_parameter.sql"), + "QUERYPARAMETER", + "notes.id" + ); } lazy_static! { - static ref SELECT_NOTES_BACKREFENCING_KASTEN_SQL: &'static str = - include_str!("sql/select_notes_backreferencing_kasten.sql"); + static ref SELECT_NOTES_BACKREFENCING_KASTEN_SQL: &'static str = + include_str!("sql/select_notes_backreferencing_kasten.sql"); } // ___ _ @@ -47,10 +47,13 @@ lazy_static! { pub(crate) async fn reset_database<'a, E>(executor: E) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let initialize_sql = include_str!("sql/initialize_database.sql"); - sqlx::query(initialize_sql).execute(executor).await.map(|_| ()) + let initialize_sql = include_str!("sql/initialize_database.sql"); + sqlx::query(initialize_sql) + .execute(executor) + .await + .map(|_| ()) } // ___ _ _ _ __ _ @@ -70,13 +73,13 @@ where // kasten. pub(crate) async fn select_kasten_by_slug<'a, E>(executor: E, slug: &NoteId) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let r: Vec = sqlx::query_as(&SELECT_KASTEN_BY_ID_SQL) - .bind(&**slug) - .fetch_all(executor) - .await?; - Ok(r.into_iter().map(|z| Note::from(z)).collect()) + let r: Vec = sqlx::query_as(&SELECT_KASTEN_BY_ID_SQL) + .bind(&**slug) + .fetch_all(executor) + .await?; + Ok(r.into_iter().map(|z| Note::from(z)).collect()) } // Fetch the kasten by title. The return value is an array of Note @@ -84,28 +87,31 @@ where // these into a tree-like object. pub(crate) async fn select_kasten_by_title<'a, E>(executor: E, title: &str) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let r: Vec = sqlx::query_as(&SELECT_KASTEN_BY_TITLE_SQL) - .bind(&title) - .fetch_all(executor) - .await?; - Ok(r.into_iter().map(|z| Note::from(z)).collect()) + let r: Vec = sqlx::query_as(&SELECT_KASTEN_BY_TITLE_SQL) + .bind(&title) + .fetch_all(executor) + .await?; + Ok(r.into_iter().map(|z| Note::from(z)).collect()) } // Fetch all backreferences to a kasten. The return value is an array // of arrays, and inside each array is a list from a root kasten to // the note that references the give kasten. Clients may choose how // they want to display that collection. -pub(crate) async fn select_backreferences_for_kasten<'a, E>(executor: E, kasten_id: &NoteId) -> SqlResult> +pub(crate) async fn select_backreferences_for_kasten<'a, E>( + executor: E, + kasten_id: &NoteId, +) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let r: Vec = sqlx::query_as(&SELECT_NOTES_BACKREFENCING_KASTEN_SQL) - .bind(&**kasten_id) - .fetch_all(executor) - .await?; - Ok(r.into_iter().map(|z| Note::from(z)).collect()) + let r: Vec = sqlx::query_as(&SELECT_NOTES_BACKREFENCING_KASTEN_SQL) + .bind(&**kasten_id) + .fetch_all(executor) + .await?; + Ok(r.into_iter().map(|z| Note::from(z)).collect()) } // ___ _ ___ _ _ _ @@ -117,24 +123,24 @@ where // Inserts a single note into the notes table. That is all. pub(crate) async fn insert_note<'a, E>(executor: E, zettle: &NewNote) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let insert_one_page_sql = concat!( - "INSERT INTO notes (id, content, kind, ", - " creation_date, updated_date, lastview_date) ", - "VALUES (?, ?, ?, ?, ?, ?);" - ); + let insert_one_page_sql = concat!( + "INSERT INTO notes (id, content, kind, ", + " creation_date, updated_date, lastview_date) ", + "VALUES (?, ?, ?, ?, ?, ?);" + ); - let _ = sqlx::query(insert_one_page_sql) - .bind(&zettle.id) - .bind(&zettle.content) - .bind(zettle.kind.to_string()) - .bind(&zettle.creation_date) - .bind(&zettle.updated_date) - .bind(&zettle.lastview_date) - .execute(executor) - .await?; - Ok(zettle.id.clone()) + let _ = sqlx::query(insert_one_page_sql) + .bind(&zettle.id) + .bind(&zettle.content) + .bind(zettle.kind.to_string()) + .bind(&zettle.creation_date) + .bind(&zettle.updated_date) + .bind(&zettle.lastview_date) + .execute(executor) + .await?; + Ok(zettle.id.clone()) } // ___ _ _ _ _ __ _ @@ -146,21 +152,21 @@ where // Given a possible slug, find the slug with the highest // uniquification number, and return that number, if any. pub(crate) fn find_maximal_slug_number(slugs: &[JustId]) -> Option { - lazy_static! { - static ref RE_CAP_NUM: Regex = Regex::new(r"-(\d+)$").unwrap(); - } + lazy_static! { + static ref RE_CAP_NUM: Regex = Regex::new(r"-(\d+)$").unwrap(); + } - if slugs.is_empty() { - return None; - } + if slugs.is_empty() { + return None; + } - let mut slug_counters: Vec = slugs - .iter() - .filter_map(|slug| RE_CAP_NUM.captures(&slug.id)) - .map(|cap| cap.get(1).unwrap().as_str().parse::().unwrap()) - .collect(); - slug_counters.sort_unstable(); - slug_counters.pop() + let mut slug_counters: Vec = slugs + .iter() + .filter_map(|slug| RE_CAP_NUM.captures(&slug.id)) + .map(|cap| cap.get(1).unwrap().as_str().parse::().unwrap()) + .collect(); + slug_counters.sort_unstable(); + slug_counters.pop() } // Given an initial string and an existing collection of slugs, @@ -169,38 +175,38 @@ pub(crate) fn find_maximal_slug_number(slugs: &[JustId]) -> Option { // isn't all that. pub(crate) async fn generate_slug<'a, E>(executor: E, title: &str) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - lazy_static! { - static ref RE_STRIP_NUM: Regex = Regex::new(r"-\d+$").unwrap(); - static ref SLUG_FINDER_SQL: String = format!( - "SELECT id FROM notes WHERE kind = '{}' AND id LIKE '?%';", - NoteKind::Kasten.to_string() - ); - } + lazy_static! { + static ref RE_STRIP_NUM: Regex = Regex::new(r"-\d+$").unwrap(); + static ref SLUG_FINDER_SQL: String = format!( + "SELECT id FROM notes WHERE kind = '{}' AND id LIKE '?%';", + NoteKind::Kasten.to_string() + ); + } - let initial_slug = slugify(title); - let sample_slug = RE_STRIP_NUM.replace_all(&initial_slug, ""); - let similar_slugs: Vec = sqlx::query_as(&SLUG_FINDER_SQL) - .bind(&*sample_slug) - .fetch_all(executor) - .await?; - let maximal_slug_number = find_maximal_slug_number(&similar_slugs); - Ok(match maximal_slug_number { - None => initial_slug, - Some(slug_number) => format!("{}-{}", initial_slug, slug_number + 1), - }) + let initial_slug = slugify(title); + let sample_slug = RE_STRIP_NUM.replace_all(&initial_slug, ""); + let similar_slugs: Vec = sqlx::query_as(&SLUG_FINDER_SQL) + .bind(&*sample_slug) + .fetch_all(executor) + .await?; + let maximal_slug_number = find_maximal_slug_number(&similar_slugs); + Ok(match maximal_slug_number { + None => initial_slug, + Some(slug_number) => format!("{}-{}", initial_slug, slug_number + 1), + }) } // A helper function: given a title and a slug, create a KastenType // note. pub(crate) fn create_zettlekasten(title: &str, slug: &str) -> NewNote { - NewNoteBuilder::default() - .id(slug.to_string()) - .content(title.to_string()) - .kind(NoteKind::Kasten) - .build() - .unwrap() + NewNoteBuilder::default() + .id(slug.to_string()) + .content(title.to_string()) + .kind(NoteKind::Kasten) + .build() + .unwrap() } // _ _ _ _ ___ _ _ _ @@ -209,22 +215,26 @@ pub(crate) fn create_zettlekasten(title: &str, slug: &str) -> NewNote { // \___/| .__/\__,_\__,_|\__\___| \___/|_||_\___| |_|\_\___/\__\___| // |_| -pub(crate) async fn update_note_content<'a, E>(executor: E, note_id: &NoteId, content: &str) -> SqlResult<()> +pub(crate) async fn update_note_content<'a, E>( + executor: E, + note_id: &NoteId, + content: &str, +) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let update_note_content_sql = "UPDATE notes SET content = ? WHERE note_id = ?"; - let count = sqlx::query(update_note_content_sql) - .bind(content) - .bind(&**note_id) - .execute(executor) - .await? - .rows_affected(); + let update_note_content_sql = "UPDATE notes SET content = ? WHERE note_id = ?"; + let count = sqlx::query(update_note_content_sql) + .bind(content) + .bind(&**note_id) + .execute(executor) + .await? + .rows_affected(); - match count { - 1 => Ok(()), - _ => Err(sqlx::Error::RowNotFound), - } + match count { + 1 => Ok(()), + _ => Err(sqlx::Error::RowNotFound), + } } // ___ _ _ ___ _ _ _ ___ _ _ _ _ _ @@ -234,25 +244,25 @@ where // |_| pub(crate) async fn select_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &ParentId, - note_id: &NoteId, + executor: E, + parent_id: &ParentId, + note_id: &NoteId, ) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let get_note_to_note_relationship_sql = concat!( - "SELECT parent_id, note_id, location, kind ", - "FROM note_relationships ", - "WHERE parent_id = ? and note_id = ? ", - "LIMIT 1" - ); - let s: NoteRelationshipRow = sqlx::query_as(get_note_to_note_relationship_sql) - .bind(&**parent_id) - .bind(&**note_id) - .fetch_one(executor) - .await?; - Ok(NoteRelationship::from(s)) + let get_note_to_note_relationship_sql = concat!( + "SELECT parent_id, note_id, location, kind ", + "FROM note_relationships ", + "WHERE parent_id = ? and note_id = ? ", + "LIMIT 1" + ); + let s: NoteRelationshipRow = sqlx::query_as(get_note_to_note_relationship_sql) + .bind(&**parent_id) + .bind(&**note_id) + .fetch_one(executor) + .await?; + Ok(NoteRelationship::from(s)) } // _ _ _ _ _ _ _ ___ _ _ _ _ _ @@ -262,61 +272,68 @@ where // |_| pub(crate) async fn insert_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &ParentId, - note_id: &NoteId, - location: i64, - kind: &RelationshipKind, + executor: E, + parent_id: &ParentId, + note_id: &NoteId, + location: i64, + kind: &RelationshipKind, ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let insert_note_to_note_relationship_sql = concat!( - "INSERT INTO note_relationships (parent_id, note_id, location, kind) ", - "values (?, ?, ?, ?)" - ); + let insert_note_to_note_relationship_sql = concat!( + "INSERT INTO note_relationships (parent_id, note_id, location, kind) ", + "values (?, ?, ?, ?)" + ); - let _ = sqlx::query(insert_note_to_note_relationship_sql) - .bind(&**parent_id) - .bind(&**note_id) - .bind(&location) - .bind(&kind.to_string()) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(insert_note_to_note_relationship_sql) + .bind(&**parent_id) + .bind(&**note_id) + .bind(&location) + .bind(&kind.to_string()) + .execute(executor) + .await?; + Ok(()) } -pub(crate) async fn make_room_for_new_note<'a, E>(executor: E, parent_id: &ParentId, location: i64) -> SqlResult<()> +pub(crate) async fn make_room_for_new_note<'a, E>( + executor: E, + parent_id: &ParentId, + location: i64, +) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let make_room_for_new_note_sql = concat!( - "UPDATE note_relationships ", - "SET location = location + 1 ", - "WHERE location >= ? and parent_id = ?;" - ); + let make_room_for_new_note_sql = concat!( + "UPDATE note_relationships ", + "SET location = location + 1 ", + "WHERE location >= ? and parent_id = ?;" + ); - let _ = sqlx::query(make_room_for_new_note_sql) - .bind(&location) - .bind(&**parent_id) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(make_room_for_new_note_sql) + .bind(&location) + .bind(&**parent_id) + .execute(executor) + .await?; + Ok(()) } -pub(crate) async fn assert_max_child_location_for_note<'a, E>(executor: E, note_id: &ParentId) -> SqlResult +pub(crate) async fn assert_max_child_location_for_note<'a, E>( + executor: E, + note_id: &ParentId, +) -> SqlResult where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let assert_max_child_location_for_note_sql = - "SELECT MAX(location) AS count FROM note_relationships WHERE parent_id = ?;"; + let assert_max_child_location_for_note_sql = + "SELECT MAX(location) AS count FROM note_relationships WHERE parent_id = ?;"; - let count: RowCount = sqlx::query_as(assert_max_child_location_for_note_sql) - .bind(&**note_id) - .fetch_one(executor) - .await?; + let count: RowCount = sqlx::query_as(assert_max_child_location_for_note_sql) + .bind(&**note_id) + .fetch_one(executor) + .await?; - Ok(count.count) + Ok(count.count) } // _ _ _ _ _ __ _ ___ _ _ _ _ _ @@ -326,49 +343,57 @@ where // |_| pub(crate) async fn insert_bulk_note_to_kasten_relationships<'a, E>( - executor: E, - note_id: &NoteId, - references: &[NoteId], + executor: E, + note_id: &NoteId, + references: &[NoteId], ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - if references.is_empty() { - return Ok(()); - } + if references.is_empty() { + return Ok(()); + } - let insert_pattern = format!("(?, ?, '{}')", KastenRelationshipKind::Kasten.to_string()); - let insert_note_page_references_sql = "INSERT INTO note_kasten_relationships (note_id, kasten_id, kind) VALUES " - .to_string() - + &[insert_pattern.as_str()].repeat(references.len()).join(", ") - + &";".to_string(); + let insert_pattern = format!("(?, ?, '{}')", KastenRelationshipKind::Kasten.to_string()); + let insert_note_page_references_sql = + "INSERT INTO note_kasten_relationships (note_id, kasten_id, kind) VALUES ".to_string() + + &[insert_pattern.as_str()] + .repeat(references.len()) + .join(", ") + &";".to_string(); - let mut request = sqlx::query(&insert_note_page_references_sql); - for reference in references { - request = request.bind(&**note_id).bind(&**reference); - } + let mut request = sqlx::query(&insert_note_page_references_sql); + for reference in references { + request = request.bind(&**note_id).bind(&**reference); + } - request.execute(executor).await.map(|_| ()) + request.execute(executor).await.map(|_| ()) } -pub(crate) async fn delete_bulk_note_to_kasten_relationships<'a, E>(executor: E, note_id: &NoteId) -> SqlResult<()> +pub(crate) async fn delete_bulk_note_to_kasten_relationships<'a, E>( + executor: E, + note_id: &NoteId, +) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let delete_note_to_kasten_relationship_sql = "DELETE FROM note_kasten_relationships WHERE and note_id = ?;"; - let _ = sqlx::query(delete_note_to_kasten_relationship_sql) - .bind(&**note_id) - .execute(executor) - .await?; - Ok(()) + let delete_note_to_kasten_relationship_sql = + "DELETE FROM note_kasten_relationships WHERE and note_id = ?;"; + let _ = sqlx::query(delete_note_to_kasten_relationship_sql) + .bind(&**note_id) + .execute(executor) + .await?; + Ok(()) } // Given the references supplied, and the references found in the datastore, // return a list of the references not found in the datastore. -pub(crate) fn diff_references(references: &[String], found_references: &[PageTitle]) -> Vec { - let all: HashSet = references.iter().cloned().collect(); - let found: HashSet = found_references.iter().map(|r| r.content.clone()).collect(); - all.difference(&found).cloned().collect() +pub(crate) fn diff_references( + references: &[String], + found_references: &[PageTitle], +) -> Vec { + let all: HashSet = references.iter().cloned().collect(); + let found: HashSet = found_references.iter().map(|r| r.content.clone()).collect(); + all.difference(&found).cloned().collect() } // ___ _ _ _ _ __ _ ___ _ _ _ _ _ @@ -381,31 +406,32 @@ pub(crate) fn diff_references(references: &[String], found_references: &[PageTit // list of titles. Used by insert_note and update_note_content to // find the ids of all the references in a given document. pub(crate) async fn find_all_kasten_from_list_of_references<'a, E>( - executor: E, - references: &[String], + executor: E, + references: &[String], ) -> SqlResult> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - if references.is_empty() { - return Ok(vec![]); - } + if references.is_empty() { + return Ok(vec![]); + } - lazy_static! { - static ref SELECT_ALL_REFERENCES_FOR_SQL_BASE: String = format!( - "SELECT id, content FROM notes WHERE kind = '{}' AND content IN (", - NoteKind::Kasten.to_string() - ); - } + lazy_static! { + static ref SELECT_ALL_REFERENCES_FOR_SQL_BASE: String = format!( + "SELECT id, content FROM notes WHERE kind = '{}' AND content IN (", + NoteKind::Kasten.to_string() + ); + } - let find_all_references_for_sql = - SELECT_ALL_REFERENCES_FOR_SQL_BASE.to_string() + &["?"].repeat(references.len()).join(",") + &");".to_string(); + let find_all_references_for_sql = SELECT_ALL_REFERENCES_FOR_SQL_BASE.to_string() + + &["?"].repeat(references.len()).join(",") + + &");".to_string(); - let mut request = sqlx::query_as(&find_all_references_for_sql); - for id in references.iter() { - request = request.bind(id); - } - request.fetch_all(executor).await + let mut request = sqlx::query_as(&find_all_references_for_sql); + for id in references.iter() { + request = request.bind(id); + } + request.fetch_all(executor).await } // ___ _ _ @@ -415,112 +441,125 @@ where // pub(crate) async fn delete_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &ParentId, - note_id: &NoteId, + executor: E, + parent_id: &ParentId, + note_id: &NoteId, ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let delete_note_to_note_relationship_sql = concat!( - "DELETE FROM note_relationships ", - "WHERE parent_id = ? and note_id = ? " - ); + let delete_note_to_note_relationship_sql = concat!( + "DELETE FROM note_relationships ", + "WHERE parent_id = ? and note_id = ? " + ); - let count = sqlx::query(delete_note_to_note_relationship_sql) - .bind(&**parent_id) - .bind(&**note_id) - .execute(executor) - .await? - .rows_affected(); + let count = sqlx::query(delete_note_to_note_relationship_sql) + .bind(&**parent_id) + .bind(&**note_id) + .execute(executor) + .await? + .rows_affected(); - match count { - 1 => Ok(()), - _ => Err(sqlx::Error::RowNotFound), - } + match count { + 1 => Ok(()), + _ => Err(sqlx::Error::RowNotFound), + } } -pub(crate) async fn delete_note_to_kasten_relationships<'a, E>(executor: E, note_id: &NoteId) -> SqlResult<()> +pub(crate) async fn delete_note_to_kasten_relationships<'a, E>( + executor: E, + note_id: &NoteId, +) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - lazy_static! { - static ref DELETE_NOTE_TO_KASTEN_RELATIONSHIPS_SQL: String = format!( - "DELETE FROM note_relationships WHERE kind in ('{}', '{}') AND parent_id = ?;", - KastenRelationshipKind::Kasten.to_string(), - KastenRelationshipKind::Unacked.to_string() - ); - } + lazy_static! { + static ref DELETE_NOTE_TO_KASTEN_RELATIONSHIPS_SQL: String = format!( + "DELETE FROM note_relationships WHERE kind in ('{}', '{}') AND parent_id = ?;", + KastenRelationshipKind::Kasten.to_string(), + KastenRelationshipKind::Unacked.to_string() + ); + } - let _ = sqlx::query(&DELETE_NOTE_TO_KASTEN_RELATIONSHIPS_SQL) - .bind(&**note_id) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(&DELETE_NOTE_TO_KASTEN_RELATIONSHIPS_SQL) + .bind(&**note_id) + .execute(executor) + .await?; + Ok(()) } pub(crate) async fn delete_note<'a, E>(executor: E, note_id: &NoteId) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let delete_note_sql = "DELETE FROM notes WHERE note_id = ?"; + let delete_note_sql = "DELETE FROM notes WHERE note_id = ?"; - let count = sqlx::query(delete_note_sql) - .bind(&**note_id) - .execute(executor) - .await? - .rows_affected(); + let count = sqlx::query(delete_note_sql) + .bind(&**note_id) + .execute(executor) + .await? + .rows_affected(); - match count { - 1 => Ok(()), - _ => Err(sqlx::Error::RowNotFound), - } + match count { + 1 => Ok(()), + _ => Err(sqlx::Error::RowNotFound), + } } // After removing a note, recalculate the position of all notes under // the parent note, such that there order is now completely // sequential. pub(crate) async fn close_hole_for_deleted_note<'a, E>( - executor: E, - parent_id: &ParentId, - location: i64, + executor: E, + parent_id: &ParentId, + location: i64, ) -> SqlResult<()> where - E: Executor<'a, Database = Sqlite>, + E: Executor<'a, Database = Sqlite>, { - let close_hole_for_deleted_note_sql = concat!( - "UPDATE note_relationships ", - "SET location = location - 1 ", - "WHERE location > ? and parent_id = ?;" - ); + let close_hole_for_deleted_note_sql = concat!( + "UPDATE note_relationships ", + "SET location = location - 1 ", + "WHERE location > ? and parent_id = ?;" + ); - let _ = sqlx::query(close_hole_for_deleted_note_sql) - .bind(&location) - .bind(&**parent_id) - .execute(executor) - .await?; - Ok(()) + let _ = sqlx::query(close_hole_for_deleted_note_sql) + .bind(&location) + .bind(&**parent_id) + .execute(executor) + .await?; + Ok(()) } -// __ __ _ -// | \/ (_)___ __ +// __ __ _ +// | \/ (_)___ __ // | |\/| | (_-(executor: E, note_id: &NoteId) -> SqlResult -where - E: Executor<'a, Database = Sqlite>, -{ - let count_existing_note_relationships_sql = - "SELECT COUNT(*) as count FROM note_relationships WHERE note_id = ?;"; +pub(crate) async fn count_existing_note_relationships( + tx: &mut Transaction<'_, Sqlite>, + note_id: &NoteId, +) -> SqlResult { + let mut txi = tx.begin().await?; + let count_existing_note_relationships_sql = + "SELECT COUNT(*) as count FROM note_relationships WHERE note_id = ?;"; + let _: RowCount = sqlx::query_as(&count_existing_note_relationships_sql) + .bind(&**note_id) + .fetch_one(&mut txi) + .await?; - let count: RowCount = sqlx::query_as(&count_existing_note_relationships_sql) - .bind(&**note_id) - .fetch_one(executor) - .await?; + let count: RowCount = { + let count_existing_note_relationships_sql = + "SELECT COUNT(*) as count FROM note_relationships WHERE note_id = ?;"; - Ok(count.count) + sqlx::query_as(&count_existing_note_relationships_sql) + .bind(&**note_id) + .fetch_one(&mut txi) + .await? + }; + txi.commit().await?; + Ok(count.count) } diff --git a/server/nm-store/src/store/store.rs b/server/nm-store/src/store/store.rs index 8161938..d9e591d 100644 --- a/server/nm-store/src/store/store.rs +++ b/server/nm-store/src/store/store.rs @@ -71,169 +71,198 @@ pub type NoteResult = core::result::Result; // (box). impl NoteStore { - /// Initializes a new instance of the note store. Note that the - /// note store holds an Arc internally; this code is (I think) - /// safe to Send. - pub async fn new(url: &str) -> NoteResult { - let pool = SqlitePool::connect(url).await?; - Ok(NoteStore(Arc::new(pool))) - } - /// Erase all the data in the database and restore it - /// to its original empty form. Do not use unless you - /// really, really want that to happen. - pub async fn reset_database(&self) -> NoteResult<()> { - reset_database(&*self.0).await.map_err(NoteStoreError::DBError) - } + /// Initializes a new instance of the note store. Note that the + /// note store holds an Arc internally; this code is (I think) + /// safe to Send. + pub async fn new(url: &str) -> NoteResult { + let pool = SqlitePool::connect(url).await?; + Ok(NoteStore(Arc::new(pool))) + } + /// Erase all the data in the database and restore it + /// to its original empty form. Do not use unless you + /// really, really want that to happen. + pub async fn reset_database(&self) -> NoteResult<()> { + reset_database(&*self.0) + .await + .map_err(NoteStoreError::DBError) + } - /// Fetch page by slug - /// - /// Supports the use case of the user navigating to a known place - /// via a bookmark or other URL. Since the title isn't clear from - /// the slug, the slug is insufficient to generate a new page, so - /// this use case says that in the event of a failure to find the - /// requested page, return a basic NotFound. - pub async fn get_kasten_by_slug(&self, slug: &str) -> NoteResult<(Vec, Vec)> { - let kasten = select_kasten_by_slug(&*self.0, &NoteId(slug.to_string())).await?; + /// Fetch page by slug + /// + /// Supports the use case of the user navigating to a known place + /// via a bookmark or other URL. Since the title isn't clear from + /// the slug, the slug is insufficient to generate a new page, so + /// this use case says that in the event of a failure to find the + /// requested page, return a basic NotFound. + pub async fn get_kasten_by_slug(&self, slug: &str) -> NoteResult<(Vec, Vec)> { + let kasten = select_kasten_by_slug(&*self.0, &NoteId(slug.to_string())).await?; if kasten.is_empty() { - return Err(NoteStoreError::NotFound) + return Err(NoteStoreError::NotFound); } let note_id = NoteId(kasten[0].id.clone()); - Ok((kasten, select_backreferences_for_kasten(&*self.0, ¬e_id).await?)) - } + Ok(( + kasten, + select_backreferences_for_kasten(&*self.0, ¬e_id).await?, + )) + } - /// Fetch page by title + /// Fetch page by title - /// The most common use case: the user is navigating by requesting - /// a page. The page either exists or it doesn't. If it - /// doesn't, we go out and make it. Since we know it doesn't exist, - /// we also know no backreferences to it exist, so in that case you - /// get back two empty vecs. - pub async fn get_kasten_by_title(&self, title: &str) -> NoteResult<(Vec, Vec)> { - if title.len() == 0 { - return Err(NoteStoreError::NotFound); - } + /// The most common use case: the user is navigating by requesting + /// a page. The page either exists or it doesn't. If it + /// doesn't, we go out and make it. Since we know it doesn't exist, + /// we also know no backreferences to it exist, so in that case you + /// get back two empty vecs. + pub async fn get_kasten_by_title(&self, title: &str) -> NoteResult<(Vec, Vec)> { + if title.len() == 0 { + return Err(NoteStoreError::NotFound); + } - let kasten = select_kasten_by_title(&*self.0, title).await?; - if kasten.len() > 0 { + let kasten = select_kasten_by_title(&*self.0, title).await?; + if kasten.len() > 0 { let note_id = NoteId(kasten[0].id.clone()); - return Ok((kasten, select_backreferences_for_kasten(&*self.0, ¬e_id).await?)); - } + return Ok(( + kasten, + select_backreferences_for_kasten(&*self.0, ¬e_id).await?, + )); + } - // Sanity check! - let references = build_references(&title); - if references.len() > 0 { - return Err(NoteStoreError::InvalidNoteStructure( - "Titles may not contain nested references.".to_string(), - )); - } + // Sanity check! + let references = build_references(&title); + if references.len() > 0 { + return Err(NoteStoreError::InvalidNoteStructure( + "Titles may not contain nested references.".to_string(), + )); + } - let mut tx = self.0.begin().await?; - let slug = generate_slug(&mut tx, title).await?; - let zettlekasten = create_zettlekasten(&title, &slug); - let _ = insert_note(&mut tx, &zettlekasten).await?; - tx.commit().await?; + let mut tx = self.0.begin().await?; + let slug = generate_slug(&mut tx, title).await?; + let zettlekasten = create_zettlekasten(&title, &slug); + let _ = insert_note(&mut tx, &zettlekasten).await?; + tx.commit().await?; - Ok((vec![Note::from(zettlekasten)], vec![])) - } + Ok((vec![Note::from(zettlekasten)], vec![])) + } - pub async fn add_note(&self, note: &NewNote, parent_id: &str, location: Option) -> NoteResult { - let new_id = self.insert_note( - note, - &ParentId(parent_id.to_string()), - location, - RelationshipKind::Direct).await?; + pub async fn add_note( + &self, + note: &NewNote, + parent_id: &str, + location: Option, + ) -> NoteResult { + let new_id = self + .insert_note( + note, + &ParentId(parent_id.to_string()), + location, + RelationshipKind::Direct, + ) + .await?; Ok(new_id) - } + } - /// Move a note from one location to another. - pub async fn move_note( - &self, - note_id: &str, - old_parent_id: &str, - new_parent_id: &str, - new_location: i64, - ) -> NoteResult<()> { - let mut tx = self.0.begin().await?; + /// Move a note from one location to another. + pub async fn move_note( + &self, + note_id: &str, + old_parent_id: &str, + new_parent_id: &str, + new_location: i64, + ) -> NoteResult<()> { + let mut tx = self.0.begin().await?; - let old_parent_id = ParentId(old_parent_id.to_string()); - let new_parent_id = ParentId(new_parent_id.to_string()); - let note_id = NoteId(note_id.to_string()); + let old_parent_id = ParentId(old_parent_id.to_string()); + let new_parent_id = ParentId(new_parent_id.to_string()); + let note_id = NoteId(note_id.to_string()); - let old_note = select_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; - let old_note_location = old_note.location; - let old_note_kind = old_note.kind; + let old_note = select_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; + let old_note_location = old_note.location; + let old_note_kind = old_note.kind; - let _ = delete_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; - let _ = close_hole_for_deleted_note(&mut tx, &old_parent_id, old_note_location).await?; - let parent_max_location = assert_max_child_location_for_note(&mut tx, &new_parent_id).await?; - let new_location = cmp::min(parent_max_location + 1, new_location); - let _ = make_room_for_new_note(&mut tx, &new_parent_id, new_location).await?; - let _ = - insert_note_to_note_relationship(&mut tx, &new_parent_id, ¬e_id, new_location, &old_note_kind).await?; - tx.commit().await?; - Ok(()) - } + let _ = delete_note_to_note_relationship(&mut tx, &old_parent_id, ¬e_id).await?; + let _ = close_hole_for_deleted_note(&mut tx, &old_parent_id, old_note_location).await?; + let parent_max_location = + assert_max_child_location_for_note(&mut tx, &new_parent_id).await?; + let new_location = cmp::min(parent_max_location + 1, new_location); + let _ = make_room_for_new_note(&mut tx, &new_parent_id, new_location).await?; + let _ = insert_note_to_note_relationship( + &mut tx, + &new_parent_id, + ¬e_id, + new_location, + &old_note_kind, + ) + .await?; + tx.commit().await?; + Ok(()) + } - /// Updates a note's content. Completely rebuilds the note's - /// outgoing edge reference list every time. - pub async fn update_note_content(&self, note_id: &str, content: &str) -> NoteResult<()> { - let references = build_references(&content); - let note_id = NoteId(note_id.to_string()); + /// Updates a note's content. Completely rebuilds the note's + /// outgoing edge reference list every time. + pub async fn update_note_content(&self, note_id: &str, content: &str) -> NoteResult<()> { + let references = build_references(&content); + let note_id = NoteId(note_id.to_string()); - let mut tx = self.0.begin().await?; - let _ = update_note_content(&mut tx, ¬e_id, &content).await?; - let _ = delete_bulk_note_to_kasten_relationships(&mut tx, ¬e_id).await?; - let found_references = find_all_kasten_from_list_of_references(&mut tx, &references).await?; - let new_references = diff_references(&references, &found_references); - let mut known_reference_ids: Vec = Vec::new(); - for one_reference in new_references.iter() { - let slug = generate_slug(&mut tx, one_reference).await?; - let zettlekasten = create_zettlekasten(&one_reference, &slug); - let _ = insert_note(&mut tx, &zettlekasten).await?; - known_reference_ids.push(NoteId(slug)); - } + let mut tx = self.0.begin().await?; + let _ = update_note_content(&mut tx, ¬e_id, &content).await?; + let _ = delete_bulk_note_to_kasten_relationships(&mut tx, ¬e_id).await?; + let found_references = + find_all_kasten_from_list_of_references(&mut tx, &references).await?; + let new_references = diff_references(&references, &found_references); + let mut known_reference_ids: Vec = Vec::new(); + for one_reference in new_references.iter() { + let slug = generate_slug(&mut tx, one_reference).await?; + let zettlekasten = create_zettlekasten(&one_reference, &slug); + let _ = insert_note(&mut tx, &zettlekasten).await?; + known_reference_ids.push(NoteId(slug)); + } - known_reference_ids.append(&mut found_references.iter().map(|r| NoteId(r.id.clone())).collect()); - let _ = insert_bulk_note_to_kasten_relationships(&mut tx, ¬e_id, &known_reference_ids).await?; - tx.commit().await?; - Ok(()) - } + known_reference_ids.append( + &mut found_references + .iter() + .map(|r| NoteId(r.id.clone())) + .collect(), + ); + let _ = insert_bulk_note_to_kasten_relationships(&mut tx, ¬e_id, &known_reference_ids) + .await?; + tx.commit().await?; + Ok(()) + } - /// Deletes a note. If the note's relationship drops to zero, all - /// references from that note to pages are also deleted. - pub async fn delete_note(&self, note_id: &str, note_parent_id: &str) -> NoteResult<()> { - let mut tx = self.0.begin().await?; - let note_id = NoteId(note_id.to_string()); - let parent_id = ParentId(note_parent_id.to_string()); + /// Deletes a note. If the note's relationship drops to zero, all + /// references from that note to pages are also deleted. + pub async fn delete_note(&self, note_id: &str, note_parent_id: &str) -> NoteResult<()> { + let mut tx = self.0.begin().await?; + let note_id = NoteId(note_id.to_string()); + let parent_id = ParentId(note_parent_id.to_string()); if *parent_id != *note_id { let _ = delete_note_to_note_relationship(&mut tx, &parent_id, ¬e_id); } - // The big one: if zero parents report having an interest in this note, then it, - // *and any sub-relationships*, go away. - if count_existing_note_relationships(&mut tx, ¬e_id).await? == 0 { - let _ = delete_note_to_kasten_relationships(&mut tx, ¬e_id).await?; - let _ = delete_note(&mut tx, ¬e_id).await?; - } - tx.commit().await?; - Ok(()) - } - + // The big one: if zero parents report having an interest in this note, then it, + // *and any sub-relationships*, go away. + if count_existing_note_relationships(&mut tx, ¬e_id).await? == 0 { + let _ = delete_note_to_kasten_relationships(&mut tx, ¬e_id).await?; + let _ = delete_note(&mut tx, ¬e_id).await?; + } + tx.commit().await?; + Ok(()) + } } // The Private stuff impl NoteStore { - // Pretty much the most dangerous function in our system. Has to - // have ALL the error checking. - async fn insert_note( - &self, - note: &NewNote, - parent_id: &ParentId, - location: Option, - kind: RelationshipKind, - ) -> NoteResult { + // Pretty much the most dangerous function in our system. Has to + // have ALL the error checking. + async fn insert_note( + &self, + note: &NewNote, + parent_id: &ParentId, + location: Option, + kind: RelationshipKind, + ) -> NoteResult { if let Some(location) = location { if location < 0 { return Err(NoteStoreError::InvalidNoteStructure( @@ -243,53 +272,60 @@ impl NoteStore { } if parent_id.is_empty() { - return Err(NoteStoreError::InvalidNoteStructure( - "Add note: A parent note ID is required.".to_string(), - )); - } + return Err(NoteStoreError::InvalidNoteStructure( + "Add note: A parent note ID is required.".to_string(), + )); + } - if note.id.is_empty() { - return Err(NoteStoreError::InvalidNoteStructure( - "Add note: Your note should have an id already".to_string(), - )); - } + if note.id.is_empty() { + return Err(NoteStoreError::InvalidNoteStructure( + "Add note: Your note should have an id already".to_string(), + )); + } - if note.content.is_empty() { - return Err(NoteStoreError::InvalidNoteStructure( - "Add note: Empty notes are not supported.".to_string(), - )); - } + if note.content.is_empty() { + return Err(NoteStoreError::InvalidNoteStructure( + "Add note: Empty notes are not supported.".to_string(), + )); + } - let references = build_references(¬e.content); + let references = build_references(¬e.content); - let mut tx = self.0.begin().await?; - let location = { + let mut tx = self.0.begin().await?; + let location = { let max_child = assert_max_child_location_for_note(&mut tx, parent_id).await? + 1; if let Some(location) = location { cmp::min(max_child, location) } else { max_child } - }; + }; - let note_id = NoteId(note.id.clone()); - insert_note(&mut tx, ¬e).await?; - make_room_for_new_note(&mut tx, &parent_id, location).await?; - insert_note_to_note_relationship(&mut tx, &parent_id, ¬e_id, location, &kind).await?; + let note_id = NoteId(note.id.clone()); + insert_note(&mut tx, ¬e).await?; + make_room_for_new_note(&mut tx, &parent_id, location).await?; + insert_note_to_note_relationship(&mut tx, &parent_id, ¬e_id, location, &kind).await?; - let found_references = find_all_kasten_from_list_of_references(&mut tx, &references).await?; - let new_references = diff_references(&references, &found_references); - let mut known_reference_ids: Vec = Vec::new(); - for one_reference in new_references.iter() { - let slug = generate_slug(&mut tx, one_reference).await?; - let zettlekasten = create_zettlekasten(&one_reference, &slug); - let _ = insert_note(&mut tx, &zettlekasten).await?; - known_reference_ids.push(NoteId(slug)); - } + let found_references = + find_all_kasten_from_list_of_references(&mut tx, &references).await?; + let new_references = diff_references(&references, &found_references); + let mut known_reference_ids: Vec = Vec::new(); + for one_reference in new_references.iter() { + let slug = generate_slug(&mut tx, one_reference).await?; + let zettlekasten = create_zettlekasten(&one_reference, &slug); + let _ = insert_note(&mut tx, &zettlekasten).await?; + known_reference_ids.push(NoteId(slug)); + } - known_reference_ids.append(&mut found_references.iter().map(|r| NoteId(r.id.clone())).collect()); - let _ = insert_bulk_note_to_kasten_relationships(&mut tx, ¬e_id, &known_reference_ids).await?; - tx.commit().await?; - Ok(note_id.to_string()) - } + known_reference_ids.append( + &mut found_references + .iter() + .map(|r| NoteId(r.id.clone())) + .collect(), + ); + let _ = insert_bulk_note_to_kasten_relationships(&mut tx, ¬e_id, &known_reference_ids) + .await?; + tx.commit().await?; + Ok(note_id.to_string()) + } }