feat(question-bank): refactor SQL queries to use a constant for selected columns

This commit is contained in:
2026-04-08 09:37:02 -04:00
parent 82ac2f09fc
commit 95d5dc9e3e
@@ -12,6 +12,41 @@ use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
const QUESTION_BANK_SELECT_COLUMNS: &str = r#"
id,
organization_id,
question_text,
question_type,
options,
correct_answer,
explanation,
audio_url,
audio_text,
audio_status,
audio_metadata,
media_url,
media_type,
points,
difficulty,
tags,
skill_assessed,
source,
source_metadata,
imported_mysql_id,
imported_mysql_course_id,
usage_count,
last_used_at,
is_active,
is_archived,
created_by,
created_at,
updated_at,
embedding::text AS embedding,
embedding_updated_at,
source_asset_id,
unit_number
"#;
async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCode, String)> { async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCode, String)> {
use sqlx::mysql::MySqlPoolOptions; use sqlx::mysql::MySqlPoolOptions;
@@ -253,7 +288,7 @@ pub async fn create_question(
State(pool): State<PgPool>, State(pool): State<PgPool>,
Json(payload): Json<CreateQuestionBankPayload>, Json(payload): Json<CreateQuestionBankPayload>,
) -> Result<Json<QuestionBank>, (StatusCode, String)> { ) -> Result<Json<QuestionBank>, (StatusCode, String)> {
let question: QuestionBank = sqlx::query_as( let create_question_sql = format!(
r#" r#"
INSERT INTO question_bank ( INSERT INTO question_bank (
organization_id, created_by, question_text, question_type, organization_id, created_by, question_text, question_type,
@@ -261,8 +296,13 @@ pub async fn create_question(
tags, skill_assessed, media_url, media_type, audio_status tags, skill_assessed, media_url, media_type, audio_status
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'pending') VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'pending')
RETURNING * RETURNING {}
"# "#,
QUESTION_BANK_SELECT_COLUMNS
);
let question: QuestionBank = sqlx::query_as(
&create_question_sql
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(claims.sub) .bind(claims.sub)
@@ -275,6 +315,7 @@ pub async fn create_question(
.bind(payload.difficulty.as_deref().unwrap_or("medium")) .bind(payload.difficulty.as_deref().unwrap_or("medium"))
.bind(payload.tags.as_deref()) .bind(payload.tags.as_deref())
.bind(payload.skill_assessed.as_deref()) .bind(payload.skill_assessed.as_deref())
.bind(payload.media_url.as_deref())
.bind(payload.media_type.as_deref()) .bind(payload.media_type.as_deref())
.fetch_one(&pool) .fetch_one(&pool)
.await .await
@@ -299,14 +340,20 @@ pub async fn list_questions(
{ {
// No filters - simple query // No filters - simple query
sqlx::query_as::<_, QuestionBank>( sqlx::query_as::<_, QuestionBank>(
"SELECT * FROM question_bank WHERE organization_id = $1 AND is_archived = false ORDER BY created_at DESC" &format!(
"SELECT {} FROM question_bank WHERE organization_id = $1 AND is_archived = false ORDER BY created_at DESC",
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.fetch_all(&pool) .fetch_all(&pool)
.await .await
} else if filters.question_type.is_some() { } else if filters.question_type.is_some() {
sqlx::query_as::<_, QuestionBank>( sqlx::query_as::<_, QuestionBank>(
"SELECT * FROM question_bank WHERE organization_id = $1 AND is_archived = false AND question_type = $2 ORDER BY created_at DESC" &format!(
"SELECT {} FROM question_bank WHERE organization_id = $1 AND is_archived = false AND question_type = $2 ORDER BY created_at DESC",
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(filters.question_type.unwrap()) .bind(filters.question_type.unwrap())
@@ -314,7 +361,10 @@ pub async fn list_questions(
.await .await
} else if filters.difficulty.is_some() { } else if filters.difficulty.is_some() {
sqlx::query_as::<_, QuestionBank>( sqlx::query_as::<_, QuestionBank>(
"SELECT * FROM question_bank WHERE organization_id = $1 AND is_archived = false AND difficulty = $2 ORDER BY created_at DESC" &format!(
"SELECT {} FROM question_bank WHERE organization_id = $1 AND is_archived = false AND difficulty = $2 ORDER BY created_at DESC",
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(filters.difficulty.as_ref().unwrap()) .bind(filters.difficulty.as_ref().unwrap())
@@ -322,7 +372,10 @@ pub async fn list_questions(
.await .await
} else if filters.source.is_some() { } else if filters.source.is_some() {
sqlx::query_as::<_, QuestionBank>( sqlx::query_as::<_, QuestionBank>(
"SELECT * FROM question_bank WHERE organization_id = $1 AND is_archived = false AND source = $2 ORDER BY created_at DESC" &format!(
"SELECT {} FROM question_bank WHERE organization_id = $1 AND is_archived = false AND source = $2 ORDER BY created_at DESC",
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(filters.source.as_ref().unwrap()) .bind(filters.source.as_ref().unwrap())
@@ -330,7 +383,10 @@ pub async fn list_questions(
.await .await
} else if filters.has_audio == Some(true) { } else if filters.has_audio == Some(true) {
sqlx::query_as::<_, QuestionBank>( sqlx::query_as::<_, QuestionBank>(
"SELECT * FROM question_bank WHERE organization_id = $1 AND is_archived = false AND audio_status = 'ready' ORDER BY created_at DESC" &format!(
"SELECT {} FROM question_bank WHERE organization_id = $1 AND is_archived = false AND audio_status = 'ready' ORDER BY created_at DESC",
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.fetch_all(&pool) .fetch_all(&pool)
@@ -338,7 +394,10 @@ pub async fn list_questions(
} else { } else {
// Default fallback // Default fallback
sqlx::query_as::<_, QuestionBank>( sqlx::query_as::<_, QuestionBank>(
"SELECT * FROM question_bank WHERE organization_id = $1 AND is_archived = false ORDER BY created_at DESC" &format!(
"SELECT {} FROM question_bank WHERE organization_id = $1 AND is_archived = false ORDER BY created_at DESC",
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.fetch_all(&pool) .fetch_all(&pool)
@@ -357,11 +416,16 @@ pub async fn get_question(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
State(pool): State<PgPool>, State(pool): State<PgPool>,
) -> Result<Json<QuestionBank>, (StatusCode, String)> { ) -> Result<Json<QuestionBank>, (StatusCode, String)> {
let question: QuestionBank = sqlx::query_as( let get_question_sql = format!(
r#" r#"
SELECT * FROM question_bank SELECT {} FROM question_bank
WHERE id = $1 AND organization_id = $2 AND is_archived = false WHERE id = $1 AND organization_id = $2 AND is_archived = false
"# "#,
QUESTION_BANK_SELECT_COLUMNS
);
let question: QuestionBank = sqlx::query_as(
&get_question_sql
) )
.bind(id) .bind(id)
.bind(org_ctx.id) .bind(org_ctx.id)
@@ -384,7 +448,7 @@ pub async fn update_question(
State(pool): State<PgPool>, State(pool): State<PgPool>,
Json(payload): Json<UpdateQuestionBankPayload>, Json(payload): Json<UpdateQuestionBankPayload>,
) -> Result<Json<QuestionBank>, (StatusCode, String)> { ) -> Result<Json<QuestionBank>, (StatusCode, String)> {
let question: QuestionBank = sqlx::query_as( let update_question_sql = format!(
r#" r#"
UPDATE question_bank UPDATE question_bank
SET SET
@@ -400,8 +464,13 @@ pub async fn update_question(
is_archived = COALESCE($12, is_archived), is_archived = COALESCE($12, is_archived),
updated_at = NOW() updated_at = NOW()
WHERE id = $1 AND organization_id = $2 WHERE id = $1 AND organization_id = $2
RETURNING * RETURNING {}
"# "#,
QUESTION_BANK_SELECT_COLUMNS
);
let question: QuestionBank = sqlx::query_as(
&update_question_sql
) )
.bind(id) .bind(id)
.bind(org_ctx.id) .bind(org_ctx.id)
@@ -601,6 +670,7 @@ pub async fn import_from_mysql(
}); });
let qb: QuestionBank = sqlx::query_as( let qb: QuestionBank = sqlx::query_as(
&format!(
r#" r#"
INSERT INTO question_bank ( INSERT INTO question_bank (
organization_id, created_by, question_text, question_type, organization_id, created_by, question_text, question_type,
@@ -608,8 +678,10 @@ pub async fn import_from_mysql(
audio_status, is_active audio_status, is_active
) )
VALUES ($1, $2, $3, $4, $5, $6, 'imported-mysql', $7, 'pending', true) VALUES ($1, $2, $3, $4, $5, $6, 'imported-mysql', $7, 'pending', true)
RETURNING * RETURNING {}
"# "#,
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(claims.sub) .bind(claims.sub)
@@ -682,6 +754,7 @@ pub async fn import_from_mysql(
}); });
let question: QuestionBank = sqlx::query_as( let question: QuestionBank = sqlx::query_as(
&format!(
r#" r#"
INSERT INTO question_bank ( INSERT INTO question_bank (
organization_id, created_by, question_text, question_type, organization_id, created_by, question_text, question_type,
@@ -690,8 +763,10 @@ pub async fn import_from_mysql(
audio_status, is_active audio_status, is_active
) )
VALUES ($1, $2, $3, $4, $5, $6, 'imported-mysql', $7, $8, $9, 'pending', true) VALUES ($1, $2, $3, $4, $5, $6, 'imported-mysql', $7, $8, $9, 'pending', true)
RETURNING * RETURNING {}
"# "#,
QUESTION_BANK_SELECT_COLUMNS
)
) )
.bind(org_ctx.id) .bind(org_ctx.id)
.bind(claims.sub) .bind(claims.sub)