feat(db): миграции групп, курсов, статусов и профиля
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
-- Добавляем роль преподавателя. Старые ADMIN/STUDENT сохраняются.
|
||||
ALTER TABLE accounts DROP CONSTRAINT IF EXISTS accounts_role_check;
|
||||
ALTER TABLE accounts
|
||||
ADD CONSTRAINT accounts_role_check CHECK (role IN ('ADMIN', 'TEACHER', 'STUDENT'));
|
||||
@@ -0,0 +1,62 @@
|
||||
-- Поля группы-когорты. На этой фазе nullable: существующие группы могут не
|
||||
-- соответствовать шаблону имени и требуют ручного разбора.
|
||||
ALTER TABLE groups
|
||||
ADD COLUMN stream varchar(50),
|
||||
ADD COLUMN course smallint,
|
||||
ADD COLUMN number smallint,
|
||||
ADD COLUMN degree varchar(2),
|
||||
ADD COLUMN enrollment_year smallint,
|
||||
ADD COLUMN status varchar(16) NOT NULL DEFAULT 'ACTIVE';
|
||||
|
||||
-- Имя больше не глобально уникально: одноимённые группы разных наборов легальны.
|
||||
ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_name_key;
|
||||
|
||||
-- Best-effort бэкфилл из имени. Учебный год начинается в сентябре.
|
||||
WITH params AS (
|
||||
SELECT CASE
|
||||
WHEN EXTRACT(MONTH FROM now()) >= 9 THEN EXTRACT(YEAR FROM now())
|
||||
ELSE EXTRACT(YEAR FROM now()) - 1
|
||||
END::smallint AS academic_year_start
|
||||
),
|
||||
parsed AS (
|
||||
SELECT
|
||||
g.id,
|
||||
m[1] AS stream,
|
||||
m[2]::smallint AS course,
|
||||
m[3]::smallint AS number,
|
||||
m[4] AS degree
|
||||
FROM groups g
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT regexp_match(g.name, '^(.+)-([0-9])([0-9]+)(БО|МО)$') AS m
|
||||
) parsed_name
|
||||
)
|
||||
UPDATE groups g
|
||||
SET stream = p.stream,
|
||||
course = p.course,
|
||||
number = p.number,
|
||||
degree = p.degree,
|
||||
enrollment_year = ((SELECT academic_year_start FROM params) - (p.course - 1))::smallint
|
||||
FROM parsed p
|
||||
WHERE g.id = p.id
|
||||
AND p.stream IS NOT NULL;
|
||||
|
||||
ALTER TABLE groups
|
||||
ADD CONSTRAINT groups_status_check CHECK (status IN ('ACTIVE', 'GRADUATED', 'ARCHIVED')),
|
||||
ADD CONSTRAINT groups_degree_check CHECK (degree IS NULL OR degree IN ('БО', 'МО')),
|
||||
ADD CONSTRAINT groups_course_range CHECK (
|
||||
(degree IS NULL OR course IS NULL)
|
||||
OR (degree = 'БО' AND course BETWEEN 1 AND 4)
|
||||
OR (degree = 'МО' AND course BETWEEN 1 AND 2)
|
||||
);
|
||||
|
||||
-- Уникальность когорты для уже разобранных групп. После ручного разбора
|
||||
-- и подтверждения года набора ограничение станет полным (без WHERE).
|
||||
CREATE UNIQUE INDEX groups_cohort_unique
|
||||
ON groups (stream, degree, number, course, enrollment_year)
|
||||
WHERE stream IS NOT NULL
|
||||
AND degree IS NOT NULL
|
||||
AND number IS NOT NULL
|
||||
AND course IS NOT NULL
|
||||
AND enrollment_year IS NOT NULL;
|
||||
|
||||
CREATE INDEX groups_status_idx ON groups (status);
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Академический статус студента и год набора.
|
||||
ALTER TABLE students
|
||||
ADD COLUMN status varchar(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
ADD COLUMN enrollment_year smallint;
|
||||
|
||||
ALTER TABLE students
|
||||
ADD CONSTRAINT students_status_check CHECK (
|
||||
status IN ('ACTIVE', 'ACADEMIC_LEAVE', 'EXPELLED', 'GRADUATED', 'ARCHIVED')
|
||||
);
|
||||
|
||||
-- Год набора наследуется от текущей группы; для групп без разобранного года
|
||||
-- остаётся NULL и требует подтверждения админом.
|
||||
UPDATE students s
|
||||
SET enrollment_year = g.enrollment_year
|
||||
FROM enrollments e
|
||||
JOIN groups g ON g.id = e.group_id
|
||||
WHERE e.student_id = s.account_id
|
||||
AND s.enrollment_year IS NULL;
|
||||
|
||||
CREATE INDEX students_status_idx ON students (status);
|
||||
@@ -0,0 +1,43 @@
|
||||
-- Журнал повышения курса: шапка запуска и по-элементный план/результат.
|
||||
CREATE TABLE promotion_runs (
|
||||
id uuid PRIMARY KEY DEFAULT uuidv7(),
|
||||
year integer NOT NULL,
|
||||
mode varchar(16) NOT NULL,
|
||||
actor_account_id uuid REFERENCES accounts (id) ON DELETE SET NULL,
|
||||
status varchar(16) NOT NULL DEFAULT 'PLANNED',
|
||||
started_at timestamptz NOT NULL DEFAULT now(),
|
||||
finished_at timestamptz,
|
||||
result jsonb,
|
||||
CONSTRAINT promotion_runs_mode_check CHECK (mode IN ('DRY_RUN', 'APPLY')),
|
||||
CONSTRAINT promotion_runs_status_check CHECK (
|
||||
status IN ('PLANNED', 'RUNNING', 'COMPLETED', 'FAILED', 'ROLLED_BACK')
|
||||
)
|
||||
);
|
||||
|
||||
-- Один успешный (или выполняющийся) APPLY на год. Неудачный запуск можно
|
||||
-- повторить; после отката статус ROLLED_BACK также не блокирует новый запуск.
|
||||
CREATE UNIQUE INDEX promotion_runs_apply_year_uidx
|
||||
ON promotion_runs (year)
|
||||
WHERE mode = 'APPLY' AND status IN ('RUNNING', 'COMPLETED');
|
||||
|
||||
CREATE INDEX promotion_runs_year_idx ON promotion_runs (year, mode);
|
||||
|
||||
CREATE TABLE promotion_run_items (
|
||||
id uuid PRIMARY KEY DEFAULT uuidv7(),
|
||||
run_id uuid NOT NULL REFERENCES promotion_runs (id) ON DELETE CASCADE,
|
||||
entity_type varchar(16) NOT NULL,
|
||||
entity_id uuid NOT NULL,
|
||||
action varchar(32) NOT NULL,
|
||||
before_state jsonb,
|
||||
after_state jsonb,
|
||||
status varchar(16) NOT NULL DEFAULT 'PLANNED',
|
||||
error text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT promotion_run_items_entity_check CHECK (entity_type IN ('GROUP', 'STUDENT')),
|
||||
CONSTRAINT promotion_run_items_status_check CHECK (
|
||||
status IN ('PLANNED', 'APPLIED', 'SKIPPED', 'ERROR', 'ROLLED_BACK')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX promotion_run_items_run_idx ON promotion_run_items (run_id);
|
||||
CREATE INDEX promotion_run_items_entity_idx ON promotion_run_items (entity_type, entity_id);
|
||||
@@ -0,0 +1,28 @@
|
||||
-- История членства: переводы, академ, возврат, выпуск, отчисление, повышение.
|
||||
CREATE TABLE membership_history (
|
||||
id uuid PRIMARY KEY DEFAULT uuidv7(),
|
||||
student_id uuid NOT NULL REFERENCES students (account_id) ON DELETE CASCADE,
|
||||
from_group_id uuid REFERENCES groups (id) ON DELETE SET NULL,
|
||||
to_group_id uuid REFERENCES groups (id) ON DELETE SET NULL,
|
||||
action varchar(32) NOT NULL,
|
||||
reason text NOT NULL DEFAULT '',
|
||||
actor_account_id uuid REFERENCES accounts (id) ON DELETE SET NULL,
|
||||
promotion_run_id uuid REFERENCES promotion_runs (id) ON DELETE SET NULL,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT membership_history_action_check CHECK (
|
||||
action IN (
|
||||
'ENROLL', 'TRANSFER', 'ACADEMIC', 'RETURN', 'GRADUATE',
|
||||
'EXPELL', 'PROMOTE', 'IMPORT', 'ARCHIVE', 'RESTORE'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX membership_history_student_idx ON membership_history (student_id, at DESC);
|
||||
CREATE INDEX membership_history_from_group_idx ON membership_history (from_group_id);
|
||||
CREATE INDEX membership_history_to_group_idx ON membership_history (to_group_id);
|
||||
CREATE INDEX membership_history_promotion_run_idx ON membership_history (promotion_run_id);
|
||||
|
||||
-- Текущие зачисления становятся первой записью истории.
|
||||
INSERT INTO membership_history (student_id, to_group_id, action, at)
|
||||
SELECT student_id, group_id, 'ENROLL', enrolled_at
|
||||
FROM enrollments;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Статус аккаунта для всех пользователей: архив закрывает вход.
|
||||
ALTER TABLE accounts
|
||||
ADD COLUMN status varchar(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
ADD COLUMN archived_at timestamptz;
|
||||
|
||||
ALTER TABLE accounts
|
||||
ADD CONSTRAINT accounts_status_check CHECK (status IN ('ACTIVE', 'ARCHIVED'));
|
||||
|
||||
CREATE INDEX accounts_status_idx ON accounts (status);
|
||||
Reference in New Issue
Block a user