-- Unified Question System Migration -- Replaces: practice_questions, assessment_questions, assessment_question_options, assessment_short_answers -- 1. Create unified questions table CREATE TABLE IF NOT EXISTS questions ( id BIGSERIAL PRIMARY KEY, question_text TEXT NOT NULL, question_type VARCHAR(20) NOT NULL CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER')), difficulty_level VARCHAR(20) CHECK (difficulty_level IN ('EASY', 'MEDIUM', 'HARD')), points INT NOT NULL DEFAULT 1, explanation TEXT, tips TEXT, voice_prompt TEXT, sample_answer_voice_prompt TEXT, status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT', 'PUBLISHED', 'INACTIVE', 'ARCHIVED')), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); CREATE INDEX idx_questions_type ON questions(question_type); CREATE INDEX idx_questions_status ON questions(status); CREATE INDEX idx_questions_difficulty ON questions(difficulty_level); -- 2. Create question options table (for MCQ and TRUE_FALSE) CREATE TABLE IF NOT EXISTS question_options ( id BIGSERIAL PRIMARY KEY, question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE, option_text TEXT NOT NULL, option_order INT NOT NULL DEFAULT 0, is_correct BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_question_options_question_id ON question_options(question_id); -- 3. Create question short answers table (for SHORT_ANSWER type) CREATE TABLE IF NOT EXISTS question_short_answers ( id BIGSERIAL PRIMARY KEY, question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE, acceptable_answer TEXT NOT NULL, match_type VARCHAR(20) NOT NULL DEFAULT 'EXACT' CHECK (match_type IN ('EXACT', 'CONTAINS', 'CASE_INSENSITIVE')), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_question_short_answers_question_id ON question_short_answers(question_id); -- 4. Create question sets table (replaces practices for grouping questions) CREATE TABLE IF NOT EXISTS question_sets ( id BIGSERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, set_type VARCHAR(30) NOT NULL CHECK (set_type IN ('PRACTICE', 'INITIAL_ASSESSMENT', 'QUIZ', 'EXAM', 'SURVEY')), owner_type VARCHAR(30), -- SUB_COURSE, COURSE, CATEGORY, STANDALONE owner_id BIGINT, -- References the owning entity banner_image TEXT, persona VARCHAR(100), time_limit_minutes INT, -- Optional time limit passing_score INT, -- Optional passing percentage shuffle_questions BOOLEAN NOT NULL DEFAULT FALSE, status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT', 'PUBLISHED', 'INACTIVE', 'ARCHIVED')), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); CREATE INDEX idx_question_sets_type ON question_sets(set_type); CREATE INDEX idx_question_sets_owner ON question_sets(owner_type, owner_id); CREATE INDEX idx_question_sets_status ON question_sets(status); -- 5. Create question set items table (links questions to sets) CREATE TABLE IF NOT EXISTS question_set_items ( id BIGSERIAL PRIMARY KEY, set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE, question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE, display_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(set_id, question_id) ); CREATE INDEX idx_question_set_items_set_id ON question_set_items(set_id); CREATE INDEX idx_question_set_items_question_id ON question_set_items(question_id); -- 6. Migrate data from assessment_questions to new questions table INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status, created_at) SELECT id, title, CASE question_type WHEN 'MULTIPLE_CHOICE' THEN 'MCQ' WHEN 'SHORT_ANSWER' THEN 'SHORT_ANSWER' WHEN 'TRUE_FALSE' THEN 'TRUE_FALSE' ELSE 'MCQ' END, CASE difficulty_level WHEN 'EASY' THEN 'EASY' WHEN 'MEDIUM' THEN 'MEDIUM' WHEN 'HARD' THEN 'HARD' ELSE 'MEDIUM' END, points, CASE WHEN is_active THEN 'PUBLISHED' ELSE 'INACTIVE' END, created_at FROM assessment_questions; -- 7. Migrate assessment_question_options to question_options INSERT INTO question_options (question_id, option_text, option_order, is_correct, created_at) SELECT question_id, option_text, option_order, is_correct, created_at FROM assessment_question_options; -- 8. Migrate assessment_short_answers to question_short_answers INSERT INTO question_short_answers (question_id, acceptable_answer, match_type, created_at) SELECT question_id, correct_answer, 'EXACT', created_at FROM assessment_short_answers; -- 9. Create initial assessment question set from existing data INSERT INTO question_sets (title, description, set_type, owner_type, status, created_at) VALUES ('Initial Assessment', 'Default initial assessment for new users', 'INITIAL_ASSESSMENT', 'STANDALONE', 'PUBLISHED', CURRENT_TIMESTAMP); -- Link existing assessment questions to the initial assessment set INSERT INTO question_set_items (set_id, question_id, display_order) SELECT (SELECT id FROM question_sets WHERE set_type = 'INITIAL_ASSESSMENT' LIMIT 1), id, id -- Use ID as initial display order FROM questions WHERE id IN (SELECT id FROM assessment_questions); -- 10. Migrate practice_questions to new structure -- First, get max ID from questions to avoid conflicts DO $$ DECLARE max_q_id BIGINT; practice_rec RECORD; new_set_id BIGINT; new_question_id BIGINT; BEGIN SELECT COALESCE(MAX(id), 0) INTO max_q_id FROM questions; -- For each practice in the old system, create a question_set FOR practice_rec IN SELECT DISTINCT p.id, p.sub_course_id, p.title, p.description, p.banner_image, p.persona, p.status FROM practices p LOOP -- Create question set for this practice INSERT INTO question_sets (title, description, set_type, owner_type, owner_id, banner_image, persona, status, created_at) VALUES ( practice_rec.title, practice_rec.description, 'PRACTICE', 'SUB_COURSE', practice_rec.sub_course_id, practice_rec.banner_image, practice_rec.persona, practice_rec.status, CURRENT_TIMESTAMP ) RETURNING id INTO new_set_id; -- Migrate questions from this practice FOR new_question_id IN INSERT INTO questions (question_text, question_type, tips, sample_answer_voice_prompt, voice_prompt, status, created_at) SELECT pq.question, pq.type, pq.tips, pq.sample_answer_voice_prompt, pq.question_voice_prompt, 'PUBLISHED', CURRENT_TIMESTAMP FROM practice_questions pq WHERE pq.practice_id = practice_rec.id RETURNING id LOOP -- Link question to set INSERT INTO question_set_items (set_id, question_id, display_order) VALUES (new_set_id, new_question_id, new_question_id); END LOOP; END LOOP; END $$; -- 11. Reset sequences SELECT setval(pg_get_serial_sequence('questions', 'id'), COALESCE((SELECT MAX(id) FROM questions), 1), true); SELECT setval(pg_get_serial_sequence('question_options', 'id'), COALESCE((SELECT MAX(id) FROM question_options), 1), true); SELECT setval(pg_get_serial_sequence('question_short_answers', 'id'), COALESCE((SELECT MAX(id) FROM question_short_answers), 1), true); SELECT setval(pg_get_serial_sequence('question_sets', 'id'), COALESCE((SELECT MAX(id) FROM question_sets), 1), true); SELECT setval(pg_get_serial_sequence('question_set_items', 'id'), COALESCE((SELECT MAX(id) FROM question_set_items), 1), true); -- 12. Drop old tables DROP TABLE IF EXISTS practice_questions CASCADE; DROP TABLE IF EXISTS practices CASCADE; DROP TABLE IF EXISTS assessment_attempt_answers CASCADE; DROP TABLE IF EXISTS assessment_attempt_questions CASCADE; DROP TABLE IF EXISTS assessment_attempts CASCADE; DROP TABLE IF EXISTS assessment_short_answers CASCADE; DROP TABLE IF EXISTS assessment_question_options CASCADE; DROP TABLE IF EXISTS assessment_questions CASCADE;