From 094496961a6ccaed9b06f746e32eeb0788ba039e Mon Sep 17 00:00:00 2001 From: David Westgate Date: Sun, 2 Jun 2024 18:08:22 -0700 Subject: [PATCH] fix add/fetch all questions --- migrations/0_init_schema.sql | 4 +- src/api.rs | 36 ++++++----- src/pg_store.rs | 116 +++++++++++++++++++++++------------ 3 files changed, 102 insertions(+), 54 deletions(-) diff --git a/migrations/0_init_schema.sql b/migrations/0_init_schema.sql index 8de8871..34c82a7 100644 --- a/migrations/0_init_schema.sql +++ b/migrations/0_init_schema.sql @@ -15,7 +15,9 @@ CREATE TABLE IF NOT EXISTS tags ( CREATE TABLE IF NOT EXISTS question_tag ( question_id INT REFERENCES questions(id), tag_id INT REFERENCES tags(id), - PRIMARY KEY (question_id, tag_id) + PRIMARY KEY (question_id, tag_id), + FOREIGN KEY (question_id) REFERENCES questions(id), + FOREIGN KEY (tag_id) REFERENCES tags(id) ); -- Create answers table diff --git a/src/api.rs b/src/api.rs index 9d1e56a..259d7dd 100644 --- a/src/api.rs +++ b/src/api.rs @@ -5,8 +5,9 @@ use self::{ question::{NewQuestion, QuestionDTO}, }; use crate::*; -const DEFAULT_PAGE: usize = 0; -const DEFAULT_PAGE_SIZE: usize = 10; +// sqlx wants 'bigint' = i32 +const DEFAULT_PAGE: i32 = 0; +const DEFAULT_PAGE_SIZE: i32 = 10; /// Fetches a single question using a provided id. First we fetch the question, then the questions tags, then build that into a response DTO /// # Parameters @@ -43,23 +44,28 @@ pub async fn read_questions( State(store): State>>, Query(pagination): Query, ) -> Response { - let page: usize = pagination.page.unwrap_or(DEFAULT_PAGE); - let size: usize = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE); - let start: usize = page * size; + let page: i32 = pagination.page.unwrap_or(DEFAULT_PAGE); + let size: i32 = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE); + let start: i32 = page * size; let questions_option = store.read().await.fetch_many_questions(start, size).await; match questions_option { Some(questions) => { + println!("Num question {}", questions.len()); let mut response_vec_dto: Vec = vec![]; for question in questions { //Not ideal - hitting the database serially for the tags of each invidual question. Can be optimized with more complex sql - let tags = store - .read() - .await - .fetch_tags_by_property(question.id, "question_id") - .await - .unwrap_or(vec![]); - let question_dto: QuestionDTO = QuestionDTO::new(question, tags); - response_vec_dto.push(question_dto) + let tags_option = store.read().await.get_tags_for_question(question.id).await; + match tags_option { + Some(tags) => { + println!("tags {:?}", tags); + let question_dto: QuestionDTO = QuestionDTO::new(question, tags); + response_vec_dto.push(question_dto) + } + None => { + let question_dto: QuestionDTO = QuestionDTO::new(question, vec![]); + response_vec_dto.push(question_dto) + } + } } (StatusCode::OK, Json(response_vec_dto)).into_response() } @@ -179,6 +185,6 @@ pub async fn create_answer( #[derive(Debug, Deserialize)] pub struct Pagination { - page: Option, - size: Option, + page: Option, + size: Option, } diff --git a/src/pg_store.rs b/src/pg_store.rs index 4dc016b..18e43ca 100644 --- a/src/pg_store.rs +++ b/src/pg_store.rs @@ -29,7 +29,7 @@ impl Store { .await { Ok(pool) => pool, - Err(_) => panic!("Could not estable database connection"), + Err(_) => panic!("Could not establish database connection"), }; sqlx::migrate!().run(&db_pool).await.unwrap(); Store { @@ -39,9 +39,12 @@ impl Store { //Get the question tags for a particular question pub async fn get_tags_for_question(&self, question_id: u8) -> Option> { - let query = "SELECT label FROM tag JOIN question_tag on tag.id = question_tag.tag_id JOIN question on tag.question_id = $1"; + let query = "SELECT tags.id, tags.label FROM tags + JOIN question_tag on tags.id = question_tag.tag_id + JOIN questions on question_tag.question_id = questions.id + WHERE questions.id = ($1)"; let result = sqlx::query(query) - .bind(question_id.to_string()) + .bind(i32::from(question_id)) .fetch_all(&self.connection) .await; match result { @@ -52,7 +55,10 @@ impl Store { .collect::>(); Some(tags) } - _ => None, + Err(e) => { + println!("{}", e); + None + } } } @@ -97,47 +103,68 @@ impl Store { // Add new tags to tags table, only if tags with existing label do not exist. If they already exist, just ignore that // Returns list of tags pub async fn add_tags(&mut self, tag_labels: Vec) -> Result, String> { - let insert_values = tag_labels.join(","); - let query = - "INSERT INTO tags (label) VALUES ($1) ON CONFLICT (label) DO NOTHING RETURNING *"; - let result = sqlx::query(query) - .bind(insert_values) - .fetch_all(&self.connection) + let insert_query = " + INSERT INTO tags (label) + SELECT * FROM UNNEST(($1)::text[]) AS label + ON CONFLICT (label) DO NOTHING + "; + + //First run the insert query on the new labels + let insert_result = sqlx::query(insert_query) + .bind(&tag_labels) + .execute(&self.connection) .await; - match result { - Ok(pg_rows) => { - let tags: Vec = pg_rows - .iter() - .map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label"))) - .collect(); - Ok(tags) + match insert_result { + Ok(_) => { + //Then run the select query on the new labels (which may include already existing labels) + let select_query = " + SELECT id, label FROM tags + WHERE label = ANY($1::text[]); + "; + match sqlx::query(select_query) + .bind(&tag_labels) + .fetch_all(&self.connection) + .await + { + Ok(pg_rows) => { + let tags: Vec = pg_rows + .iter() + .map(|pg_row| { + Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")) + }) + .collect(); + Ok(tags) + } + Err(e) => Err(e.to_string()), + } } Err(e) => Err(e.to_string()), } } - //Takes a question id and list of tags, and creates question_tag join associations - //Ignores if association already exists - //Returns list of the question tag associations + /// Takes a question id and list of tags, and creates question_tag join associations + /// Ignores if association already exists + /// Returns list of the question tag associations pub async fn associate_tags( &mut self, question_id: u8, tags: &[Tag], ) -> Result, String> { - //for the query binding we must convert the tag vector to a comma seperated string list of questionid/tagid tuples - let question_id_tag_id_tuple_string = tags - .iter() - .map(|tag| format!("({},{})", question_id, tag.id)) - .collect::>() - .join(","); + let tag_ids_string: Vec = tags.iter().map(|tag| tag.id.to_string()).collect(); - let query = "INSERT INTO question_tag VALUES ($1) ON CONFLICT (question_id, tag_id) DO NOTHING RETURNING *"; + let query = " + INSERT INTO question_tag (question_id, tag_id) + SELECT $1::smallint, UNNEST($2::smallint[]) + ON CONFLICT DO NOTHING; + "; let result = sqlx::query(query) - .bind(question_id_tag_id_tuple_string) + .bind(question_id.to_string()) + .bind(tag_ids_string) .fetch_all(&self.connection) .await; match result { Ok(pg_rows) => { + println!("Num rows {}", pg_rows.len()); let question_tags: Vec = pg_rows .iter() .map(|pg_row| { @@ -178,25 +205,32 @@ impl Store { } // Fetch a list of tags by either the tag id, or by the label. Up to the caller - pub async fn fetch_tags_by_property( + pub async fn _fetch_tags_by_property( &self, propert_id: u8, property_type: &str, ) -> Option> { - let result = sqlx::query("SELECT * FROM tags WHERE $1 = $2") - .bind(property_type) + println!("Property type {}", property_type); + println!("Property id {} ", propert_id); + let query = format!("SELECT * FROM tags WHERE {} = ($2);", property_type).to_string(); //looks risky, but user does not get to control property type + let result = sqlx::query(&query) + .bind(property_type.to_string()) .bind(propert_id.to_string()) .fetch_all(&self.connection) .await; match result { - Ok(tag_rows) => { - let tags: Vec = tag_rows + Ok(pg_rows) => { + println!("num tag rows {}", pg_rows.len()); + let tags: Vec = pg_rows .iter() .map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label"))) .collect(); Some(tags) } - _ => None, + Err(e) => { + println!("err {}", e); + None + } } } @@ -292,15 +326,17 @@ impl Store { } // Fetch many questions - do not worry about joining the tags - pub async fn fetch_many_questions(&self, start: usize, size: usize) -> Option> { + pub async fn fetch_many_questions(&self, start: i32, size: i32) -> Option> { let rows_result: Result, sqlx::Error> = sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2") - .bind(size.to_string()) - .bind(start.to_string()) + .bind(size) + .bind(start) .fetch_all(&self.connection) .await; + match rows_result { Ok(pg_rows) => { + println!("num rows {}", pg_rows.len()); let mut result: Vec = vec![]; for pg_row in pg_rows { result.push(Question::new( @@ -311,7 +347,10 @@ impl Store { } Some(result) } - _ => None, + Err(e) => { + println!("{}", e); + None + } } } @@ -349,6 +388,7 @@ impl Store { } } + // Add an answer entity pub async fn add_answer(&mut self, new_answer: NewAnswer) -> Result { let result = sqlx::query("INSERT INTO answers VALUES ($1,$2) RETURNING id, content, question_id")