Yimaru-BackEnd/db/migrations/000030_unified_hierarchy.up.sql

229 lines
7.0 KiB
SQL

-- Unified hierarchy
-- Course Category -> Course Sub-category -> Course -> Level -> Module -> Sub-Module
-- -> Sub-Module Videos
-- -> Sub-Module Practices (question sets)
CREATE TABLE IF NOT EXISTS course_sub_categories (
id BIGSERIAL PRIMARY KEY,
category_id BIGINT NOT NULL REFERENCES course_categories(id) ON DELETE CASCADE,
name VARCHAR(150) NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
display_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(category_id, name)
);
ALTER TABLE courses
ADD COLUMN IF NOT EXISTS sub_category_id BIGINT REFERENCES course_sub_categories(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_courses_sub_category_id ON courses(sub_category_id);
CREATE TABLE IF NOT EXISTS levels (
id BIGSERIAL PRIMARY KEY,
course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
cefr_level VARCHAR(2) NOT NULL,
display_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(course_id, cefr_level),
CHECK (cefr_level IN ('A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'))
);
CREATE INDEX IF NOT EXISTS idx_levels_course_id ON levels(course_id);
CREATE TABLE IF NOT EXISTS modules (
id BIGSERIAL PRIMARY KEY,
level_id BIGINT NOT NULL REFERENCES levels(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT,
display_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_modules_level_id ON modules(level_id);
CREATE TABLE IF NOT EXISTS sub_modules (
id BIGSERIAL PRIMARY KEY,
module_id BIGINT NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT,
display_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
legacy_sub_course_id BIGINT UNIQUE
);
CREATE INDEX IF NOT EXISTS idx_sub_modules_module_id ON sub_modules(module_id);
CREATE TABLE IF NOT EXISTS sub_module_videos (
id BIGSERIAL PRIMARY KEY,
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT,
video_url TEXT NOT NULL,
duration INT,
resolution VARCHAR(20),
is_published BOOLEAN NOT NULL DEFAULT FALSE,
publish_date TIMESTAMPTZ,
visibility VARCHAR(50),
instructor_id VARCHAR(100),
thumbnail TEXT,
display_order INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
vimeo_id TEXT,
vimeo_embed_url TEXT,
vimeo_player_html TEXT,
vimeo_status VARCHAR(50),
video_host_provider VARCHAR(20),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_sub_module_videos_sub_module_id ON sub_module_videos(sub_module_id);
CREATE TABLE IF NOT EXISTS sub_module_practices (
id BIGSERIAL PRIMARY KEY,
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
intro_video_url TEXT,
display_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(question_set_id)
);
CREATE INDEX IF NOT EXISTS idx_sub_module_practices_sub_module_id ON sub_module_practices(sub_module_id);
-- Practice progress now supports sub-module owned practices where no legacy sub_course exists.
ALTER TABLE user_practice_progress
ALTER COLUMN sub_course_id DROP NOT NULL;
-- Backfill from existing structure
INSERT INTO course_sub_categories (category_id, name, description, display_order, is_active)
SELECT cc.id, c.title || ' Group', 'Auto-generated from existing course structure', 0, TRUE
FROM courses c
JOIN course_categories cc ON cc.id = c.category_id
LEFT JOIN course_sub_categories csc
ON csc.category_id = cc.id AND csc.name = c.title || ' Group'
WHERE csc.id IS NULL;
UPDATE courses c
SET sub_category_id = csc.id
FROM course_sub_categories csc
WHERE csc.category_id = c.category_id
AND csc.name = c.title || ' Group'
AND c.sub_category_id IS NULL;
INSERT INTO levels (course_id, cefr_level, display_order, is_active)
SELECT
sc.course_id,
sc.sub_level,
MIN(sc.display_order),
BOOL_AND(sc.is_active)
FROM sub_courses sc
WHERE sc.sub_level IN ('A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3')
GROUP BY sc.course_id, sc.sub_level
ON CONFLICT (course_id, cefr_level) DO NOTHING;
INSERT INTO modules (level_id, title, description, display_order, is_active)
SELECT
l.id,
l.cefr_level || ' Module 1',
'Auto-generated default module for ' || l.cefr_level,
1,
l.is_active
FROM levels l
LEFT JOIN modules m ON m.level_id = l.id AND m.display_order = 1
WHERE m.id IS NULL;
INSERT INTO sub_modules (module_id, title, description, display_order, is_active, legacy_sub_course_id)
SELECT
m.id,
sc.title,
sc.description,
sc.display_order,
sc.is_active,
sc.id
FROM sub_courses sc
JOIN levels l
ON l.course_id = sc.course_id
AND l.cefr_level = sc.sub_level
JOIN modules m
ON m.level_id = l.id
AND m.display_order = 1
LEFT JOIN sub_modules sm ON sm.legacy_sub_course_id = sc.id
WHERE sm.id IS NULL;
INSERT INTO sub_module_videos (
sub_module_id,
title,
description,
video_url,
duration,
resolution,
is_published,
publish_date,
visibility,
instructor_id,
thumbnail,
display_order,
status,
vimeo_id,
vimeo_embed_url,
vimeo_player_html,
vimeo_status,
video_host_provider
)
SELECT
sm.id,
scv.title,
scv.description,
scv.video_url,
scv.duration,
scv.resolution,
scv.is_published,
scv.publish_date,
scv.visibility,
scv.instructor_id,
scv.thumbnail,
scv.display_order,
scv.status,
scv.vimeo_id,
scv.vimeo_embed_url,
scv.vimeo_player_html,
scv.vimeo_status,
scv.video_host_provider
FROM sub_course_videos scv
JOIN sub_modules sm ON sm.legacy_sub_course_id = scv.sub_course_id
WHERE NOT EXISTS (
SELECT 1
FROM sub_module_videos smv
WHERE smv.sub_module_id = sm.id
AND smv.title = scv.title
AND COALESCE(smv.video_url, '') = COALESCE(scv.video_url, '')
);
UPDATE question_sets qs
SET owner_type = 'SUB_MODULE',
owner_id = sm.id
FROM sub_modules sm
WHERE qs.owner_type = 'SUB_COURSE'
AND qs.owner_id = sm.legacy_sub_course_id
AND qs.set_type = 'PRACTICE';
INSERT INTO sub_module_practices (sub_module_id, question_set_id, intro_video_url, display_order, is_active)
SELECT
sm.id,
qs.id,
qs.intro_video_url,
COALESCE(qs.display_order, 0),
(qs.status != 'ARCHIVED')
FROM question_sets qs
JOIN sub_modules sm
ON qs.owner_type = 'SUB_MODULE'
AND qs.owner_id = sm.id
WHERE qs.set_type = 'PRACTICE'
ON CONFLICT (question_set_id) DO NOTHING;