From 29ebb7b31a2d3d50e19b8b2638613c8162f1af82 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Wed, 26 Jun 2024 12:00:33 +0200 Subject: [PATCH] deleting colors(+cats) works --- members/sqlite-demo/src/db.rs | 174 ++++++++++++++++ members/sqlite-demo/src/main.rs | 349 +++++++++----------------------- 2 files changed, 274 insertions(+), 249 deletions(-) create mode 100644 members/sqlite-demo/src/db.rs diff --git a/members/sqlite-demo/src/db.rs b/members/sqlite-demo/src/db.rs new file mode 100644 index 0000000..7433e7e --- /dev/null +++ b/members/sqlite-demo/src/db.rs @@ -0,0 +1,174 @@ +use std::path::PathBuf; +use std::{env, fs}; + +use rusqlite::Connection; +pub const DBNAME: &str = "cats.db"; +pub const TABLE_CAT_COLOR: &str = "cat_colors"; +pub const TABLE_CAT: &str = "cats"; + + +pub fn connect() -> anyhow::Result { + let mut wd = env::current_dir()?; + let mut dbpath: Option = None; + + // does the current directory have a data/cats.db ? (the file need not exist, we can create it + // if data exists) + { + let mut wddata = wd.clone(); + wddata.push("data"); + if wddata.exists() { + println!("found {DBNAME} in {:?}", &wddata); + wddata.push(DBNAME); + dbpath = Some(wddata); + } + } + + // otherwise, does the current or any higher directories contain a cats.db ? + 'search_dir: while dbpath.is_none() { + for entry in fs::read_dir(&wd)? { + let entry = entry?; + if entry.file_name() == DBNAME { + println!("found {DBNAME} in {:?}", wd); + dbpath = Some(entry.path()); + break 'search_dir; + } + } + if !wd.pop() { + // we are at the root! + break 'search_dir; + } + } + + // if all fails, use $PWD/data/cats.db + if dbpath.is_none() { + println!( + "No {DBNAME} found, using {:?}/data/{DBNAME}", + env::current_dir()? + ); + fs::create_dir("data")?; + let mut path = env::current_dir()?; + path.push("data"); + path.push(DBNAME); + dbpath = Some(path); + } + + let conn = Connection::open(dbpath.unwrap())?; + + Ok(conn) +} + +pub fn setup(conn: &Connection) -> anyhow::Result<()> { + conn.execute( + &format!( + "CREATE TABLE IF NOT EXISTS {TABLE_CAT_COLOR} ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE COLLATE NOCASE + )" + ), + (), + )?; + conn.execute( + &format!( + "CREATE TABLE IF NOT EXISTS {TABLE_CAT} ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL COLLATE NOCASE, + color_id INTEGER NOT NULL REFERENCES CAT_COLORS(ID) + )" + ), + (), + )?; + + Ok(()) +} + +// color names are unique, so we use the color name instead of the id because it's easier +pub fn check_if_color_exists(conn: &Connection, color: &str) -> anyhow::Result { + let mut stmt = conn.prepare(&format!( + "SELECT EXISTS( SELECT 1 FROM {TABLE_CAT_COLOR} WHERE name = (LOWER(?1)))" + ))?; + + // we need the return or the temporary result is dropped somehow + #[allow(clippy::needless_return)] + return Ok(stmt + .query([color.to_string()])? + .next()? + .unwrap() + .get::(0)? + == 1); +} + +// note that cat names are not UNIQUE, so we need to use the id instead +pub fn check_if_cat_exists(conn: &Connection, id: usize) -> anyhow::Result { + let mut stmt = conn.prepare(&format!( + "SELECT EXISTS( SELECT 1 FROM {TABLE_CAT} WHERE id = (?1))" + ))?; + + // we need the return or the temporary result is dropped somehow + #[allow(clippy::needless_return)] + return Ok(stmt.query([id])?.next()?.unwrap().get::(0)? == 1); +} + +/// Add a new color to the cat_colors table +/// +/// Will fail if the sql fails +/// +/// If the color already exists, will return Ok(id) +/// +/// The table stores the name in lowercase +/// +/// Returns the id of the color +pub fn new_color(conn: &Connection, color: &str) -> anyhow::Result { + if check_if_color_exists(conn, color)? { + return get_color_id(conn, color); + } + + conn.execute( + &format!("INSERT INTO {TABLE_CAT_COLOR} (name) VALUES (LOWER(?1))"), + [color.to_string()], + )?; + + get_color_id(conn, color) +} + +pub fn get_color_id(conn: &Connection, color: &str) -> anyhow::Result { + Ok(conn.query_row( + &format!("SELECT id FROM {TABLE_CAT_COLOR} WHERE name = (LOWER(?1))"), + [color.to_string()], + |row| row.get::(0), + )?) +} + +pub fn get_color_by_id(conn: &Connection, id: usize) -> anyhow::Result> { + let maybe = conn.query_row( + &format!("SELECT name FROM {TABLE_CAT_COLOR} WHERE id = (?1)"), + [id.to_string()], + |row| row.get::(0), + ); + if let Result::Ok(color) = maybe { + Ok(Some(color)) + } else { + let err: rusqlite::Error = maybe.unwrap_err(); + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { + Ok(None) + } else { + Err(err.into()) + } + } +} + +pub fn new_cat(conn: &Connection, name: &str, color_id: usize) -> anyhow::Result { + conn.execute( + &format!("INSERT INTO {TABLE_CAT} (name, color_id) VALUES (LOWER(?1), ?2)"), + [name.to_string(), color_id.to_string()], + )?; + + Ok(conn.last_insert_rowid() as usize) +} + +pub fn get_cat_color(conn: &Connection, id: usize) -> anyhow::Result { + Ok(conn.query_row( + &format!("SELECT cc.name FROM {TABLE_CAT_COLOR} cc, {TABLE_CAT} c WHERE c.id = (?1) AND c.color_id = cc.id"), + [id.to_string()], + |row| row.get::(0), + )?) +} diff --git a/members/sqlite-demo/src/main.rs b/members/sqlite-demo/src/main.rs index 221b4da..f8f38c4 100644 --- a/members/sqlite-demo/src/main.rs +++ b/members/sqlite-demo/src/main.rs @@ -1,189 +1,21 @@ +use std::io; /// This demo application uses a sqlite file to store some data. It does *not* use ORM (that would /// be done with the `diesel` crate.)! /// /// A very useful ressource is the /// [rust-cookbook](https://rust-lang-nursery.github.io/rust-cookbook/database/sqlite.html). use std::io::{BufRead, Write}; -use std::path::PathBuf; -use std::{env, fs, io}; use rusqlite::{Connection, Rows}; -const DBNAME: &str = "cats.db"; -const TABLE_CAT_COLOR: &str = "cat_colors"; -const TABLE_CAT: &str = "cats"; +mod db; +use db::*; const USAGE_DELETE: &str = "Usage: > D cat 15 (to delete cat with id 15) > D cat 15 16 (to delete cat with id 15 and 16) > D color 5 (to delete color with id 5)"; -fn connect() -> anyhow::Result { - let mut wd = env::current_dir()?; - let mut dbpath: Option = None; - - // does the current directory have a data/cats.db ? (the file need not exist, we can create it - // if data exists) - { - let mut wddata = wd.clone(); - wddata.push("data"); - if wddata.exists() { - println!("found {DBNAME} in {:?}", &wddata); - wddata.push(DBNAME); - dbpath = Some(wddata); - } - } - - // otherwise, does the current or any higher directories contain a cats.db ? - 'search_dir: while dbpath.is_none() { - for entry in fs::read_dir(&wd)? { - let entry = entry?; - if entry.file_name() == DBNAME { - println!("found {DBNAME} in {:?}", wd); - dbpath = Some(entry.path()); - break 'search_dir; - } - } - if !wd.pop() { - // we are at the root! - break 'search_dir; - } - } - - // if all fails, use $PWD/data/cats.db - if dbpath.is_none() { - println!( - "No {DBNAME} found, using {:?}/data/{DBNAME}", - env::current_dir()? - ); - fs::create_dir("data")?; - let mut path = env::current_dir()?; - path.push("data"); - path.push(DBNAME); - dbpath = Some(path); - } - - let conn = Connection::open(dbpath.unwrap())?; - - Ok(conn) -} - -fn setup(conn: &Connection) -> anyhow::Result<()> { - conn.execute( - &format!( - "CREATE TABLE IF NOT EXISTS {TABLE_CAT_COLOR} ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL UNIQUE COLLATE NOCASE - )" - ), - (), - )?; - conn.execute( - &format!( - "CREATE TABLE IF NOT EXISTS {TABLE_CAT} ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL COLLATE NOCASE, - color_id INTEGER NOT NULL REFERENCES CAT_COLORS(ID) - )" - ), - (), - )?; - - Ok(()) -} - -// color names are unique, so we use the color name instead of the id because it's easier -fn check_if_color_exists(conn: &Connection, color: &str) -> anyhow::Result { - let mut stmt = conn.prepare(&format!( - "SELECT EXISTS( SELECT 1 FROM {TABLE_CAT_COLOR} WHERE name = (LOWER(?1)))" - ))?; - - // we need the return or the temporary result is dropped somehow - #[allow(clippy::needless_return)] - return Ok(stmt - .query([color.to_string()])? - .next()? - .unwrap() - .get::(0)? - == 1); -} - -// note that cat names are not UNIQUE, so we need to use the id instead -fn check_if_cat_exists(conn: &Connection, id: usize) -> anyhow::Result { - let mut stmt = conn.prepare(&format!( - "SELECT EXISTS( SELECT 1 FROM {TABLE_CAT} WHERE id = (?1))" - ))?; - - // we need the return or the temporary result is dropped somehow - #[allow(clippy::needless_return)] - return Ok(stmt.query([id])?.next()?.unwrap().get::(0)? == 1); -} - -/// Add a new color to the cat_colors table -/// -/// Will fail if the sql fails -/// -/// If the color already exists, will return Ok(id) -/// -/// The table stores the name in lowercase -/// -/// Returns the id of the color -fn new_color(conn: &Connection, color: &str) -> anyhow::Result { - if check_if_color_exists(conn, color)? { - return get_color_id(conn, color); - } - - conn.execute( - &format!("INSERT INTO {TABLE_CAT_COLOR} (name) VALUES (LOWER(?1))"), - [color.to_string()], - )?; - - get_color_id(conn, color) -} - -fn get_color_id(conn: &Connection, color: &str) -> anyhow::Result { - Ok(conn.query_row( - &format!("SELECT id FROM {TABLE_CAT_COLOR} WHERE name = (LOWER(?1))"), - [color.to_string()], - |row| row.get::(0), - )?) -} - -fn get_color_by_id(conn: &Connection, id: usize) -> anyhow::Result> { - let maybe = conn.query_row( - &format!("SELECT name FROM {TABLE_CAT_COLOR} WHERE id = (?1)"), - [id.to_string()], - |row| row.get::(0), - ); - if let Result::Ok(color) = maybe { - Ok(Some(color)) - } else { - let err: rusqlite::Error = maybe.unwrap_err(); - if matches!(err, rusqlite::Error::QueryReturnedNoRows) { - Ok(None) - } else { - Err(err.into()) - } - } -} - -fn new_cat(conn: &Connection, name: &str, color_id: usize) -> anyhow::Result { - conn.execute( - &format!("INSERT INTO {TABLE_CAT} (name, color_id) VALUES (LOWER(?1), ?2)"), - [name.to_string(), color_id.to_string()], - )?; - - Ok(conn.last_insert_rowid() as usize) -} - -fn get_cat_color(conn: &Connection, id: usize) -> anyhow::Result { - Ok(conn.query_row( - &format!("SELECT cc.name FROM {TABLE_CAT_COLOR} cc, {TABLE_CAT} c WHERE c.id = (?1) AND c.color_id = cc.id"), - [id.to_string()], - |row| row.get::(0), - )?) -} - fn interactive_add_cat(conn: &Connection) -> anyhow::Result { let stdin = io::stdin(); print!("the name of your cat?\n> "); @@ -234,6 +66,97 @@ fn interactive_find_cat(conn: &Connection) -> anyhow::Result<()> { Ok(()) } +fn interactive_delete(conn: &Connection, buf: &mut String) -> anyhow::Result<()> { + let words: Vec<&str> = buf.split(' ').collect(); + if words.len() < 2 { + println!("{USAGE_DELETE}"); + } else { + let stdin = io::stdin(); + let mode = words[1]; + let mut nums: Vec = Vec::new(); + for word in words[2..].iter() { + nums.push(match word.parse() { + Ok(n) => n, + Err(e) => { + eprintln!("Could not parse '{word}' to id: {e}"); + continue; + } + }) + } + match mode { + "CAT" => { + let mut stmt = conn.prepare(&format!("DELETE FROM {TABLE_CAT} WHERE id = (?1)"))?; + for n in nums { + stmt.execute([n])?; + } + } + "COLOR" => { + // Cats have colors, so if we delete a color, we need to delete cats with + // that color too. + let mut stmt_how_many_cats_with_color = conn.prepare(&format!( + "SELECT COUNT(1) FROM {TABLE_CAT} c, {TABLE_CAT_COLOR} + cc WHERE c.color_id = (?1) AND cc.id = (?1)" + ))?; + // FIXME: this must still be wrong? + let mut stmt_cats_with_color = conn.prepare(&format!( + "SELECT c.* FROM {TABLE_CAT} c, {TABLE_CAT_COLOR} cc + WHERE c.color_id = (?1) AND cc.id = (?1)" + ))?; + // works: `SELECT cats.* FROM cats, cat_colors WHERE cats.color_id = 2 AND cat_colors.id = 2;` + + for color_id in &nums { + let cats_amount: usize = stmt_how_many_cats_with_color + .query_row([color_id], |row| row.get::<_, usize>(0))?; + + if cats_amount > 0 { + let mut cats = stmt_cats_with_color.query([color_id])?; // Get the cats + // that would be deleted + println!( + "\nYou are about to also delete these cats,\n\ + as they have the color id {color_id}. Type 'YES' to confirm." + ); + print_cats(conn, &mut cats)?; + drop(cats); // we need to renew this, because Rows is not Clone and we have + // consumed the Rows in print_cats. The Rows index can not be reset, + // probably because of a sqlite API limitation. + + let mut cats = stmt_cats_with_color.query([color_id])?; + let mut cat_ids: Vec = Vec::new(); + + while let Some(cat) = cats.next()? { + cat_ids.push(cat.get::<_, usize>(0)?); + } + buf.clear(); + let _ = stdin.lock().read_line(buf); // wait for enter as confirmation + *buf = buf.trim().to_string(); + *buf = buf.to_uppercase().to_string(); + dbg!(&buf); + if buf.as_str() != "YES" { + continue; + } + let mut stmt = + conn.prepare(&format!("DELETE FROM {TABLE_CAT} WHERE id = (?1)"))?; + for cat_id in cat_ids { + stmt.execute([cat_id])?; + } + } + + let mut stmt = + conn.prepare(&format!("DELETE FROM {TABLE_CAT_COLOR} WHERE id = (?1)"))?; + for n in &nums { + stmt.execute([n])?; + } + } + println!("deleted ids {nums:?}"); + } + _ => { + println!("{USAGE_DELETE}"); + } + } + } + Ok(()) +} + fn print_colors(_conn: &Connection, colors: &mut Rows) -> anyhow::Result<()> { println!("{: <14}| {: <19}", "id", "name"); println!("{:=^80}", ""); @@ -247,6 +170,9 @@ fn print_colors(_conn: &Connection, colors: &mut Rows) -> anyhow::Result<()> { Ok(()) } +/// Print [Rows] of cats. +/// +/// This needs all columns of the [TABLE_CAT], otherwise it will error. fn print_cats(conn: &Connection, cats: &mut Rows) -> anyhow::Result<()> { println!( "{: <14}| {: <19}| {: <19} -> {: <19}", @@ -289,7 +215,7 @@ fn main() -> anyhow::Result<()> { loop { buf.clear(); - print!("{}[2J", 27 as char); // clear terminal + // print!("{}[2J", 27 as char); // clear terminal io::stdout().flush()?; print!("(A)dd a cat, (F)ind a cat, (P)rint out all data, (D)elete data, or (E)xit?\n> "); io::stdout().flush()?; @@ -303,87 +229,12 @@ fn main() -> anyhow::Result<()> { } else if buf.starts_with('P') { print_all_data(&conn)?; } else if buf.starts_with('D') { - let words: Vec<&str> = buf.split(' ').collect(); - dbg!(&words); - if words.len() < 2 { - println!("{USAGE_DELETE}"); - } else { - let mode = words[1]; - let nums: Vec = words[2..] - .iter() - .map(|word| { - word.parse::() - .expect("could not parse db integer to rust integer") - }) - .collect(); - dbg!(&nums); - match mode { - "CAT" => { - let mut stmt = - conn.prepare(&format!("DELETE FROM {TABLE_CAT} WHERE id = (?1)"))?; - for n in nums { - stmt.execute([n])?; - } - } - "COLOR" => { - // Cats have colors, so if we delete a color, we need to delete cats with - // that color too. - let mut stmt_how_many_cats_with_color = conn.prepare(&format!( - "SELECT cc.id FROM {TABLE_CAT} c, {TABLE_CAT_COLOR} - cc WHERE c.color_id = (?1)" - ))?; - let mut stmt_cats_with_color = conn.prepare(&format!( - "SELECT cc.id FROM {TABLE_CAT} c, {TABLE_CAT_COLOR} - cc WHERE c.color_id = (?1)" - ))?; - - for color_id in &nums { - let cats_amount: usize = stmt_how_many_cats_with_color - .query_row([color_id], |row| row.get::<_, usize>(0))?; - - if cats_amount > 0 { - // FIXME: cats is empty? - let mut cats = stmt_cats_with_color.query([color_id])?; - let mut cat_ids: Vec = Vec::new(); - - while let Some(cat) = cats.next()? { - cat_ids.push(cat.get::<_, usize>(0)?); - } - - println!("You are about to also delete these cats, as they have the color id {color_id}. Type 'YES' to confirm"); - print_cats(&conn, &mut cats)?; - buf.clear(); - let _ = stdin.lock().read_line(&mut buf); // wait for enter as confirmation - buf = buf.trim().to_string(); - buf = buf.to_uppercase().to_string(); - if buf.as_str() != "YES" { - continue; - } - // FIXME: I DELETED ALL THE CATS OH NO - let mut stmt = conn - .prepare(&format!("DELETE FROM {TABLE_CAT} WHERE id = (?1)"))?; - for cat_id in cat_ids { - stmt.execute([cat_id])?; - } - } - - let mut stmt = conn.prepare(&format!( - "DELETE FROM {TABLE_CAT_COLOR} WHERE id = (?1)" - ))?; - let _ = nums.iter().map(|n| stmt.execute([n])); - } - } - _ => { - println!("{USAGE_DELETE}"); - } - } - } + interactive_delete(&conn, &mut buf)?; } else if buf.starts_with('E') { println!("Goodbye"); break; } - println!("\n(Enter to continue)"); - let _ = stdin.lock().read_line(&mut buf); // wait for enter as confirmation + println!("\n"); } Ok(())