diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..83e0517 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/data/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "migrations" \ No newline at end of file diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2025-08-27-231618_setup/down.sql b/migrations/2025-08-27-231618_setup/down.sql new file mode 100644 index 0000000..d781e9d --- /dev/null +++ b/migrations/2025-08-27-231618_setup/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS "courses"; +DROP TABLE IF EXISTS "course_metrics"; +DROP TABLE IF EXISTS "course_audits"; diff --git a/migrations/2025-08-27-231618_setup/up.sql b/migrations/2025-08-27-231618_setup/up.sql new file mode 100644 index 0000000..9282a4a --- /dev/null +++ b/migrations/2025-08-27-231618_setup/up.sql @@ -0,0 +1,35 @@ +-- Your SQL goes here +CREATE TABLE "courses"( + "id" INT4 NOT NULL PRIMARY KEY, + "crn" VARCHAR NOT NULL, + "subject" VARCHAR NOT NULL, + "course_number" VARCHAR NOT NULL, + "title" VARCHAR NOT NULL, + "term_code" VARCHAR NOT NULL, + "enrollment" INT4 NOT NULL, + "max_enrollment" INT4 NOT NULL, + "wait_count" INT4 NOT NULL, + "wait_capacity" INT4 NOT NULL, + "last_scraped_at" TIMESTAMPTZ NOT NULL +); + +CREATE TABLE "course_metrics"( + "id" INT4 NOT NULL PRIMARY KEY, + "course_id" INT4 NOT NULL, + "timestamp" TIMESTAMPTZ NOT NULL, + "enrollment" INT4 NOT NULL, + "wait_count" INT4 NOT NULL, + "seats_available" INT4 NOT NULL, + FOREIGN KEY ("course_id") REFERENCES "courses"("id") +); + +CREATE TABLE "course_audits"( + "id" INT4 NOT NULL PRIMARY KEY, + "course_id" INT4 NOT NULL, + "timestamp" TIMESTAMPTZ NOT NULL, + "field_changed" VARCHAR NOT NULL, + "old_value" TEXT NOT NULL, + "new_value" TEXT NOT NULL, + FOREIGN KEY ("course_id") REFERENCES "courses"("id") +); + diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..b2f4caa --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,4 @@ +//! Database models and schema. + +pub mod models; +pub mod schema; diff --git a/src/data/models.rs b/src/data/models.rs new file mode 100644 index 0000000..0fbd31f --- /dev/null +++ b/src/data/models.rs @@ -0,0 +1,80 @@ +//! Diesel models for the database schema. + +use crate::data::schema::{course_audits, course_metrics, courses}; +use chrono::{DateTime, Utc}; +use diesel::{Insertable, Queryable, Selectable}; + +#[derive(Queryable, Selectable)] +#[diesel(table_name = courses)] +pub struct Course { + pub id: i32, + pub crn: String, + pub subject: String, + pub course_number: String, + pub title: String, + pub term_code: String, + pub enrollment: i32, + pub max_enrollment: i32, + pub wait_count: i32, + pub wait_capacity: i32, + pub last_scraped_at: DateTime, +} + +#[derive(Insertable)] +#[diesel(table_name = courses)] +pub struct NewCourse<'a> { + pub crn: &'a str, + pub subject: &'a str, + pub course_number: &'a str, + pub title: &'a str, + pub term_code: &'a str, + pub enrollment: i32, + pub max_enrollment: i32, + pub wait_count: i32, + pub wait_capacity: i32, + pub last_scraped_at: DateTime, +} + +#[derive(Queryable, Selectable)] +#[diesel(table_name = course_metrics)] +#[diesel(belongs_to(Course))] +pub struct CourseMetric { + pub id: i32, + pub course_id: i32, + pub timestamp: DateTime, + pub enrollment: i32, + pub wait_count: i32, + pub seats_available: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = course_metrics)] +pub struct NewCourseMetric { + pub course_id: i32, + pub timestamp: DateTime, + pub enrollment: i32, + pub wait_count: i32, + pub seats_available: i32, +} + +#[derive(Queryable, Selectable)] +#[diesel(table_name = course_audits)] +#[diesel(belongs_to(Course))] +pub struct CourseAudit { + pub id: i32, + pub course_id: i32, + pub timestamp: DateTime, + pub field_changed: String, + pub old_value: String, + pub new_value: String, +} + +#[derive(Insertable)] +#[diesel(table_name = course_audits)] +pub struct NewCourseAudit<'a> { + pub course_id: i32, + pub timestamp: DateTime, + pub field_changed: &'a str, + pub old_value: &'a str, + pub new_value: &'a str, +} diff --git a/src/data/schema.rs b/src/data/schema.rs new file mode 100644 index 0000000..0575af9 --- /dev/null +++ b/src/data/schema.rs @@ -0,0 +1,42 @@ +diesel::table! { + courses (id) { + id -> Int4, + crn -> Varchar, + subject -> Varchar, + course_number -> Varchar, + title -> Varchar, + term_code -> Varchar, + enrollment -> Int4, + max_enrollment -> Int4, + wait_count -> Int4, + wait_capacity -> Int4, + last_scraped_at -> Timestamptz, + } +} + +diesel::table! { + course_metrics (id) { + id -> Int4, + course_id -> Int4, + timestamp -> Timestamptz, + enrollment -> Int4, + wait_count -> Int4, + seats_available -> Int4, + } +} + +diesel::table! { + course_audits (id) { + id -> Int4, + course_id -> Int4, + timestamp -> Timestamptz, + field_changed -> Varchar, + old_value -> Text, + new_value -> Text, + } +} + +diesel::joinable!(course_metrics -> courses (course_id)); +diesel::joinable!(course_audits -> courses (course_id)); + +diesel::allow_tables_to_appear_in_same_query!(courses, course_metrics, course_audits,); diff --git a/src/lib.rs b/src/lib.rs index 03a21aa..ad75f5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod app_state; pub mod banner; pub mod bot; +pub mod data; pub mod error; pub mod services; pub mod web; diff --git a/src/main.rs b/src/main.rs index de29a02..e5b0426 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ mod app_state; mod banner; mod bot; mod config; +mod data; mod error; mod services; mod web;