deleting colors(+cats) works

This commit is contained in:
Christoph J. Scherr 2024-06-26 12:00:33 +02:00
parent 22fa06e2b6
commit 29ebb7b31a
2 changed files with 274 additions and 249 deletions

View File

@ -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<Connection> {
let mut wd = env::current_dir()?;
let mut dbpath: Option<PathBuf> = 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<bool> {
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::<usize, usize>(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<bool> {
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::<usize, usize>(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<usize> {
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<usize> {
Ok(conn.query_row(
&format!("SELECT id FROM {TABLE_CAT_COLOR} WHERE name = (LOWER(?1))"),
[color.to_string()],
|row| row.get::<usize, usize>(0),
)?)
}
pub fn get_color_by_id(conn: &Connection, id: usize) -> anyhow::Result<Option<String>> {
let maybe = conn.query_row(
&format!("SELECT name FROM {TABLE_CAT_COLOR} WHERE id = (?1)"),
[id.to_string()],
|row| row.get::<usize, String>(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<usize> {
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<String> {
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::<usize, String>(0),
)?)
}

View File

@ -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 /// This demo application uses a sqlite file to store some data. It does *not* use ORM (that would
/// be done with the `diesel` crate.)! /// be done with the `diesel` crate.)!
/// ///
/// A very useful ressource is the /// A very useful ressource is the
/// [rust-cookbook](https://rust-lang-nursery.github.io/rust-cookbook/database/sqlite.html). /// [rust-cookbook](https://rust-lang-nursery.github.io/rust-cookbook/database/sqlite.html).
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
use std::path::PathBuf;
use std::{env, fs, io};
use rusqlite::{Connection, Rows}; use rusqlite::{Connection, Rows};
const DBNAME: &str = "cats.db"; mod db;
const TABLE_CAT_COLOR: &str = "cat_colors"; use db::*;
const TABLE_CAT: &str = "cats";
const USAGE_DELETE: &str = "Usage: const USAGE_DELETE: &str = "Usage:
> D cat 15 (to delete cat with id 15) > D cat 15 (to delete cat with id 15)
> D cat 15 16 (to delete cat with id 15 and 16) > D cat 15 16 (to delete cat with id 15 and 16)
> D color 5 (to delete color with id 5)"; > D color 5 (to delete color with id 5)";
fn connect() -> anyhow::Result<Connection> {
let mut wd = env::current_dir()?;
let mut dbpath: Option<PathBuf> = 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<bool> {
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::<usize, usize>(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<bool> {
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::<usize, usize>(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<usize> {
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<usize> {
Ok(conn.query_row(
&format!("SELECT id FROM {TABLE_CAT_COLOR} WHERE name = (LOWER(?1))"),
[color.to_string()],
|row| row.get::<usize, usize>(0),
)?)
}
fn get_color_by_id(conn: &Connection, id: usize) -> anyhow::Result<Option<String>> {
let maybe = conn.query_row(
&format!("SELECT name FROM {TABLE_CAT_COLOR} WHERE id = (?1)"),
[id.to_string()],
|row| row.get::<usize, String>(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<usize> {
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<String> {
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::<usize, String>(0),
)?)
}
fn interactive_add_cat(conn: &Connection) -> anyhow::Result<usize> { fn interactive_add_cat(conn: &Connection) -> anyhow::Result<usize> {
let stdin = io::stdin(); let stdin = io::stdin();
print!("the name of your cat?\n> "); print!("the name of your cat?\n> ");
@ -234,6 +66,97 @@ fn interactive_find_cat(conn: &Connection) -> anyhow::Result<()> {
Ok(()) 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<usize> = 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<usize> = 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<()> { fn print_colors(_conn: &Connection, colors: &mut Rows) -> anyhow::Result<()> {
println!("{: <14}| {: <19}", "id", "name"); println!("{: <14}| {: <19}", "id", "name");
println!("{:=^80}", ""); println!("{:=^80}", "");
@ -247,6 +170,9 @@ fn print_colors(_conn: &Connection, colors: &mut Rows) -> anyhow::Result<()> {
Ok(()) 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<()> { fn print_cats(conn: &Connection, cats: &mut Rows) -> anyhow::Result<()> {
println!( println!(
"{: <14}| {: <19}| {: <19} -> {: <19}", "{: <14}| {: <19}| {: <19} -> {: <19}",
@ -289,7 +215,7 @@ fn main() -> anyhow::Result<()> {
loop { loop {
buf.clear(); buf.clear();
print!("{}[2J", 27 as char); // clear terminal // print!("{}[2J", 27 as char); // clear terminal
io::stdout().flush()?; io::stdout().flush()?;
print!("(A)dd a cat, (F)ind a cat, (P)rint out all data, (D)elete data, or (E)xit?\n> "); print!("(A)dd a cat, (F)ind a cat, (P)rint out all data, (D)elete data, or (E)xit?\n> ");
io::stdout().flush()?; io::stdout().flush()?;
@ -303,87 +229,12 @@ fn main() -> anyhow::Result<()> {
} else if buf.starts_with('P') { } else if buf.starts_with('P') {
print_all_data(&conn)?; print_all_data(&conn)?;
} else if buf.starts_with('D') { } else if buf.starts_with('D') {
let words: Vec<&str> = buf.split(' ').collect(); interactive_delete(&conn, &mut buf)?;
dbg!(&words);
if words.len() < 2 {
println!("{USAGE_DELETE}");
} else {
let mode = words[1];
let nums: Vec<usize> = words[2..]
.iter()
.map(|word| {
word.parse::<usize>()
.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<usize> = 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}");
}
}
}
} else if buf.starts_with('E') { } else if buf.starts_with('E') {
println!("Goodbye"); println!("Goodbye");
break; break;
} }
println!("\n(Enter to continue)"); println!("\n");
let _ = stdin.lock().read_line(&mut buf); // wait for enter as confirmation
} }
Ok(()) Ok(())