-- Migration: Simplify course hierarchy -- OLD: Course Category → Course → Program → Level → Module → (Video, Practice) -- NEW: Course Category → Course → Sub-course (with level) → (Video, Practice) -- Step 1: Create new tables CREATE TABLE IF NOT EXISTS sub_courses ( id BIGSERIAL PRIMARY KEY, course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, description TEXT, thumbnail TEXT, display_order INT NOT NULL DEFAULT 0, level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED is_active BOOLEAN NOT NULL DEFAULT TRUE, CHECK (level IN ('BEGINNER', 'INTERMEDIATE', 'ADVANCED')) ); CREATE INDEX IF NOT EXISTS idx_sub_courses_course_id ON sub_courses(course_id); CREATE TABLE IF NOT EXISTS sub_course_videos ( id BIGSERIAL PRIMARY KEY, sub_course_id BIGINT NOT NULL REFERENCES sub_courses(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, description TEXT, video_url TEXT NOT NULL, duration INT NOT NULL, -- seconds resolution VARCHAR(20), -- "720p", "1080p" is_published BOOLEAN NOT NULL DEFAULT FALSE, publish_date TIMESTAMPTZ, visibility VARCHAR(50), -- public, private, unlisted instructor_id VARCHAR(100), thumbnail TEXT, display_order INT NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE ); CREATE INDEX IF NOT EXISTS idx_sub_course_videos_sub_course_id ON sub_course_videos(sub_course_id); -- Step 2: Add sub_course_id to practices (nullable during migration) ALTER TABLE practices ADD COLUMN IF NOT EXISTS sub_course_id BIGINT NULL; CREATE INDEX IF NOT EXISTS idx_practices_sub_course_id ON practices(sub_course_id); -- Step 3: Migrate data from old structure to new structure -- Insert sub-courses from (program, level) combinations INSERT INTO sub_courses (course_id, title, description, thumbnail, display_order, level, is_active) SELECT p.course_id, (p.title || ' - ' || l.title) as title, COALESCE(l.description, p.description) as description, p.thumbnail, (p.display_order * 100 + l.level_index) as display_order, CASE l.level_index WHEN 1 THEN 'BEGINNER' WHEN 2 THEN 'INTERMEDIATE' WHEN 3 THEN 'ADVANCED' ELSE 'BEGINNER' END as level, (l.is_active AND p.is_active) as is_active FROM levels l JOIN programs p ON p.id = l.program_id; -- Create temporary mapping table for migration CREATE TEMP TABLE level_to_sub_course AS SELECT l.id as level_id, sc.id as sub_course_id FROM levels l JOIN programs p ON p.id = l.program_id JOIN sub_courses sc ON sc.course_id = p.course_id AND sc.title = (p.title || ' - ' || l.title); -- Create temporary mapping for modules to sub-courses CREATE TEMP TABLE module_to_sub_course AS SELECT m.id as module_id, lsc.sub_course_id FROM modules m JOIN level_to_sub_course lsc ON lsc.level_id = m.level_id; -- Migrate videos from module_videos to sub_course_videos INSERT INTO sub_course_videos ( sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, is_active ) SELECT msc.sub_course_id, mv.title, mv.description, mv.video_url, mv.duration, mv.resolution, mv.is_published, mv.publish_date, mv.visibility, mv.instructor_id, mv.thumbnail, (m.display_order * 100 + mv.id) as display_order, mv.is_active FROM module_videos mv JOIN modules m ON m.id = mv.module_id JOIN module_to_sub_course msc ON msc.module_id = m.id; -- Migrate practices owned by LEVEL UPDATE practices pr SET sub_course_id = lsc.sub_course_id FROM level_to_sub_course lsc WHERE pr.owner_type = 'LEVEL' AND pr.owner_id = lsc.level_id; -- Migrate practices owned by MODULE UPDATE practices pr SET sub_course_id = msc.sub_course_id FROM module_to_sub_course msc WHERE pr.owner_type = 'MODULE' AND pr.owner_id = msc.module_id; -- Step 4: Enforce integrity on practices ALTER TABLE practices ADD CONSTRAINT practices_sub_course_id_fkey FOREIGN KEY (sub_course_id) REFERENCES sub_courses(id) ON DELETE CASCADE; -- Make sub_course_id NOT NULL (only if there's data to migrate) -- If practices table has rows without sub_course_id after migration, this will fail -- ALTER TABLE practices ALTER COLUMN sub_course_id SET NOT NULL; -- Step 5: Drop old columns from practices ALTER TABLE practices DROP CONSTRAINT IF EXISTS practices_owner_type_check; ALTER TABLE practices DROP COLUMN IF EXISTS owner_type; ALTER TABLE practices DROP COLUMN IF EXISTS owner_id; -- Step 6: Drop old indexes DROP INDEX IF EXISTS idx_videos_module_id; DROP INDEX IF EXISTS idx_modules_level_id; DROP INDEX IF EXISTS idx_levels_program_id; DROP INDEX IF EXISTS idx_programs_course_id; DROP INDEX IF EXISTS idx_practices_owner; -- Step 7: Drop old tables (CASCADE to remove FK references) DROP TABLE IF EXISTS module_videos CASCADE; DROP TABLE IF EXISTS modules CASCADE; DROP TABLE IF EXISTS levels CASCADE; DROP TABLE IF EXISTS programs CASCADE;