fix add/fetch all questions

This commit is contained in:
David Westgate 2024-06-02 18:08:22 -07:00
parent 036f4c4779
commit 094496961a
3 changed files with 102 additions and 54 deletions

View File

@ -15,7 +15,9 @@ CREATE TABLE IF NOT EXISTS tags (
CREATE TABLE IF NOT EXISTS question_tag ( CREATE TABLE IF NOT EXISTS question_tag (
question_id INT REFERENCES questions(id), question_id INT REFERENCES questions(id),
tag_id INT REFERENCES tags(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 -- Create answers table

View File

@ -5,8 +5,9 @@ use self::{
question::{NewQuestion, QuestionDTO}, question::{NewQuestion, QuestionDTO},
}; };
use crate::*; use crate::*;
const DEFAULT_PAGE: usize = 0; // sqlx wants 'bigint' = i32
const DEFAULT_PAGE_SIZE: usize = 10; 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 /// 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 /// # Parameters
@ -43,23 +44,28 @@ pub async fn read_questions(
State(store): State<Arc<RwLock<Store>>>, State(store): State<Arc<RwLock<Store>>>,
Query(pagination): Query<Pagination>, Query(pagination): Query<Pagination>,
) -> Response { ) -> Response {
let page: usize = pagination.page.unwrap_or(DEFAULT_PAGE); let page: i32 = pagination.page.unwrap_or(DEFAULT_PAGE);
let size: usize = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE); let size: i32 = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE);
let start: usize = page * size; let start: i32 = page * size;
let questions_option = store.read().await.fetch_many_questions(start, size).await; let questions_option = store.read().await.fetch_many_questions(start, size).await;
match questions_option { match questions_option {
Some(questions) => { Some(questions) => {
println!("Num question {}", questions.len());
let mut response_vec_dto: Vec<QuestionDTO> = vec![]; let mut response_vec_dto: Vec<QuestionDTO> = vec![];
for question in questions { for question in questions {
//Not ideal - hitting the database serially for the tags of each invidual question. Can be optimized with more complex sql //Not ideal - hitting the database serially for the tags of each invidual question. Can be optimized with more complex sql
let tags = store let tags_option = store.read().await.get_tags_for_question(question.id).await;
.read() match tags_option {
.await Some(tags) => {
.fetch_tags_by_property(question.id, "question_id") println!("tags {:?}", tags);
.await let question_dto: QuestionDTO = QuestionDTO::new(question, tags);
.unwrap_or(vec![]); response_vec_dto.push(question_dto)
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() (StatusCode::OK, Json(response_vec_dto)).into_response()
} }
@ -179,6 +185,6 @@ pub async fn create_answer(
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Pagination { pub struct Pagination {
page: Option<usize>, page: Option<i32>,
size: Option<usize>, size: Option<i32>,
} }

View File

@ -29,7 +29,7 @@ impl Store {
.await .await
{ {
Ok(pool) => pool, Ok(pool) => pool,
Err(_) => panic!("Could not estable database connection"), Err(_) => panic!("Could not establish database connection"),
}; };
sqlx::migrate!().run(&db_pool).await.unwrap(); sqlx::migrate!().run(&db_pool).await.unwrap();
Store { Store {
@ -39,9 +39,12 @@ impl Store {
//Get the question tags for a particular question //Get the question tags for a particular question
pub async fn get_tags_for_question(&self, question_id: u8) -> Option<Vec<Tag>> { pub async fn get_tags_for_question(&self, question_id: u8) -> Option<Vec<Tag>> {
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) let result = sqlx::query(query)
.bind(question_id.to_string()) .bind(i32::from(question_id))
.fetch_all(&self.connection) .fetch_all(&self.connection)
.await; .await;
match result { match result {
@ -52,7 +55,10 @@ impl Store {
.collect::<Vec<Tag>>(); .collect::<Vec<Tag>>();
Some(tags) 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 // 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 // Returns list of tags
pub async fn add_tags(&mut self, tag_labels: Vec<String>) -> Result<Vec<Tag>, String> { pub async fn add_tags(&mut self, tag_labels: Vec<String>) -> Result<Vec<Tag>, String> {
let insert_values = tag_labels.join(","); let insert_query = "
let query = INSERT INTO tags (label)
"INSERT INTO tags (label) VALUES ($1) ON CONFLICT (label) DO NOTHING RETURNING *"; SELECT * FROM UNNEST(($1)::text[]) AS label
let result = sqlx::query(query) ON CONFLICT (label) DO NOTHING
.bind(insert_values) ";
.fetch_all(&self.connection)
//First run the insert query on the new labels
let insert_result = sqlx::query(insert_query)
.bind(&tag_labels)
.execute(&self.connection)
.await; .await;
match result { match insert_result {
Ok(pg_rows) => { Ok(_) => {
let tags: Vec<Tag> = pg_rows //Then run the select query on the new labels (which may include already existing labels)
.iter() let select_query = "
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label"))) SELECT id, label FROM tags
.collect(); WHERE label = ANY($1::text[]);
Ok(tags) ";
match sqlx::query(select_query)
.bind(&tag_labels)
.fetch_all(&self.connection)
.await
{
Ok(pg_rows) => {
let tags: Vec<Tag> = 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()), Err(e) => Err(e.to_string()),
} }
} }
//Takes a question id and list of tags, and creates question_tag join associations /// Takes a question id and list of tags, and creates question_tag join associations
//Ignores if association already exists /// Ignores if association already exists
//Returns list of the question tag associations /// Returns list of the question tag associations
pub async fn associate_tags( pub async fn associate_tags(
&mut self, &mut self,
question_id: u8, question_id: u8,
tags: &[Tag], tags: &[Tag],
) -> Result<Vec<QuestionTag>, String> { ) -> Result<Vec<QuestionTag>, String> {
//for the query binding we must convert the tag vector to a comma seperated string list of questionid/tagid tuples let tag_ids_string: Vec<String> = tags.iter().map(|tag| tag.id.to_string()).collect();
let question_id_tag_id_tuple_string = tags
.iter()
.map(|tag| format!("({},{})", question_id, tag.id))
.collect::<Vec<String>>()
.join(",");
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) 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) .fetch_all(&self.connection)
.await; .await;
match result { match result {
Ok(pg_rows) => { Ok(pg_rows) => {
println!("Num rows {}", pg_rows.len());
let question_tags: Vec<QuestionTag> = pg_rows let question_tags: Vec<QuestionTag> = pg_rows
.iter() .iter()
.map(|pg_row| { .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 // 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, &self,
propert_id: u8, propert_id: u8,
property_type: &str, property_type: &str,
) -> Option<Vec<Tag>> { ) -> Option<Vec<Tag>> {
let result = sqlx::query("SELECT * FROM tags WHERE $1 = $2") println!("Property type {}", property_type);
.bind(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()) .bind(propert_id.to_string())
.fetch_all(&self.connection) .fetch_all(&self.connection)
.await; .await;
match result { match result {
Ok(tag_rows) => { Ok(pg_rows) => {
let tags: Vec<Tag> = tag_rows println!("num tag rows {}", pg_rows.len());
let tags: Vec<Tag> = pg_rows
.iter() .iter()
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label"))) .map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
.collect(); .collect();
Some(tags) 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 // Fetch many questions - do not worry about joining the tags
pub async fn fetch_many_questions(&self, start: usize, size: usize) -> Option<Vec<Question>> { pub async fn fetch_many_questions(&self, start: i32, size: i32) -> Option<Vec<Question>> {
let rows_result: Result<Vec<PgRow>, sqlx::Error> = let rows_result: Result<Vec<PgRow>, sqlx::Error> =
sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2") sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2")
.bind(size.to_string()) .bind(size)
.bind(start.to_string()) .bind(start)
.fetch_all(&self.connection) .fetch_all(&self.connection)
.await; .await;
match rows_result { match rows_result {
Ok(pg_rows) => { Ok(pg_rows) => {
println!("num rows {}", pg_rows.len());
let mut result: Vec<Question> = vec![]; let mut result: Vec<Question> = vec![];
for pg_row in pg_rows { for pg_row in pg_rows {
result.push(Question::new( result.push(Question::new(
@ -311,7 +347,10 @@ impl Store {
} }
Some(result) 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<Answer, String> { pub async fn add_answer(&mut self, new_answer: NewAnswer) -> Result<Answer, String> {
let result = let result =
sqlx::query("INSERT INTO answers VALUES ($1,$2) RETURNING id, content, question_id") sqlx::query("INSERT INTO answers VALUES ($1,$2) RETURNING id, content, question_id")