Files
banner/tests/db_batch_upsert.rs
Xevion 5d7d60cd96 fix: prevent session pool deadlock on acquire cancellation
Replace is_creating mutex with atomic flag and RAII guard to ensure
proper cleanup when acquire() futures are cancelled mid-creation,
preventing permanent deadlock for subsequent callers.
2026-01-30 20:19:10 -06:00

327 lines
8.8 KiB
Rust

#[allow(dead_code)]
mod helpers;
use banner::data::batch::batch_upsert_courses;
use sqlx::PgPool;
#[sqlx::test]
async fn test_batch_upsert_empty_slice(pool: PgPool) {
batch_upsert_courses(&[], &pool).await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count.0, 0);
}
#[sqlx::test]
async fn test_batch_upsert_inserts_new_courses(pool: PgPool) {
let courses = vec![
helpers::make_course("10001", "202510", "CS", "1083", "Intro to CS", 25, 30, 0, 5),
helpers::make_course(
"10002",
"202510",
"MAT",
"1214",
"Calculus I",
40,
45,
3,
10,
),
];
batch_upsert_courses(&courses, &pool).await.unwrap();
let rows: Vec<(String, String, String, String, i32, i32, i32, i32)> = sqlx::query_as(
"SELECT crn, subject, course_number, title, enrollment, max_enrollment, wait_count, wait_capacity
FROM courses ORDER BY crn",
)
.fetch_all(&pool)
.await
.unwrap();
assert_eq!(rows.len(), 2);
let (crn, subject, course_number, title, enrollment, max_enrollment, wait_count, wait_capacity) =
&rows[0];
assert_eq!(crn, "10001");
assert_eq!(subject, "CS");
assert_eq!(course_number, "1083");
assert_eq!(title, "Intro to CS");
assert_eq!(*enrollment, 25);
assert_eq!(*max_enrollment, 30);
assert_eq!(*wait_count, 0);
assert_eq!(*wait_capacity, 5);
let (crn, subject, ..) = &rows[1];
assert_eq!(crn, "10002");
assert_eq!(subject, "MAT");
}
#[sqlx::test]
async fn test_batch_upsert_updates_existing(pool: PgPool) {
let initial = vec![helpers::make_course(
"20001",
"202510",
"CS",
"3443",
"App Programming",
10,
35,
0,
5,
)];
batch_upsert_courses(&initial, &pool).await.unwrap();
// Upsert the same CRN+term with updated enrollment
let updated = vec![helpers::make_course(
"20001",
"202510",
"CS",
"3443",
"App Programming",
30,
35,
2,
5,
)];
batch_upsert_courses(&updated, &pool).await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count.0, 1, "upsert should not create a duplicate row");
let (enrollment, wait_count): (i32, i32) =
sqlx::query_as("SELECT enrollment, wait_count FROM courses WHERE crn = '20001'")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(enrollment, 30);
assert_eq!(wait_count, 2);
}
#[sqlx::test]
async fn test_batch_upsert_mixed_insert_and_update(pool: PgPool) {
let initial = vec![
helpers::make_course("30001", "202510", "CS", "1083", "Intro to CS", 10, 30, 0, 5),
helpers::make_course(
"30002",
"202510",
"CS",
"2073",
"Computer Architecture",
20,
30,
0,
5,
),
];
batch_upsert_courses(&initial, &pool).await.unwrap();
// Update both existing courses and add a new one
let mixed = vec![
helpers::make_course("30001", "202510", "CS", "1083", "Intro to CS", 15, 30, 1, 5),
helpers::make_course(
"30002",
"202510",
"CS",
"2073",
"Computer Architecture",
25,
30,
0,
5,
),
helpers::make_course(
"30003",
"202510",
"MAT",
"1214",
"Calculus I",
40,
45,
3,
10,
),
];
batch_upsert_courses(&mixed, &pool).await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count.0, 3, "should have 2 updated + 1 new = 3 total rows");
// Verify updated values
let (enrollment,): (i32,) =
sqlx::query_as("SELECT enrollment FROM courses WHERE crn = '30001'")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(enrollment, 15);
let (enrollment,): (i32,) =
sqlx::query_as("SELECT enrollment FROM courses WHERE crn = '30002'")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(enrollment, 25);
// Verify new row
let (subject,): (String,) = sqlx::query_as("SELECT subject FROM courses WHERE crn = '30003'")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(subject, "MAT");
}
#[sqlx::test]
async fn test_batch_upsert_unique_constraint_crn_term(pool: PgPool) {
// Same CRN, different term codes → should produce two separate rows
let courses = vec![
helpers::make_course("40001", "202510", "CS", "1083", "Intro to CS", 25, 30, 0, 5),
helpers::make_course("40001", "202520", "CS", "1083", "Intro to CS", 10, 30, 0, 5),
];
batch_upsert_courses(&courses, &pool).await.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses WHERE crn = '40001'")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
count.0, 2,
"same CRN with different term codes should be separate rows"
);
let rows: Vec<(String, i32)> = sqlx::query_as(
"SELECT term_code, enrollment FROM courses WHERE crn = '40001' ORDER BY term_code",
)
.fetch_all(&pool)
.await
.unwrap();
assert_eq!(rows[0].0, "202510");
assert_eq!(rows[0].1, 25);
assert_eq!(rows[1].0, "202520");
assert_eq!(rows[1].1, 10);
}
#[sqlx::test]
async fn test_batch_upsert_creates_audit_and_metric_entries(pool: PgPool) {
// Insert initial data — should NOT create audits/metrics (it's a fresh insert)
let initial = vec![helpers::make_course(
"50001",
"202510",
"CS",
"3443",
"App Programming",
10,
35,
0,
5,
)];
batch_upsert_courses(&initial, &pool).await.unwrap();
let (audit_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_audits")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
audit_count, 0,
"initial insert should not create audit entries"
);
let (metric_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_metrics")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
metric_count, 0,
"initial insert should not create metric entries"
);
// Update enrollment and wait_count
let updated = vec![helpers::make_course(
"50001",
"202510",
"CS",
"3443",
"App Programming",
20,
35,
2,
5,
)];
batch_upsert_courses(&updated, &pool).await.unwrap();
// Should have audit entries for enrollment and wait_count changes
let (audit_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_audits")
.fetch_one(&pool)
.await
.unwrap();
assert!(
audit_count >= 2,
"should have audit entries for enrollment and wait_count changes, got {audit_count}"
);
// Should have exactly 1 metric entry
let (metric_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_metrics")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(metric_count, 1, "should have 1 metric snapshot");
// Verify metric values
let (enrollment, wait_count, seats): (i32, i32, i32) = sqlx::query_as(
"SELECT enrollment, wait_count, seats_available FROM course_metrics LIMIT 1",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(enrollment, 20);
assert_eq!(wait_count, 2);
assert_eq!(seats, 15); // 35 - 20
}
#[sqlx::test]
async fn test_batch_upsert_no_change_no_audit(pool: PgPool) {
// Insert then re-insert identical data — should produce zero audits/metrics
let course = vec![helpers::make_course(
"60001",
"202510",
"CS",
"1083",
"Intro to CS",
25,
30,
0,
5,
)];
batch_upsert_courses(&course, &pool).await.unwrap();
batch_upsert_courses(&course, &pool).await.unwrap();
let (audit_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_audits")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
audit_count, 0,
"identical re-upsert should not create audit entries"
);
let (metric_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM course_metrics")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
metric_count, 0,
"identical re-upsert should not create metric entries"
);
}