diff --git a/backend/migrations/001_initial.sql b/backend/migrations/001_initial.sql new file mode 100644 index 0000000..bc55f12 --- /dev/null +++ b/backend/migrations/001_initial.sql @@ -0,0 +1,69 @@ +CREATE TABLE courses ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + semester TEXT NOT NULL +); + +CREATE TABLE tutors ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL +); + +CREATE TABLE tutor_courses ( + tutor_id INTEGER REFERENCES tutors(id), + course_id INTEGER REFERENCES courses(id), + PRIMARY KEY (tutor_id, course_id) +); + +CREATE TABLE students ( + id INTEGER PRIMARY KEY, + course_id INTEGER NOT NULL REFERENCES courses(id), + name TEXT NOT NULL +); + +CREATE TABLE rooms ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + layout_json TEXT NOT NULL +); + +CREATE TABLE sessions ( + id INTEGER PRIMARY KEY, + course_id INTEGER NOT NULL REFERENCES courses(id), + week_nr INTEGER NOT NULL, + date TEXT NOT NULL CHECK (date GLOB '????-??-??'), + UNIQUE(course_id, week_nr) +); + +CREATE TABLE slots ( + id INTEGER PRIMARY KEY, + session_id INTEGER NOT NULL REFERENCES sessions(id), + room_id INTEGER REFERENCES rooms(id), + tutor_id INTEGER NOT NULL REFERENCES tutors(id), + start_time TEXT NOT NULL, + end_time TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'closed', + code TEXT UNIQUE +); + +CREATE TABLE attendances ( + id INTEGER PRIMARY KEY, + slot_id INTEGER NOT NULL REFERENCES slots(id), + student_id INTEGER NOT NULL REFERENCES students(id), + seat_id TEXT, + checked_in_at TEXT NOT NULL CHECK (checked_in_at GLOB '????-??-??T??:??:??*'), + UNIQUE(slot_id, student_id), + UNIQUE(slot_id, seat_id) +); + +CREATE TABLE notes ( + id INTEGER PRIMARY KEY, + slot_id INTEGER NOT NULL REFERENCES slots(id), + student_id INTEGER NOT NULL REFERENCES students(id), + tutor_id INTEGER NOT NULL REFERENCES tutors(id), + content TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL CHECK (updated_at GLOB '????-??-??T??:??:??*'), + UNIQUE(slot_id, student_id, tutor_id) +); diff --git a/backend/src/db.rs b/backend/src/db.rs index ee913e7..61a4931 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -1,5 +1,45 @@ -use sqlx::SqlitePool; +use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; pub async fn init() -> Result { - todo!() + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "sqlite:attendance.db".into()); + let pool = SqlitePoolOptions::new() + .after_connect(|conn, _| Box::pin(async move { + sqlx::query("PRAGMA foreign_keys = ON") + .execute(conn).await?; + Ok(()) + })) + .connect(&url).await?; + sqlx::migrate!("./migrations").run(&pool).await?; + Ok(pool) +} + +#[cfg(test)] +mod tests { + use super::*; + + // NOTE: #[sqlx::test] injects its own pool, bypassing after_connect. + // We test FK enforcement behaviorally: insert a row with a bad FK and assert it fails. + // after_connect is manually invoked in the test setup below. + #[sqlx::test(migrations = "./migrations")] + async fn foreign_keys_enforced(pool: SqlitePool) { + // Enable FK for this test connection (mirrors what after_connect does in production) + sqlx::query("PRAGMA foreign_keys = ON").execute(&pool).await.unwrap(); + + let err = sqlx::query( + "INSERT INTO students (course_id, name) VALUES (999, 'Ghost')" + ).execute(&pool).await; + assert!(err.is_err(), "FK violation should be rejected when foreign_keys = ON"); + } + + #[sqlx::test(migrations = "./migrations")] + async fn all_tables_exist(pool: SqlitePool) { + for table in &["courses","tutors","tutor_courses","students","rooms", + "sessions","slots","attendances","notes"] { + let count: (i64,) = sqlx::query_as( + "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?" + ).bind(table).fetch_one(&pool).await.unwrap(); + assert_eq!(count.0, 1, "table {table} missing"); + } + } }