-- Preflight (до миграций) для перехода на группы-когорты, курсы и перенос лаб/материалов. -- Только чтение. Запускать на копии рабочей БД до применения миграций. -- Пример: -- psql -h localhost -U lms -d lms -f deploy/preflight/groups-courses.sql \pset pager off \echo '=== 1. Группы, не подходящие под шаблон ^(.+)-([0-9])([0-9]+)(БО|МО)$ ===' SELECT id, name FROM identity.groups WHERE name !~ '^(.+)-([0-9])([0-9]+)(БО|МО)$' ORDER BY name; \echo '=== 2. Разобранные группы с одинаковым кортежем (stream, degree, number, course, enrollment_year) ===' WITH params AS ( SELECT CASE WHEN EXTRACT(MONTH FROM now()) >= 9 THEN EXTRACT(YEAR FROM now()) ELSE EXTRACT(YEAR FROM now()) - 1 END::int AS academic_year_start ), parsed AS ( SELECT g.name, m[1] AS stream, m[2]::int AS course, m[3]::int AS number, m[4] AS degree, (SELECT academic_year_start FROM params) - (m[2]::int - 1) AS enrollment_year FROM identity.groups g CROSS JOIN LATERAL ( SELECT regexp_match(g.name, '^(.+)-([0-9])([0-9]+)(БО|МО)$') AS m ) parsed_name WHERE m IS NOT NULL ) SELECT stream, degree, number, course, enrollment_year, count(*) AS groups, array_agg(name ORDER BY name) AS names FROM parsed GROUP BY stream, degree, number, course, enrollment_year HAVING count(*) > 1 ORDER BY stream, degree, course, number; \echo '=== 3. Группы, которые не разберутся шаблоном (потребуют ручного разбора) ===' WITH parsed AS ( SELECT g.id, g.name, m[1] AS stream, m[2]::int AS course, m[3]::int AS number, m[4] AS degree FROM identity.groups g CROSS JOIN LATERAL ( SELECT regexp_match(g.name, '^(.+)-([0-9])([0-9]+)(БО|МО)$') AS m ) parsed_name ) SELECT id, name, stream, course, number, degree FROM parsed WHERE stream IS NULL OR course IS NULL OR number IS NULL OR degree IS NULL ORDER BY name; \echo '=== 4. Одинаковые имена групп (после снятия UNIQUE станут допустимы) ===' SELECT name, count(*) AS groups FROM identity.groups GROUP BY name HAVING count(*) > 1 ORDER BY name; \echo '=== 5. Студенты без текущей группы ===' SELECT s.student_no, a.login, s.last_name, s.first_name FROM identity.students s JOIN identity.accounts a ON a.id = s.account_id LEFT JOIN identity.enrollments e ON e.student_id = s.account_id WHERE e.id IS NULL ORDER BY s.student_no; \echo '=== 6. Аккаунты по ролям (до появления статусов) ===' SELECT role, count(*) AS accounts FROM identity.accounts GROUP BY role ORDER BY role; \echo '=== 7. Дубли номеров лабораторных (все лабы переезжают в один курс C) ===' SELECT number, count(*) AS labs, array_agg(id ORDER BY created_at) AS ids, array_agg(title ORDER BY created_at) AS titles FROM academic.labs GROUP BY number HAVING count(*) > 1 ORDER BY number; \echo '=== 8. Лабораторные по семестрам ===' SELECT semester_id, number, count(*) AS labs FROM academic.labs GROUP BY semester_id, number HAVING count(*) > 1 ORDER BY semester_id, number; \echo '=== 9. Дубли slug глав (до перехода на частичные индексы) ===' SELECT slug, count(*) AS chapters FROM academic.chapters GROUP BY slug HAVING count(*) > 1 ORDER BY slug; \echo '=== 10. Объём материалов ===' SELECT (SELECT count(*) FROM academic.chapters) AS chapters, (SELECT count(*) FROM academic.sections) AS sections, (SELECT count(*) FROM academic.articles) AS articles, (SELECT count(*) FROM academic.article_assets) AS article_assets; \echo '=== 11. Активы без статьи ===' SELECT count(*) AS assets_without_article FROM academic.article_assets WHERE article_id IS NULL; \echo '=== 12. Семестры ===' SELECT id, title, year, active FROM academic.semesters ORDER BY year, title;