From 9337b98ad3c4b10a89ed2db9cc336e36230ae75c Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Wed, 4 Nov 2020 17:53:25 -0800 Subject: [PATCH] REFACTOR Again! note->note and note->kasten are now separate tables This was getting semantically confusing, so I decided to short circuit the whole mess by separating the two. The results are promising. It does mean that deleting a note means traversing two tables to clean out all the cruft, which is *sigh*, but it also means that the tree is stored in one table and the graph in another, giving us a much better separation of concerns down at the SQL layer. --- server/nm-store/src/lib.rs | 12 +- .../nm-store/src/sql/initialize_database.sql | 17 +- .../select_notes_backreferencing_kasten.sql | 72 ++ server/nm-store/src/store.rs | 77 ++- server/nm-store/src/store_private.rs | 623 ++++++++++-------- server/nm-store/src/structs.rs | 163 ++--- 6 files changed, 595 insertions(+), 369 deletions(-) create mode 100644 server/nm-store/src/sql/select_notes_backreferencing_kasten.sql diff --git a/server/nm-store/src/lib.rs b/server/nm-store/src/lib.rs index c57a104..27fda81 100644 --- a/server/nm-store/src/lib.rs +++ b/server/nm-store/src/lib.rs @@ -29,8 +29,8 @@ mod tests { #[tokio::test(threaded_scheduler)] async fn fetching_unfound_page_by_slug_works() { let storagepool = fresh_inmemory_database().await; - let foundkasten = storagepool.get_kasten_by_slug("nonexistent-kasten").await.unwrap(); - assert_eq!(foundkasten.len(), 0, "{:?}", foundkasten); + let foundkasten = storagepool.get_kasten_by_slug("nonexistent-kasten").await; + assert!(foundkasten.is_err()); } // Request for the page by title. If the page exists, return it. @@ -45,7 +45,7 @@ mod tests { let newpageresult = storagepool.get_kasten_by_title(&title).await; assert!(newpageresult.is_ok(), "{:?}", newpageresult); - let newpages = newpageresult.unwrap(); + let (newpages, _) = newpageresult.unwrap(); assert_eq!(newpages.len(), 1); let newpage = newpages.iter().next().unwrap(); @@ -73,7 +73,7 @@ mod tests { let newpageresult = storagepool.get_kasten_by_title(&title).await; assert!(newpageresult.is_ok(), "{:?}", newpageresult); - let newpages = newpageresult.unwrap(); + let (newpages, _) = newpageresult.unwrap(); assert_eq!(newpages.len(), 1); let root = &newpages[0]; @@ -102,7 +102,7 @@ mod tests { let newpageresult = storagepool.get_kasten_by_title(&title).await; assert!(newpageresult.is_ok(), "{:?}", newpageresult); - let newpages = newpageresult.unwrap(); + let (newpages, _) = newpageresult.unwrap(); assert_eq!(newpages.len(), 5); let newroot = newpages.iter().next().unwrap(); @@ -113,4 +113,6 @@ mod tests { assert_eq!(newpages[1].parent_id, Some(newroot.id.clone())); assert_eq!(newpages[2].parent_id, Some(newpages[1].id.clone())); } + + } diff --git a/server/nm-store/src/sql/initialize_database.sql b/server/nm-store/src/sql/initialize_database.sql index 79583f1..e72adba 100644 --- a/server/nm-store/src/sql/initialize_database.sql +++ b/server/nm-store/src/sql/initialize_database.sql @@ -1,6 +1,6 @@ DROP TABLE IF EXISTS notes; DROP TABLE IF EXISTS note_relationships; -DROP INDEX IF EXISTS note_ids; +DROP TABLE IF EXISTS note_kasten_relationships; DROP TABLE IF EXISTS favorites; CREATE TABLE notes ( @@ -21,6 +21,10 @@ CREATE TABLE favorites ( FOREIGN KEY (id) REFERENCES notes (id) ON DELETE CASCADE ); +-- This table represents the forest of data relating a kasten to its +-- collections of notes. The root is itself "a note," but the content +-- of that note will always be just the title of the kasten. +-- CREATE TABLE note_relationships ( note_id TEXT NOT NULL, parent_id TEXT NOT NULL, @@ -31,3 +35,14 @@ CREATE TABLE note_relationships ( FOREIGN KEY (parent_id) REFERENCES notes (id) ON DELETE CASCADE ); +-- This table represents the graph of data relating notes to kastens. +-- +CREATE TABLE note_kasten_relationships ( + note_id TEXT NOT NULL, + kasten_id TEXT NOT NULL, + kind TEXT NOT NULL, + -- If either note disappears, we want all the edges to disappear as well. + FOREIGN KEY (note_id) REFERENCES notes (id) ON DELETE CASCADE, + FOREIGN KEY (kasten_id) REFERENCES notes (id) ON DELETE CASCADE +); + diff --git a/server/nm-store/src/sql/select_notes_backreferencing_kasten.sql b/server/nm-store/src/sql/select_notes_backreferencing_kasten.sql new file mode 100644 index 0000000..32b1962 --- /dev/null +++ b/server/nm-store/src/sql/select_notes_backreferencing_kasten.sql @@ -0,0 +1,72 @@ +SELECT + id, + parent_id, + content, + location, + kind, + creation_date, + updated_date, + lastview_date, + deleted_date + +FROM ( + + WITH RECURSIVE parents ( + id, + parent_id, + content, + location, + kind, + creation_date, + updated_date, + lastview_date, + deleted_date, + cycle + ) + + AS ( + + SELECT + notes.id, + note_parents.id, + notes.content, + note_relationships.location, + notes.kind, + notes.creation_date, + notes.updated_date, + notes.lastview_date, + notes.deleted_date, + ','||notes.id||',' + FROM notes + INNER JOIN note_relationships + ON notes.id = note_relationships.note_id + AND notes.kind = 'note' + INNER JOIN notes as note_parents + ON note_parents.id = note_relationships.parent_id + WHERE notes.id + IN (SELECT note_id + FROM note_kasten_relationships + WHERE kasten_id = ?) -- IMPORTANT: THIS IS THE PARAMETER + + UNION + SELECT DISTINCT + notes.id, + next_parent.id, + notes.content, + note_relationships.location, + notes.kind, + notes.creation_date, + notes.updated_date, + notes.lastview_date, + notes.deleted_date, + parents.cycle||notes.id||',' + FROM notes + INNER JOIN parents + ON parents.parent_id = notes.id + LEFT JOIN note_relationships + ON note_relationships.note_id = notes.id + LEFT JOIN notes as next_parent + ON next_parent.id = note_relationships.parent_id + WHERE parents.cycle NOT LIKE '%,'||notes.id||',%' + ) + SELECT * from parents); diff --git a/server/nm-store/src/store.rs b/server/nm-store/src/store.rs index bc82916..844cc2a 100644 --- a/server/nm-store/src/store.rs +++ b/server/nm-store/src/store.rs @@ -92,25 +92,35 @@ impl NoteStore { /// 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> { - let maybekasten = select_kasten_by_slug(&*self.0, slug).await; - match maybekasten { - Ok(v) => Ok(v), - Err(sqlx::Error::RowNotFound) => Err(NoteStoreError::NotFound), - Err(_) => maybekasten.map_err(NoteStoreError::DBError), - } + 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) + } + + let note_id = NoteId(kasten[0].id.clone()); + Ok((kasten, select_backreferences_for_kasten(&*self.0, ¬e_id).await?)) } - pub async fn get_kasten_by_title(&self, title: &str) -> NoteResult> { + /// 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); } let kasten = select_kasten_by_title(&*self.0, title).await?; if kasten.len() > 0 { - return Ok(kasten); + let note_id = NoteId(kasten[0].id.clone()); + 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( @@ -124,11 +134,17 @@ impl NoteStore { let _ = insert_note(&mut tx, &zettlekasten).await?; tx.commit().await?; - Ok(vec![Note::from(zettlekasten)]) + Ok((vec![Note::from(zettlekasten)], vec![])) } pub async fn add_note(&self, note: &NewNote, parent_id: &str, location: i64) -> NoteResult { - self.insert_note(note, &ParentId(parent_id.to_string()), location, RelationshipKind::Direct).await + self.insert_note( + note, + &ParentId(parent_id.to_string()), + location, + RelationshipKind::Direct, + ) + .await } /// Move a note from one location to another. @@ -141,10 +157,10 @@ impl NoteStore { ) -> 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; @@ -164,12 +180,12 @@ impl NoteStore { /// 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()); - - 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. + let note_id = NoteId(note_id.to_string()); + let parent_id = ParentId(note_parent_id.to_string()); + + 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?; @@ -182,11 +198,12 @@ impl NoteStore { /// 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 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 found_references = find_all_kasten_references_for(&mut tx, &references).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() { @@ -201,8 +218,6 @@ impl NoteStore { tx.commit().await?; Ok(()) } - - } // The Private stuff @@ -210,7 +225,13 @@ impl NoteStore { 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: i64, kind: RelationshipKind) -> NoteResult { + async fn insert_note( + &self, + note: &NewNote, + parent_id: &ParentId, + location: i64, + kind: RelationshipKind, + ) -> NoteResult { if location < 0 { return Err(NoteStoreError::InvalidNoteStructure( "Add note: A negative position is not valid.".to_string(), @@ -243,12 +264,12 @@ impl NoteStore { location, ); - let note_id = NoteId(note.id.clone()); + 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_references_for(&mut tx, &references).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() { diff --git a/server/nm-store/src/store_private.rs b/server/nm-store/src/store_private.rs index dd3a05f..c038d64 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, Executor, Done}; +use sqlx::{sqlite::Sqlite, Done, Executor}; use std::collections::HashSet; type SqlResult = sqlx::Result; @@ -18,6 +18,12 @@ type SqlResult = sqlx::Result; // coherent and easily readable, and hides away the gnarliness of some // of the SQL queries. +// Important!!! Note_relationships are usually (parent_note -> note), +// but Note to Kasten relationships are always (note-as-parent -> +// kasten_note), so when looking for "all the notes referring to this +// kasten", you use the kasten's id as the TARGET note_id, and the +// note referring to the kasten in the parent_id. + lazy_static! { static ref SELECT_KASTEN_BY_TITLE_SQL: String = str::replace( include_str!("sql/select_notes_by_parameter.sql"), @@ -34,6 +40,17 @@ lazy_static! { ); } +lazy_static! { + static ref SELECT_NOTES_BACKREFENCING_KASTEN_SQL: &'static str = + include_str!("sql/select_notes_backreferencing_kasten.sql"); +} + +// ___ _ +// | _ \___ ___ ___| |_ +// | / -_|_-(executor: E) -> SqlResult<()> where E: Executor<'a, Database = Sqlite>, @@ -42,12 +59,18 @@ where sqlx::query(initialize_sql).execute(executor).await.map(|_| ()) } -pub(crate) async fn select_kasten_by_slug<'a, E>(executor: E, slug: &str) -> SqlResult> +// ___ _ _ _ __ _ +// | __|__| |_ __| |_ | |/ /__ _ __| |_ ___ _ _ +// | _/ -_) _/ _| ' \ | ' (executor: E, slug: &NoteId) -> SqlResult> where E: Executor<'a, Database = Sqlite>, { let r: Vec = sqlx::query_as(&SELECT_KASTEN_BY_ID_SQL) - .bind(&slug) + .bind(&**slug) .fetch_all(executor) .await?; Ok(r.into_iter().map(|z| Note::from(z)).collect()) @@ -64,28 +87,23 @@ where Ok(r.into_iter().map(|z| Note::from(z)).collect()) } -pub(crate) async fn select_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &ParentId, - note_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>, { - 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) + let r: Vec = sqlx::query_as(&SELECT_NOTES_BACKREFENCING_KASTEN_SQL) + .bind(&**kasten_id) + .fetch_all(executor) .await?; - Ok(NoteRelationship::from(s)) + Ok(r.into_iter().map(|z| Note::from(z)).collect()) } +// ___ _ ___ _ _ _ +// |_ _|_ _ ___ ___ _ _| |_ / _ \ _ _ ___ | \| |___| |_ ___ +// | || ' \(_-(executor: E, zettle: &NewNote) -> SqlResult where E: Executor<'a, Database = Sqlite>, @@ -108,246 +126,11 @@ where Ok(zettle.id.clone()) } -pub(crate) async fn update_note_content<'a, E>(executor: E, note_id: &NoteId, content: &str) -> SqlResult<()> -where - 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(); - - match count { - 1 => Ok(()), - _ => Err(sqlx::Error::RowNotFound), - } -} - -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>, -{ - 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(()) -} - -pub(crate) async fn assert_max_child_location_for_note<'a, E>(executor: E, note_id: &ParentId) -> SqlResult -where - 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 count: RowCount = sqlx::query_as(assert_max_child_location_for_note_sql) - .bind(&**note_id) - .fetch_one(executor) - .await?; - - Ok(count.count) -} - -pub(crate) async fn insert_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &ParentId, - note_id: &NoteId, - location: i64, - kind: &RelationshipKind, -) -> SqlResult<()> -where - 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 _ = 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 insert_bulk_note_to_kasten_relationships<'a, E>( - executor: E, - note_id: &NoteId, - references: &[NoteId], -) -> SqlResult<()> -where - E: Executor<'a, Database = Sqlite>, -{ - if references.is_empty() { - return Ok(()); - } - - let insert_pattern = format!("(?, ?, 0, {})", RelationshipKind::Kasten.to_string()); - let insert_note_page_references_sql = - "INSERT INTO note_relationships (parent_id, note_id, location, 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); - } - - request.execute(executor).await.map(|_| ()) -} - - -pub(crate) async fn find_all_kasten_references_for<'a, E>(executor: E, references: &[String]) -> SqlResult> -where - E: Executor<'a, Database = Sqlite>, -{ - 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() - ); - } - - 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 -} - -pub(crate) async fn delete_note_to_note_relationship<'a, E>( - executor: E, - parent_id: &ParentId, - note_id: &NoteId, -) -> SqlResult<()> -where - E: Executor<'a, Database = Sqlite>, -{ - 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(); - - 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<()> -where - 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 = ?;", - RelationshipKind::Kasten.to_string(), - RelationshipKind::Unacked.to_string() - ); - } - - 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>, -{ - 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(); - - 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) -> SqlResult<()> -where - 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 _ = sqlx::query(close_hole_for_deleted_note_sql) - .bind(&location) - .bind(&**parent_id) - .execute(executor) - .await?; - Ok(()) -} - -pub(crate) async fn count_existing_note_relationships<'a, E>(executor: E, note_id: &NoteId) -> SqlResult -where - E: Executor<'a, Database = Sqlite>, -{ - lazy_static! { - static ref COUNT_EXISTING_NOTE_RELATIONSHIPS_SQL: String = format!( - "SELECT COUNT(*) as count FROM note_relationships WHERE kind IN ({}, {}, {}) and note_id = ?;", - RelationshipKind::Direct.to_string(), - RelationshipKind::Reference.to_string(), - RelationshipKind::Embed.to_string(), - ); - } - - let count: RowCount = sqlx::query_as(&COUNT_EXISTING_NOTE_RELATIONSHIPS_SQL) - .bind(&**note_id) - .fetch_one(executor) - .await?; - - Ok(count.count) -} - -// 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() -} +// ___ _ _ _ _ __ _ +// | _ )_ _(_) |__| | | |/ /__ _ __| |_ ___ _ _ +// | _ \ || | | / _` | | ' NewNote { .build() .unwrap() } + +// _ _ _ _ ___ _ _ _ +// | | | |_ __ __| |__ _| |_ ___ / _ \ _ _ ___ | \| |___| |_ ___ +// | |_| | '_ \/ _` / _` | _/ -_) | (_) | ' \/ -_) | .` / _ \ _/ -_) +// \___/| .__/\__,_\__,_|\__\___| \___/|_||_\___| |_|\_\___/\__\___| +// |_| + +pub(crate) async fn update_note_content<'a, E>(executor: E, note_id: &NoteId, content: &str) -> SqlResult<()> +where + 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(); + + match count { + 1 => Ok(()), + _ => Err(sqlx::Error::RowNotFound), + } +} + +// ___ _ _ ___ _ _ _ ___ _ _ _ _ _ +// | __|__| |_ __| |_ / _ \ _ _ ___ | \| |___| |_ ___ | _ \___| |__ _| |_(_)___ _ _ __| |_ (_)_ __ +// | _/ -_) _/ _| ' \ | (_) | ' \/ -_) | .` / _ \ _/ -_) | / -_) / _` | _| / _ \ ' \(_-< ' \| | '_ \ +// |_|\___|\__\__|_||_| \___/|_||_\___| |_|\_\___/\__\___| |_|_\___|_\__,_|\__|_\___/_||_/__/_||_|_| .__/ +// |_| + +pub(crate) async fn select_note_to_note_relationship<'a, E>( + executor: E, + parent_id: &ParentId, + note_id: &NoteId, +) -> SqlResult +where + 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)) +} + +// _ _ _ _ _ _ _ ___ _ _ _ _ _ +// | \| |___| |_ ___ | |_ ___ | \| |___| |_ ___ | _ \___| |__ _| |_(_)___ _ _ __| |_ (_)_ __ ___ +// | .` / _ \ _/ -_) | _/ _ \ | .` / _ \ _/ -_) | / -_) / _` | _| / _ \ ' \(_-< ' \| | '_ (_-< +// |_|\_\___/\__\___| \__\___/ |_|\_\___/\__\___| |_|_\___|_\__,_|\__|_\___/_||_/__/_||_|_| .__/__/ +// |_| + +pub(crate) async fn insert_note_to_note_relationship<'a, E>( + executor: E, + parent_id: &ParentId, + note_id: &NoteId, + location: i64, + kind: &RelationshipKind, +) -> SqlResult<()> +where + 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 _ = 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<()> +where + 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 _ = 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 +where + 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 count: RowCount = sqlx::query_as(assert_max_child_location_for_note_sql) + .bind(&**note_id) + .fetch_one(executor) + .await?; + + Ok(count.count) +} + +// _ _ _ _ _ __ _ ___ _ _ _ _ _ +// | \| |___| |_ ___ | |_ ___ | |/ /__ _ __| |_ ___ _ _ | _ \___| |__ _| |_(_)___ _ _ __| |_ (_)_ __ ___ +// | .` / _ \ _/ -_) | _/ _ \ | ' ( + executor: E, + note_id: &NoteId, + references: &[NoteId], +) -> SqlResult<()> +where + E: Executor<'a, Database = Sqlite>, +{ + 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 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(|_| ()) +} + +pub(crate) async fn delete_bulk_note_to_kasten_relationships<'a, E>(executor: E, note_id: &NoteId) -> SqlResult<()> +where + 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(()) +} + +// 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() +} + +// ___ _ _ _ _ __ _ ___ _ _ _ _ _ +// / __|___ _ _| |_ ___ _ _| |_ | |_ ___ | |/ /__ _ __| |_ ___ _ _ | _ \___| |__ _| |_(_)___ _ _ __| |_ (_)_ __ ___ +// | (__/ _ \ ' \ _/ -_) ' \ _| | _/ _ \ | ' ( + executor: E, + references: &[String], +) -> SqlResult> +where + E: Executor<'a, Database = Sqlite>, +{ + 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() + ); + } + + 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 +} + +// ___ _ _ +// | \ ___| |___| |_ ___ +// | |) / -_) / -_) _/ -_) +// |___/\___|_\___|\__\___| +// + +pub(crate) async fn delete_note_to_note_relationship<'a, E>( + executor: E, + parent_id: &ParentId, + note_id: &NoteId, +) -> SqlResult<()> +where + E: Executor<'a, Database = Sqlite>, +{ + 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(); + + 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<()> +where + 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() + ); + } + + 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>, +{ + 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(); + + 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, +) -> SqlResult<()> +where + 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 _ = 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 = ?;"; + + let count: RowCount = sqlx::query_as(&count_existing_note_relationships_sql) + .bind(&**note_id) + .fetch_one(executor) + .await?; + + Ok(count.count) +} diff --git a/server/nm-store/src/structs.rs b/server/nm-store/src/structs.rs index ffef83c..3f19aaa 100644 --- a/server/nm-store/src/structs.rs +++ b/server/nm-store/src/structs.rs @@ -10,92 +10,79 @@ use sqlx::{self, FromRow}; // "cargo," "cell," and so forth. If I'd wanted to go the Full // Noguchi, I guess I could have used "envelope." +// In order to prevent arbitrary enumeration tokens from getting into +// the database, the private layer takes a very hard line on insisting +// that everything sent TO the datastore come in the enumerated +// format, and everything coming OUT of the database be converted back +// into an enumeration. These macros instantiate those objects +// and their conversions to/from strings. + +macro_rules! build_conversion_enums { + ( $ty:ident, $( $s:literal => $x:ident, )*) => { + #[derive(Clone, Debug, PartialEq, Eq)] + pub enum $ty { + $( $x ), * + } + + impl From for $ty { + fn from(kind: String) -> Self { + match &kind[..] { + $( $s => $ty::$x, )* + _ => panic!("Illegal value in $ty database: {}", kind), + } + } + } + + impl From<$ty> for String { + fn from(kind: $ty) -> Self { + match kind { + $( $ty::$x => $s ),* + } + .to_string() + } + } + + impl $ty { + pub fn to_string(&self) -> String { + String::from(self.clone()) + } + } + }; +} + #[derive(Shrinkwrap, Clone)] pub(crate) struct NoteId(pub String); #[derive(Shrinkwrap, Clone)] pub(crate) struct ParentId(pub String); -/// The different kinds of objects we support. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum NoteKind { - Kasten, - Note, - Resource, -} +// The different kinds of objects we support. -impl From for NoteKind { - fn from(kind: String) -> Self { - match &kind[..] { - "box" => NoteKind::Kasten, - "note" => NoteKind::Note, - "resource" => NoteKind::Resource, - _ => panic!("Illegal value in database: {}", kind), - } - } -} +build_conversion_enums!( + NoteKind, + "box" => Kasten, + "note" => Note, + "resource" => Resource, +); -impl From for String { - fn from(kind: NoteKind) -> Self { - match kind { - NoteKind::Kasten => "box", - NoteKind::Note => "note", - NoteKind::Resource => "resource", - } - .to_string() - } -} +// The different kinds of relationships we support. I do not yet +// know how to ensure that there is a maximum of one (a -> +// b)::Direct, and that for any (a -> b) there is no (b <- a), that +// is, nor, for that matter, do I know how to prevent cycles. -impl NoteKind { - pub fn to_string(&self) -> String { - String::from(self.clone()) - } -} +build_conversion_enums!( + RelationshipKind, + "direct" => Direct, + "reference" => Reference, + "embed" => Embed, +); -/// The different kinds of relationships we support. I do not yet -/// know how to ensure that there is a maximum of one (a -> -/// b)::Direct, and that for any (a -> b) there is no (b <- a), that -/// is, nor, for that matter, do I know how to prevent cycles. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RelationshipKind { - Direct, - Reference, - Embed, - Kasten, - Unacked, -} - -impl From for RelationshipKind { - fn from(kind: String) -> Self { - match &kind[..] { - "direct" => RelationshipKind::Direct, - "reference" => RelationshipKind::Reference, - "embed" => RelationshipKind::Embed, - "kasten" => RelationshipKind::Kasten, - "unacked" => RelationshipKind::Unacked, - _ => panic!("Illegal value in database: {}", kind), - } - } -} - -impl From for String { - fn from(kind: RelationshipKind) -> Self { - match kind { - RelationshipKind::Direct => "direct", - RelationshipKind::Reference => "reference", - RelationshipKind::Embed => "embed", - RelationshipKind::Kasten => "kasten", - RelationshipKind::Unacked => "unacked", - } - .to_string() - } -} - -impl RelationshipKind { - pub fn to_string(&self) -> String { - String::from(self.clone()) - } -} +build_conversion_enums!( + KastenRelationshipKind, + "kasten" => Kasten, + "unacked" => Unacked, + "cancelled" => Cancelled, +); // A Note is the base construct of our system. It represents a // single note and contains information about its parent and location. @@ -229,6 +216,30 @@ impl From for NoteRelationship { } } +#[derive(Clone, Debug, FromRow)] +pub(crate) struct KastenRelationshipRow { + pub note_id: String, + pub kasten_id: String, + pub kind: String, +} + +#[derive(Clone, Debug)] +pub(crate) struct KastenRelationship { + pub note_id: String, + pub kasten_id: String, + pub kind: KastenRelationshipKind, +} + +impl From for KastenRelationship { + fn from(rel: KastenRelationshipRow) -> Self { + Self { + kasten_id: rel.kasten_id, + note_id: rel.note_id, + kind: KastenRelationshipKind::from(rel.kind), + } + } +} + #[cfg(test)] mod tests { use super::*;