-- ═══════════════════════════════════════════════════════════════════════════ -- JUNCTION REVAMP — Units and Lessons run independently (Jul. 7, 2026) -- -- courses ⇄ course_units ⇄ units ⇄ unit_lessons ⇄ lessons -- -- Run order matters: create → backfill → relax progress FKs → drop old columns. -- Postgres / CockroachDB compatible. Take a backup before running. -- ═══════════════════════════════════════════════════════════════════════════ BEGIN; -- ── 1. Junction tables ─────────────────────────────────────────────────────── CREATE TABLE course_units ( course_unit_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, course_id BIGINT NOT NULL REFERENCES courses (course_id) ON DELETE CASCADE, unit_id BIGINT NOT NULL REFERENCES units (unit_id) ON DELETE CASCADE, order_index INTEGER NOT NULL DEFAULT 0, "createdBy" BIGINT NULL, "updatedBy" BIGINT NULL, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_course_units_course_unit UNIQUE (course_id, unit_id) ); CREATE INDEX idx_course_units_course_id ON course_units (course_id); CREATE INDEX idx_course_units_unit_id ON course_units (unit_id); CREATE INDEX idx_course_units_order ON course_units (order_index); CREATE TABLE unit_lessons ( unit_lesson_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, unit_id BIGINT NOT NULL REFERENCES units (unit_id) ON DELETE CASCADE, lesson_id BIGINT NOT NULL REFERENCES lessons (lesson_id) ON DELETE CASCADE, order_index INTEGER NOT NULL DEFAULT 0, "createdBy" BIGINT NULL, "updatedBy" BIGINT NULL, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_unit_lessons_unit_lesson UNIQUE (unit_id, lesson_id) ); CREATE INDEX idx_unit_lessons_unit_id ON unit_lessons (unit_id); CREATE INDEX idx_unit_lessons_lesson_id ON unit_lessons (lesson_id); CREATE INDEX idx_unit_lessons_order ON unit_lessons (order_index); -- ── 2. Backfill from the old direct FKs (preserves ordering) ───────────────── INSERT INTO course_units (course_id, unit_id, order_index, "createdBy", "createdAt", "updatedAt") SELECT u.course_id, u.unit_id, COALESCE(u.order_index, 0), u."createdBy", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP FROM units u WHERE u.course_id IS NOT NULL; INSERT INTO unit_lessons (unit_id, lesson_id, order_index, "createdBy", "createdAt", "updatedAt") SELECT l.unit_id, l.lesson_id, COALESCE(l.order_index, 0), l."createdBy", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP FROM lessons l WHERE l.unit_id IS NOT NULL; -- ── 3. Progress tables — standalone reads carry NULL course/unit context ───── ALTER TABLE unit_reading_progress ALTER COLUMN course_id DROP NOT NULL; ALTER TABLE lesson_reading_progress ALTER COLUMN course_id DROP NOT NULL; ALTER TABLE lesson_reading_progress ALTER COLUMN unit_id DROP NOT NULL; -- ── 4. Drop the old FK + order columns ─────────────────────────────────────── -- (dependent indexes/constraints on these columns are dropped automatically) ALTER TABLE units DROP COLUMN course_id; ALTER TABLE units DROP COLUMN order_index; ALTER TABLE lessons DROP COLUMN unit_id; ALTER TABLE lessons DROP COLUMN order_index; COMMIT; -- ── Sanity checks (run after commit) ───────────────────────────────────────── -- SELECT COUNT(*) FROM course_units; -- should equal old COUNT(*) FROM units WHERE course_id IS NOT NULL -- SELECT COUNT(*) FROM unit_lessons; -- should equal old COUNT(*) FROM lessons WHERE unit_id IS NOT NULL -- SELECT c.title, u.title, cu.order_index FROM course_units cu -- JOIN courses c USING (course_id) JOIN units u USING (unit_id) -- ORDER BY c.title, cu.order_index LIMIT 20;