229 lines
7.0 KiB
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;
|
|
|