-- +goose Up CREATE TYPE user_role AS ENUM ('student', 'teacher'); CREATE TYPE question_status AS ENUM ('draft', 'published', 'archived'); CREATE TYPE assignment_status AS ENUM ('draft', 'assigned', 'closed'); CREATE TYPE answer_status AS ENUM ('not_started', 'in_progress', 'submitted', 'reviewed'); CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, password_hash TEXT, role user_role NOT NULL, full_name VARCHAR(255) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE classrooms ( id BIGSERIAL PRIMARY KEY, teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, code VARCHAR(50) UNIQUE, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE classroom_students ( classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE, student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (classroom_id, student_id) ); CREATE TABLE questions ( id BIGSERIAL PRIMARY KEY, author_teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, title VARCHAR(255) NOT NULL, prompt TEXT NOT NULL, subject VARCHAR(100), source TEXT, status question_status NOT NULL DEFAULT 'draft', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE tags ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE question_tags ( question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE, tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (question_id, tag_id) ); CREATE TABLE assignments ( id BIGSERIAL PRIMARY KEY, classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE, teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, title VARCHAR(255) NOT NULL, instructions TEXT, status assignment_status NOT NULL DEFAULT 'draft', due_at TIMESTAMPTZ, published_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE assignment_assignees ( assignment_id BIGINT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE, student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (assignment_id, student_id) ); CREATE TABLE assignment_questions ( assignment_id BIGINT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE, question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE RESTRICT, position INTEGER NOT NULL, PRIMARY KEY (assignment_id, question_id), CONSTRAINT assignment_questions_assignment_position_key UNIQUE (assignment_id, position) ); CREATE TABLE student_answers ( id BIGSERIAL PRIMARY KEY, assignment_id BIGINT NOT NULL REFERENCES assignments(id) ON DELETE CASCADE, question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE RESTRICT, student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, answer_text TEXT, ai_feedback TEXT, teacher_feedback TEXT, status answer_status NOT NULL DEFAULT 'not_started', submitted_at TIMESTAMPTZ, reviewed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT student_answers_assignment_question_student_key UNIQUE (assignment_id, question_id, student_id) ); CREATE INDEX idx_users_role ON users(role); CREATE INDEX idx_classrooms_teacher_id ON classrooms(teacher_id); CREATE INDEX idx_questions_author_teacher_id ON questions(author_teacher_id); CREATE INDEX idx_questions_status ON questions(status); CREATE INDEX idx_assignments_classroom_id ON assignments(classroom_id); CREATE INDEX idx_assignments_teacher_id ON assignments(teacher_id); CREATE INDEX idx_assignment_assignees_student_id ON assignment_assignees(student_id); CREATE INDEX idx_student_answers_student_id ON student_answers(student_id); CREATE INDEX idx_student_answers_assignment_id ON student_answers(assignment_id); -- +goose StatementBegin CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- +goose StatementEnd CREATE TRIGGER users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER classrooms_updated_at BEFORE UPDATE ON classrooms FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER questions_updated_at BEFORE UPDATE ON questions FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER assignments_updated_at BEFORE UPDATE ON assignments FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER student_answers_updated_at BEFORE UPDATE ON student_answers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- +goose Down DROP TRIGGER IF EXISTS student_answers_updated_at ON student_answers; DROP TRIGGER IF EXISTS assignments_updated_at ON assignments; DROP TRIGGER IF EXISTS questions_updated_at ON questions; DROP TRIGGER IF EXISTS classrooms_updated_at ON classrooms; DROP TRIGGER IF EXISTS users_updated_at ON users; DROP FUNCTION IF EXISTS update_updated_at(); DROP TABLE IF EXISTS student_answers; DROP TABLE IF EXISTS assignment_questions; DROP TABLE IF EXISTS assignment_assignees; DROP TABLE IF EXISTS assignments; DROP TABLE IF EXISTS question_tags; DROP TABLE IF EXISTS tags; DROP TABLE IF EXISTS questions; DROP TABLE IF EXISTS classroom_students; DROP TABLE IF EXISTS classrooms; DROP TABLE IF EXISTS users; DROP TYPE IF EXISTS answer_status; DROP TYPE IF EXISTS assignment_status; DROP TYPE IF EXISTS question_status; DROP TYPE IF EXISTS user_role;