Files
magistr/backend/src/main/resources/db/migration/V1__init.sql

1048 lines
49 KiB
PL/PgSQL
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ==========================================
-- Инициализация расширений
-- ==========================================
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- ===============================
-- Справочники высшего уровня
-- ===============================
CREATE TABLE IF NOT EXISTS departments (
id BIGSERIAL UNIQUE PRIMARY KEY NOT NULL,
name VARCHAR(255) NOT NULL,
code BIGINT UNIQUE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
INSERT INTO departments (name, code) VALUES
('Кафедра ИБ', 1),
('Кафедра ВТ', 2),
('Кафедра КТ', 3);
CREATE TABLE IF NOT EXISTS specialties (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
specialty_code VARCHAR(255) UNIQUE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
INSERT INTO specialties (name, specialty_code) VALUES
('Информационная безопасность', '10.03.01'),
('Информатика и вычислительная техника', '09.03.01'),
('Программная инженерия', '09.03.04')
ON CONFLICT (specialty_code) DO NOTHING;
CREATE TABLE IF NOT EXISTS specialty_profiles (
id BIGSERIAL PRIMARY KEY,
specialty_id BIGINT NOT NULL REFERENCES specialties(id) ON DELETE CASCADE,
name VARCHAR(500) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT,
CONSTRAINT uq_specialty_profiles_name UNIQUE (specialty_id, name)
);
INSERT INTO specialty_profiles (specialty_id, name)
SELECT id, 'Без профиля'
FROM specialties
ON CONFLICT (specialty_id, name) DO NOTHING;
-- ==========================================
-- Пользователи и роли
-- ==========================================
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(30) NOT NULL DEFAULT 'STUDENT',
full_name VARCHAR(255) NOT NULL,
job_title VARCHAR(255) NOT NULL,
department_id BIGINT NOT NULL REFERENCES departments(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
-- Админ по умолчанию: admin / admin (bcrypt через pgcrypto)
INSERT INTO users (username, password, role, full_name, job_title, department_id)
VALUES ('admin', crypt('admin', gen_salt('bf', 10)), 'ADMIN', 'Иванов Админ Иванович', 'Доцент', 1),
('Тестовый преподаватель', crypt('1234567890', gen_salt('bf', 10)), 'TEACHER', 'Петров Препод Петрович', 'Профессор', 2),
('учебный_отдел', crypt('1234567890', gen_salt('bf', 10)), 'EDUCATION_OFFICE', 'Оператор учебного отдела', 'Учебный отдел', 1),
('кафедраб', crypt('1234567890', gen_salt('bf', 10)), 'DEPARTMENT', 'Оператор кафедры ИБ', 'Кафедра', 1),
('просмотр_расписаний', crypt('1234567890', gen_salt('bf', 10)), 'SCHEDULE_VIEWER', 'Оператор просмотра расписаний', 'Просмотр расписаний', 1)
ON CONFLICT (username) DO NOTHING;
CREATE TABLE IF NOT EXISTS teacher_department_assignments (
id BIGSERIAL PRIMARY KEY,
teacher_id BIGINT NOT NULL REFERENCES users(id),
department_id BIGINT NOT NULL REFERENCES departments(id),
valid_from DATE NOT NULL,
valid_to DATE,
is_primary BOOLEAN NOT NULL DEFAULT TRUE,
comment TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by BIGINT REFERENCES users(id),
CONSTRAINT chk_teacher_department_dates CHECK (valid_to IS NULL OR valid_to >= valid_from)
);
CREATE INDEX IF NOT EXISTS idx_teacher_department_teacher_dates
ON teacher_department_assignments(teacher_id, valid_from, valid_to);
CREATE INDEX IF NOT EXISTS idx_teacher_department_department_dates
ON teacher_department_assignments(department_id, valid_from, valid_to);
CREATE UNIQUE INDEX IF NOT EXISTS uq_teacher_department_open_primary
ON teacher_department_assignments(teacher_id)
WHERE is_primary = TRUE AND valid_to IS NULL;
INSERT INTO teacher_department_assignments (teacher_id, department_id, valid_from, is_primary, comment)
SELECT u.id, u.department_id, COALESCE(u.created_at::DATE, CURRENT_DATE), TRUE, 'Начальная кафедра из users.department_id'
FROM users u
WHERE u.role = 'TEACHER'
AND NOT EXISTS (
SELECT 1
FROM teacher_department_assignments existing
WHERE existing.teacher_id = u.id
);
-- ==========================================
-- Образовательные формы
-- ==========================================
CREATE TABLE IF NOT EXISTS education_forms (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
INSERT INTO education_forms (name) VALUES
('Бакалавриат'),
('Магистратура'),
('Специалитет')
ON CONFLICT (name) DO NOTHING;
-- ==========================================
-- Учебные группы
-- ==========================================
CREATE TABLE IF NOT EXISTS student_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
group_size BIGINT NOT NULL,
education_form_id BIGINT NOT NULL REFERENCES education_forms(id),
department_id BIGINT NOT NULL REFERENCES departments(id),
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
year_start_study BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
-- Тестовая базовая группа для работы
INSERT INTO student_groups (name, group_size, education_form_id, department_id, specialty_id, specialty_profile_id, year_start_study)
SELECT data.name, data.group_size, ef.id, dept.id, specialty.id, profile.id, data.year_start_study
FROM (VALUES
('ИВТ-21-1', 25::BIGINT, 'Бакалавриат', 'Кафедра ИБ', '09.03.01', 2025::BIGINT),
('ИБ-41м', 15::BIGINT, 'Магистратура', 'Кафедра ИБ', '10.03.01', 2024::BIGINT)
) AS data(name, group_size, education_form_name, department_name, specialty_code, year_start_study)
JOIN education_forms ef ON ef.name = data.education_form_name
JOIN departments dept ON dept.name = data.department_name
JOIN specialties specialty ON specialty.specialty_code = data.specialty_code
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
WHERE NOT EXISTS (SELECT 1 FROM student_groups existing WHERE existing.name = data.name);
-- ==========================================
-- Подгруппы (например: "ИВТ-21-1 Подгруппа 1")
-- ==========================================
CREATE TABLE IF NOT EXISTS subgroups (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
student_capacity INT,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT,
UNIQUE(group_id, name)
);
INSERT INTO subgroups (group_id, name, student_capacity)
SELECT
sg.id,
data.name,
CASE
WHEN data.part_number = 1 THEN CEIL(sg.group_size::NUMERIC / 2)::INT
ELSE FLOOR(sg.group_size::NUMERIC / 2)::INT
END
FROM student_groups sg
CROSS JOIN (VALUES
('Подгруппа 1', 1),
('Подгруппа 2', 2)
) AS data(name, part_number)
WHERE sg.name IN ('ИВТ-21-1', 'ИБ-41м')
ON CONFLICT (group_id, name) DO NOTHING;
-- ==========================================
-- Справочники
-- ==========================================
-- Дисциплины
CREATE TABLE IF NOT EXISTS subjects (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) UNIQUE NOT NULL,
code VARCHAR(20),
department_id BIGINT NOT NULL REFERENCES departments(id),
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
INSERT INTO subjects (name, department_id) VALUES
('Высшая математика', 1),
('Философия', 1),
('Информатика', 1),
('Базы данных', 1),
('Английский язык', 1)
ON CONFLICT (name) DO NOTHING;
CREATE TABLE IF NOT EXISTS subject_comments (
id BIGSERIAL PRIMARY KEY,
subject_id BIGINT NOT NULL REFERENCES subjects(id),
author_id BIGINT REFERENCES users(id),
comment TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_subject_comments_subject
ON subject_comments(subject_id, created_at DESC);
-- Типы занятий
CREATE TABLE IF NOT EXISTS lesson_types (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
color_code VARCHAR(7) DEFAULT '#3788d8',
duration_minutes INT DEFAULT 90
);
INSERT INTO lesson_types (name, color_code) VALUES
('Лекция', '#FF6B6B'),
('Практика', '#4ECDC4'),
('Лабораторная работа', '#45B7D1')
ON CONFLICT (name) DO NOTHING;
-- Оборудование
CREATE TABLE IF NOT EXISTS equipments (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
inventory_number VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
INSERT INTO equipments (name) VALUES
('Проектор'),
('ПК'),
('Лаборатория'),
('Интерактивная доска'),
('Документ-камера'),
('Аудиосистема')
ON CONFLICT (name) DO NOTHING;
-- Аудитории
CREATE TABLE IF NOT EXISTS classrooms (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
capacity INT NOT NULL CHECK (capacity > 0),
building VARCHAR(50),
floor INT,
is_available BOOLEAN DEFAULT TRUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
active_from DATE,
active_to DATE,
archived_at TIMESTAMP,
archived_by BIGINT,
archive_reason TEXT
);
INSERT INTO classrooms (name, capacity, building, floor) VALUES
('101 Ленинская', 120, 'Главный корпус', 1),
('202 IT Lab', 20, 'Корпус IT', 2),
('303 Обычная', 30, 'Главный корпус', 3)
ON CONFLICT (name) DO NOTHING;
-- Привязка оборудования к аудиториям (Many-to-Many)
CREATE TABLE IF NOT EXISTS classroom_equipments (
classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE,
equipment_id BIGINT NOT NULL REFERENCES equipments(id) ON DELETE CASCADE,
quantity INT DEFAULT 1 CHECK (quantity > 0),
notes TEXT,
PRIMARY KEY (classroom_id, equipment_id)
);
INSERT INTO classroom_equipments (classroom_id, equipment_id, quantity)
SELECT c.id, e.id,
CASE
WHEN e.name = 'ПК' AND c.name = '202 IT Lab' THEN 15
WHEN e.name = 'ПК' THEN 1
ELSE 1
END
FROM classrooms c, equipments e
WHERE
(c.name = '101 Ленинская' AND e.name IN ('Проектор', 'Интерактивная доска', 'Аудиосистема'))
OR (c.name = '202 IT Lab' AND e.name IN ('ПК', 'Проектор', 'Лаборатория', 'Интерактивная доска'))
OR (c.name = '303 Обычная' AND e.name IN ('Проектор'))
ON CONFLICT (classroom_id, equipment_id) DO NOTHING;
CREATE INDEX IF NOT EXISTS idx_departments_status ON departments(status);
CREATE INDEX IF NOT EXISTS idx_specialties_status ON specialties(status);
CREATE INDEX IF NOT EXISTS idx_specialty_profiles_status ON specialty_profiles(status);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
CREATE INDEX IF NOT EXISTS idx_education_forms_status ON education_forms(status);
CREATE INDEX IF NOT EXISTS idx_student_groups_status ON student_groups(status);
CREATE INDEX IF NOT EXISTS idx_subgroups_status ON subgroups(status);
CREATE INDEX IF NOT EXISTS idx_subjects_status ON subjects(status);
CREATE INDEX IF NOT EXISTS idx_equipments_status ON equipments(status);
CREATE INDEX IF NOT EXISTS idx_classrooms_status ON classrooms(status);
-- ==========================================
-- Связи для преподавателей
-- ==========================================
CREATE TABLE IF NOT EXISTS teacher_subjects (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
subject_id BIGINT NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
qualification_level VARCHAR(50),
experience_years INT,
PRIMARY KEY(user_id, subject_id)
);
CREATE TABLE IF NOT EXISTS teacher_lesson_types (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
subject_id BIGINT NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, subject_id, lesson_type_id)
);
-- ==========================================
-- Динамическое расписание: календарь, слоты и правила
-- ==========================================
CREATE TABLE IF NOT EXISTS time_slot_scopes (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(120) NOT NULL,
apply_mode VARCHAR(20) NOT NULL,
day_of_week INT NULL,
system_scope BOOLEAN NOT NULL DEFAULT FALSE,
display_order INT NOT NULL DEFAULT 100,
CONSTRAINT chk_time_slot_scopes_mode CHECK (apply_mode IN ('DEFAULT', 'WEEKDAY', 'MANUAL')),
CONSTRAINT chk_time_slot_scopes_day CHECK (day_of_week IS NULL OR day_of_week BETWEEN 1 AND 7),
CONSTRAINT chk_time_slot_scopes_weekday CHECK (
(apply_mode = 'WEEKDAY' AND day_of_week IS NOT NULL)
OR (apply_mode <> 'WEEKDAY' AND day_of_week IS NULL)
)
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_time_slot_scopes_default
ON time_slot_scopes(apply_mode)
WHERE apply_mode = 'DEFAULT';
CREATE UNIQUE INDEX IF NOT EXISTS uq_time_slot_scopes_weekday
ON time_slot_scopes(day_of_week)
WHERE apply_mode = 'WEEKDAY';
INSERT INTO time_slot_scopes (code, name, apply_mode, day_of_week, system_scope, display_order) VALUES
('default', 'Базовая сетка', 'DEFAULT', NULL, TRUE, 10),
('saturday', 'Субботняя сетка', 'WEEKDAY', 6, TRUE, 20)
ON CONFLICT (code) DO NOTHING;
CREATE TABLE IF NOT EXISTS time_slots (
id BIGSERIAL PRIMARY KEY,
time_slot_scope_id BIGINT NOT NULL REFERENCES time_slot_scopes(id) ON DELETE CASCADE,
order_number INT NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
duration_minutes INT NOT NULL,
CONSTRAINT chk_time_slots_order_positive CHECK (order_number > 0),
CONSTRAINT chk_time_slots_time_range CHECK (start_time < end_time),
CONSTRAINT chk_time_slots_duration_positive CHECK (duration_minutes > 0)
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_time_slots_scope_order
ON time_slots(time_slot_scope_id, order_number);
CREATE INDEX IF NOT EXISTS idx_time_slots_scope ON time_slots(time_slot_scope_id, order_number);
INSERT INTO time_slots (time_slot_scope_id, order_number, start_time, end_time, duration_minutes)
SELECT scope.id, data.order_number, data.start_time::TIME, data.end_time::TIME, data.duration_minutes
FROM (VALUES
(1, '08:00', '09:30', 90),
(2, '09:40', '11:10', 90),
(3, '11:40', '13:10', 90),
(4, '13:20', '14:50', 90),
(5, '15:00', '16:30', 90),
(6, '16:50', '18:20', 90),
(7, '18:30', '20:00', 90)
) AS data(order_number, start_time, end_time, duration_minutes)
CROSS JOIN time_slot_scopes scope
WHERE scope.code IN ('default', 'saturday')
ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS time_slot_date_assignments (
id BIGSERIAL PRIMARY KEY,
assignment_date DATE NOT NULL UNIQUE,
time_slot_scope_id BIGINT NOT NULL REFERENCES time_slot_scopes(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_time_slot_date_assignments_scope
ON time_slot_date_assignments(time_slot_scope_id);
CREATE TABLE IF NOT EXISTS academic_years (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(20) NOT NULL UNIQUE,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT chk_academic_years_dates CHECK (start_date <= end_date)
);
INSERT INTO academic_years (title, start_date, end_date) VALUES
('2024-2025', '2024-09-01', '2025-06-30'),
('2025-2026', '2025-09-01', '2026-06-30')
ON CONFLICT (title) DO NOTHING;
CREATE TABLE IF NOT EXISTS semesters (
id BIGSERIAL PRIMARY KEY,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
semester_type VARCHAR(20) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT chk_semesters_type CHECK (semester_type IN ('autumn', 'spring')),
CONSTRAINT chk_semesters_dates CHECK (start_date <= end_date),
CONSTRAINT uq_semesters_year_type UNIQUE (academic_year_id, semester_type)
);
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
SELECT id, 'autumn', start_date, MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 1, 31)
FROM academic_years
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
SELECT id, 'spring', MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 2, 1), end_date
FROM academic_years
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
CREATE INDEX IF NOT EXISTS idx_semesters_dates ON semesters(start_date, end_date);
CREATE TABLE IF NOT EXISTS academic_calendar_activity_types (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
allow_schedule BOOLEAN NOT NULL DEFAULT FALSE,
color_code VARCHAR(7) NOT NULL DEFAULT '#64748b',
display_order INT NOT NULL DEFAULT 100,
description TEXT
);
INSERT INTO academic_calendar_activity_types (code, name, allow_schedule, color_code, display_order, description) VALUES
('Т', 'Теоретическое обучение', TRUE, '#10b981', 10, 'Обычные занятия динамического расписания разрешены'),
('Э', 'Экзаменационная сессия', FALSE, '#f59e0b', 20, 'Обычные занятия не генерируются'),
('К', 'Каникулы', FALSE, '#38bdf8', 30, 'Обычные занятия не генерируются'),
('У', 'Учебная практика', FALSE, '#a78bfa', 40, 'Обычные занятия не генерируются'),
('П', 'Производственная практика', FALSE, '#fb7185', 50, 'Обычные занятия не генерируются'),
('Пд', 'Преддипломная практика', FALSE, '#f97316', 60, 'Обычные занятия не генерируются'),
('Н', 'Научно-исследовательская работа', FALSE, '#22c55e', 70, 'Обычные занятия не генерируются'),
('Г', 'Подготовка к сдаче и сдача государственного экзамена', FALSE, '#ef4444', 80, 'Обычные занятия не генерируются'),
('Д', 'Подготовка к защите и защита выпускной квалификационной работы', FALSE, '#dc2626', 90, 'Обычные занятия не генерируются'),
('ПА', 'Повторная промежуточная аттестация', FALSE, '#eab308', 100, 'Обычные занятия не генерируются'),
('С', 'Симуляционный курс', FALSE, '#06b6d4', 110, 'Обычные занятия не генерируются'),
('*', 'Нерабочий праздничный день', FALSE, '#64748b', 120, 'День исключается из генерации расписания'),
('=', 'День вне учебного года', FALSE, '#475569', 130, 'День исключается из генерации расписания')
ON CONFLICT (code) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendars (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
study_form_id BIGINT NOT NULL REFERENCES education_forms(id),
course_count INT NOT NULL DEFAULT 4,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_academic_calendars_course_count CHECK (course_count > 0),
CONSTRAINT uq_academic_calendar_title UNIQUE (academic_year_id, specialty_profile_id, study_form_id, title)
);
CREATE INDEX IF NOT EXISTS idx_academic_calendars_context
ON academic_calendars(academic_year_id, specialty_id, specialty_profile_id, study_form_id);
INSERT INTO academic_calendars (title, academic_year_id, specialty_id, specialty_profile_id, study_form_id, course_count)
SELECT
specialty.specialty_code || ' ' || profile.name || ' ' || study_form.name || ' ' || ay.title,
ay.id,
specialty.id,
profile.id,
study_form.id,
4
FROM academic_years ay
CROSS JOIN education_forms study_form
JOIN specialties specialty ON specialty.specialty_code IN ('09.03.01', '10.03.01')
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
WHERE ay.title = '2025-2026'
ON CONFLICT (academic_year_id, specialty_profile_id, study_form_id, title) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendar_days (
id BIGSERIAL PRIMARY KEY,
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
course_number INT NOT NULL,
date DATE NOT NULL,
week_number INT NOT NULL,
day_of_week INT NOT NULL,
activity_type_id BIGINT NOT NULL REFERENCES academic_calendar_activity_types(id),
CONSTRAINT chk_calendar_days_course_positive CHECK (course_number > 0),
CONSTRAINT chk_calendar_days_week_positive CHECK (week_number > 0),
CONSTRAINT chk_calendar_days_day CHECK (day_of_week BETWEEN 1 AND 7),
CONSTRAINT uq_calendar_days UNIQUE (calendar_id, course_number, date)
);
CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup
ON academic_calendar_days(calendar_id, course_number, date);
INSERT INTO academic_calendar_days (calendar_id, course_number, date, week_number, day_of_week, activity_type_id)
SELECT
calendar.id,
course.course_number,
days.date::DATE,
((days.date::DATE - ay.start_date) / 7) + 1,
EXTRACT(ISODOW FROM days.date::DATE)::INT,
activity.id
FROM academic_calendars calendar
JOIN academic_years ay ON ay.id = calendar.academic_year_id
JOIN academic_calendar_activity_types activity ON activity.code = 'Т'
CROSS JOIN GENERATE_SERIES(1, calendar.course_count) AS course(course_number)
CROSS JOIN GENERATE_SERIES(ay.start_date, ay.end_date, INTERVAL '1 day') AS days(date)
ON CONFLICT (calendar_id, course_number, date) DO NOTHING;
CREATE TABLE IF NOT EXISTS student_group_calendar_assignments (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_group_calendar_year UNIQUE (group_id, academic_year_id)
);
CREATE INDEX IF NOT EXISTS idx_group_calendar_assignments_lookup
ON student_group_calendar_assignments(group_id, academic_year_id);
INSERT INTO student_group_calendar_assignments (group_id, academic_year_id, calendar_id)
SELECT
sg.id,
ay.id,
calendar.id
FROM student_groups sg
JOIN academic_years ay ON ay.title = '2025-2026'
JOIN academic_calendars calendar
ON calendar.academic_year_id = ay.id
AND calendar.specialty_profile_id = sg.specialty_profile_id
WHERE sg.name IN ('ИВТ-21-1', 'ИБ-41м')
ON CONFLICT (group_id, academic_year_id) DO NOTHING;
CREATE TABLE IF NOT EXISTS schedule_rules (
id BIGSERIAL PRIMARY KEY,
subject_id BIGINT NOT NULL REFERENCES subjects(id),
semester_id BIGINT NOT NULL REFERENCES semesters(id) ON DELETE CASCADE,
lecture_academic_hours INT NOT NULL,
laboratory_academic_hours INT NOT NULL,
practice_academic_hours INT NOT NULL,
lecture_start_week INT NOT NULL,
laboratory_start_week INT NOT NULL,
practice_start_week INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
valid_from DATE,
valid_to DATE,
version_group_id BIGINT,
change_reason TEXT,
created_by BIGINT REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_schedule_rules_type_hours_non_negative CHECK (
lecture_academic_hours >= 0
AND laboratory_academic_hours >= 0
AND practice_academic_hours >= 0
),
CONSTRAINT chk_schedule_rules_has_type_hours CHECK (
lecture_academic_hours + laboratory_academic_hours + practice_academic_hours > 0
),
CONSTRAINT chk_schedule_rules_start_weeks_positive CHECK (
lecture_start_week > 0
AND laboratory_start_week > 0
AND practice_start_week > 0
),
CONSTRAINT chk_schedule_rules_dates CHECK (valid_to IS NULL OR valid_from IS NULL OR valid_to >= valid_from)
);
CREATE INDEX IF NOT EXISTS idx_schedule_rules_semester ON schedule_rules(semester_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rules_subject ON schedule_rules(subject_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rules_status_dates
ON schedule_rules(status, valid_from, valid_to);
CREATE TABLE IF NOT EXISTS schedule_rule_groups (
schedule_rule_id BIGINT NOT NULL REFERENCES schedule_rules(id) ON DELETE CASCADE,
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
PRIMARY KEY (schedule_rule_id, group_id)
);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_groups_group ON schedule_rule_groups(group_id);
CREATE TABLE IF NOT EXISTS schedule_rule_slots (
id BIGSERIAL PRIMARY KEY,
schedule_rule_id BIGINT NOT NULL REFERENCES schedule_rules(id) ON DELETE CASCADE,
day_of_week INT NOT NULL,
parity VARCHAR(10) NOT NULL,
time_slot_id BIGINT NOT NULL REFERENCES time_slots(id),
teacher_id BIGINT NOT NULL REFERENCES users(id),
classroom_id BIGINT NOT NULL REFERENCES classrooms(id),
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
lesson_format VARCHAR(30) NOT NULL,
time_locked BOOLEAN NOT NULL DEFAULT FALSE,
classroom_locked BOOLEAN NOT NULL DEFAULT FALSE,
teacher_locked BOOLEAN NOT NULL DEFAULT FALSE,
locked_by BIGINT REFERENCES users(id),
locked_at TIMESTAMP,
lock_comment TEXT,
CONSTRAINT chk_schedule_rule_slots_day CHECK (day_of_week BETWEEN 1 AND 7),
CONSTRAINT chk_schedule_rule_slots_parity CHECK (parity IN ('BOTH', 'EVEN', 'ODD')),
CONSTRAINT chk_schedule_rule_slots_format CHECK (lesson_format IN ('Очно', 'Онлайн'))
);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_rule ON schedule_rule_slots(schedule_rule_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_teacher ON schedule_rule_slots(teacher_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_classroom ON schedule_rule_slots(classroom_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_time ON schedule_rule_slots(day_of_week, parity, time_slot_id);
CREATE TABLE IF NOT EXISTS schedule_rule_slot_subgroups (
schedule_rule_slot_id BIGINT NOT NULL REFERENCES schedule_rule_slots(id) ON DELETE CASCADE,
subgroup_id BIGINT NOT NULL REFERENCES subgroups(id) ON DELETE CASCADE,
PRIMARY KEY (schedule_rule_slot_id, subgroup_id)
);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slot_subgroups_subgroup ON schedule_rule_slot_subgroups(subgroup_id);
CREATE TABLE IF NOT EXISTS schedule_overrides (
id BIGSERIAL PRIMARY KEY,
base_rule_slot_id BIGINT NOT NULL REFERENCES schedule_rule_slots(id),
lesson_date DATE NOT NULL,
action VARCHAR(20) NOT NULL,
new_time_slot_id BIGINT REFERENCES time_slots(id),
new_classroom_id BIGINT REFERENCES classrooms(id),
new_teacher_id BIGINT REFERENCES users(id),
new_lesson_format VARCHAR(30),
comment TEXT,
created_by BIGINT REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_schedule_overrides_action CHECK (action IN ('MOVE', 'CANCEL', 'REPLACE')),
CONSTRAINT uq_schedule_overrides_slot_date UNIQUE (base_rule_slot_id, lesson_date)
);
CREATE INDEX IF NOT EXISTS idx_schedule_overrides_date
ON schedule_overrides(lesson_date);
CREATE INDEX IF NOT EXISTS idx_schedule_overrides_new_teacher
ON schedule_overrides(new_teacher_id);
CREATE OR REPLACE FUNCTION validate_schedule_rule_slot_subgroups()
RETURNS TRIGGER AS $$
DECLARE
lesson_type_name VARCHAR(50);
rule_id BIGINT;
subgroup_group_id BIGINT;
has_group_duplicate BOOLEAN;
BEGIN
SELECT slot.schedule_rule_id, type.name INTO rule_id, lesson_type_name
FROM schedule_rule_slots slot
JOIN lesson_types type ON type.id = slot.lesson_type_id
WHERE slot.id = NEW.schedule_rule_slot_id;
IF lesson_type_name IS NULL OR LOWER(lesson_type_name) NOT LIKE '%лаб%' THEN
RAISE EXCEPTION 'Подгруппы можно выбирать только для лабораторных занятий';
END IF;
SELECT group_id INTO subgroup_group_id
FROM subgroups
WHERE id = NEW.subgroup_id;
IF subgroup_group_id IS NULL THEN
RAISE EXCEPTION 'Подгруппа не найдена';
END IF;
IF NOT EXISTS (
SELECT 1
FROM schedule_rule_groups rule_group
WHERE rule_group.schedule_rule_id = rule_id
AND rule_group.group_id = subgroup_group_id
) THEN
RAISE EXCEPTION 'Подгруппа должна относиться к одной из групп правила';
END IF;
IF TG_OP = 'UPDATE' THEN
SELECT EXISTS (
SELECT 1
FROM schedule_rule_slot_subgroups slot_subgroup
JOIN subgroups subgroup ON subgroup.id = slot_subgroup.subgroup_id
WHERE slot_subgroup.schedule_rule_slot_id = NEW.schedule_rule_slot_id
AND subgroup.group_id = subgroup_group_id
AND NOT (
slot_subgroup.schedule_rule_slot_id = OLD.schedule_rule_slot_id
AND slot_subgroup.subgroup_id = OLD.subgroup_id
)
) INTO has_group_duplicate;
ELSE
SELECT EXISTS (
SELECT 1
FROM schedule_rule_slot_subgroups slot_subgroup
JOIN subgroups subgroup ON subgroup.id = slot_subgroup.subgroup_id
WHERE slot_subgroup.schedule_rule_slot_id = NEW.schedule_rule_slot_id
AND subgroup.group_id = subgroup_group_id
) INTO has_group_duplicate;
END IF;
IF has_group_duplicate THEN
RAISE EXCEPTION 'В одном слоте можно выбрать не больше одной подгруппы каждой группы';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER validate_schedule_rule_slot_subgroups
BEFORE INSERT OR UPDATE ON schedule_rule_slot_subgroups
FOR EACH ROW
EXECUTE FUNCTION validate_schedule_rule_slot_subgroups();
CREATE TEMP TABLE tmp_default_rules AS
SELECT
NEXTVAL(PG_GET_SERIAL_SEQUENCE('schedule_rules', 'id')) AS schedule_rule_id,
rule_key,
subject_id,
semester_id,
lecture_academic_hours,
laboratory_academic_hours,
practice_academic_hours,
lecture_start_week,
laboratory_start_week,
practice_start_week
FROM (
SELECT
data.rule_key,
subj.id AS subject_id,
sem.id AS semester_id,
data.lecture_academic_hours,
data.laboratory_academic_hours,
data.practice_academic_hours,
data.lecture_start_week,
data.laboratory_start_week,
data.practice_start_week
FROM (VALUES
('stream_math', 'Высшая математика', '2025-2026', 'spring', 72, 0, 0, 1, 1, 1),
('ivt_informatics_lab', 'Информатика', '2025-2026', 'spring', 0, 72, 0, 1, 1, 1),
('ivt_databases_practice', 'Базы данных', '2025-2026', 'spring', 0, 0, 72, 1, 1, 1),
('ib_philosophy', 'Философия', '2025-2026', 'spring', 54, 0, 0, 1, 1, 1)
) AS data(
rule_key,
subject_name,
year_title,
semester_type,
lecture_academic_hours,
laboratory_academic_hours,
practice_academic_hours,
lecture_start_week,
laboratory_start_week,
practice_start_week
)
JOIN subjects subj ON subj.name = data.subject_name
JOIN academic_years ay ON ay.title = data.year_title
JOIN semesters sem ON sem.academic_year_id = ay.id AND sem.semester_type = data.semester_type
) prepared_rules;
INSERT INTO schedule_rules (
id,
subject_id,
semester_id,
lecture_academic_hours,
laboratory_academic_hours,
practice_academic_hours,
lecture_start_week,
laboratory_start_week,
practice_start_week
)
SELECT
schedule_rule_id,
subject_id,
semester_id,
lecture_academic_hours,
laboratory_academic_hours,
practice_academic_hours,
lecture_start_week,
laboratory_start_week,
practice_start_week
FROM tmp_default_rules;
INSERT INTO schedule_rule_groups (schedule_rule_id, group_id)
SELECT
rules.schedule_rule_id,
sg.id
FROM (VALUES
('stream_math', 'ИВТ-21-1'),
('stream_math', 'ИБ-41м'),
('ivt_informatics_lab', 'ИВТ-21-1'),
('ivt_databases_practice', 'ИВТ-21-1'),
('ib_philosophy', 'ИБ-41м')
) AS data(rule_key, group_name)
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
JOIN student_groups sg ON sg.name = data.group_name
ON CONFLICT DO NOTHING;
INSERT INTO schedule_rule_slots (
schedule_rule_id,
day_of_week,
parity,
time_slot_id,
teacher_id,
classroom_id,
lesson_type_id,
lesson_format
)
SELECT
rules.schedule_rule_id,
data.day_of_week,
data.parity,
ts.id,
teacher.id,
classroom.id,
lt.id,
data.lesson_format
FROM (VALUES
('stream_math', 1, 'BOTH', 3, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Очно'),
('ivt_informatics_lab', 3, 'BOTH', 1, 'Тестовый преподаватель', '202 IT Lab', 'Лабораторная работа', 'Очно'),
('ivt_databases_practice', 5, 'EVEN', 5, 'Тестовый преподаватель', '303 Обычная', 'Практика', 'Онлайн'),
('ib_philosophy', 2, 'ODD', 2, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Онлайн')
) AS data(rule_key, day_of_week, parity, time_slot_order, teacher_username, classroom_name, lesson_type_name, lesson_format)
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
JOIN time_slot_scopes scope ON scope.code = 'default'
JOIN time_slots ts ON ts.time_slot_scope_id = scope.id AND ts.order_number = data.time_slot_order
JOIN users teacher ON teacher.username = data.teacher_username
JOIN classrooms classroom ON classroom.name = data.classroom_name
JOIN lesson_types lt ON lt.name = data.lesson_type_name;
-- ==========================================
-- Функция обновления timestamp
-- ==========================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_academic_calendars_updated_at
BEFORE UPDATE ON academic_calendars
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ==========================================
-- Комментарии к таблицам и полям (для документации)
-- ==========================================
COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)';
COMMENT ON TABLE departments IS 'Кафедры';
COMMENT ON TABLE specialties IS 'Специальности';
COMMENT ON TABLE specialty_profiles IS 'Профили обучения внутри специальности';
COMMENT ON TABLE time_slot_scopes IS 'Именованные сетки времени: базовая, автоматическая субботняя и ручные сетки';
COMMENT ON TABLE time_slots IS 'Настраиваемая сетка временных слотов занятий';
COMMENT ON TABLE time_slot_date_assignments IS 'Ручное назначение сетки времени на конкретную дату';
COMMENT ON TABLE academic_years IS 'Учебные годы';
COMMENT ON TABLE semesters IS 'Семестры учебного года';
COMMENT ON TABLE academic_calendar_activity_types IS 'Коды активностей календарного учебного графика';
COMMENT ON TABLE academic_calendars IS 'Календарные учебные графики по профилю, форме обучения и учебному году';
COMMENT ON TABLE academic_calendar_days IS 'Дневные ячейки календарного учебного графика';
COMMENT ON TABLE student_group_calendar_assignments IS 'Назначения календарных графиков учебным группам по учебному году';
COMMENT ON TABLE schedule_rules IS 'Правила динамической генерации расписания';
COMMENT ON TABLE schedule_rule_groups IS 'Привязка правил расписания к учебным группам';
COMMENT ON TABLE schedule_rule_slots IS 'Слоты проведения занятий внутри правила расписания';
COMMENT ON TABLE schedule_rule_slot_subgroups IS 'Подгруппы лабораторных слотов правила расписания';
COMMENT ON TABLE teacher_department_assignments IS 'История принадлежности преподавателей к кафедрам';
COMMENT ON TABLE subject_comments IS 'Комментарии кафедр и администраторов к дисциплинам';
COMMENT ON TABLE schedule_overrides IS 'Точечные изменения конкретных сгенерированных пар';
COMMENT ON COLUMN schedule_rules.lecture_academic_hours IS 'Лимит академических часов лекций';
COMMENT ON COLUMN schedule_rules.laboratory_academic_hours IS 'Лимит академических часов лабораторных работ';
COMMENT ON COLUMN schedule_rules.practice_academic_hours IS 'Лимит академических часов практик';
COMMENT ON COLUMN schedule_rules.lecture_start_week IS 'Неделя начала лекций в семестре';
COMMENT ON COLUMN schedule_rules.laboratory_start_week IS 'Неделя начала лабораторных работ в семестре';
COMMENT ON COLUMN schedule_rules.practice_start_week IS 'Неделя начала практик в семестре';
COMMENT ON COLUMN schedule_rules.valid_from IS 'Дата начала действия версии правила расписания';
COMMENT ON COLUMN schedule_rules.valid_to IS 'Дата окончания действия версии правила расписания';
COMMENT ON TABLE education_forms IS 'Формы обучения';
COMMENT ON TABLE subgroups IS 'Подгруппы';
COMMENT ON TABLE lesson_types IS 'Типы занятий';
COMMENT ON TABLE equipments IS 'Оборудование';
COMMENT ON TABLE classrooms IS 'Аудитории';
COMMENT ON TABLE classroom_equipments IS 'Привязка оборудования к аудиториям';
COMMENT ON TABLE teacher_subjects IS 'Привязка преподавателей к дисциплинам';
COMMENT ON TABLE teacher_lesson_types IS 'Типы занятий преподавателя';
COMMENT ON COLUMN users.id IS 'ID пользователя';
COMMENT ON COLUMN users.username IS 'Логин пользователя';
COMMENT ON COLUMN users.password IS 'Хэш пароля пользователя';
COMMENT ON COLUMN users.role IS 'Роль пользователя';
COMMENT ON COLUMN users.status IS 'Жизненный цикл пользователя: ACTIVE или ARCHIVED';
COMMENT ON COLUMN users.created_at IS 'Дата и время создания';
COMMENT ON COLUMN users.updated_at IS 'Дата и время последнего обновления';
COMMENT ON COLUMN users.full_name IS 'ФИО пользователя';
COMMENT ON COLUMN users.job_title IS 'Должность пользователя';
COMMENT ON COLUMN users.department_id IS 'ID кафедры';
COMMENT ON COLUMN education_forms.id IS 'ID формы обучения';
COMMENT ON COLUMN education_forms.name IS 'Название формы обучения';
COMMENT ON COLUMN education_forms.description IS 'Описание';
COMMENT ON COLUMN education_forms.created_at IS 'Дата и время создания';
COMMENT ON COLUMN student_groups.id IS 'ID учебной группы';
COMMENT ON COLUMN student_groups.name IS 'Название группы';
COMMENT ON COLUMN student_groups.group_size IS 'Количество студентов';
COMMENT ON COLUMN student_groups.education_form_id IS 'ID формы обучения, к которой относится группа';
COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
COMMENT ON COLUMN student_groups.specialty_id IS 'ID специальности';
COMMENT ON COLUMN student_groups.specialty_profile_id IS 'ID профиля обучения';
COMMENT ON COLUMN student_groups.year_start_study IS 'Год начала обучения';
COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
COMMENT ON COLUMN subgroups.id IS 'ID подгруппы';
COMMENT ON COLUMN subgroups.group_id IS 'ID учебной группы, к которой относится подгруппа';
COMMENT ON COLUMN subgroups.name IS 'Название подгруппы';
COMMENT ON COLUMN subgroups.student_capacity IS 'Количество студентов в подгруппе';
COMMENT ON COLUMN subjects.id IS 'ID предмета';
COMMENT ON COLUMN subjects.name IS 'Название предмета';
COMMENT ON COLUMN subjects.code IS 'Код предмета';
COMMENT ON COLUMN subjects.department_id IS 'ID кафедры';
COMMENT ON COLUMN subjects.description IS 'Описание предмета';
COMMENT ON COLUMN subjects.created_at IS 'Дата и время создания';
COMMENT ON COLUMN lesson_types.id IS 'ID урока';
COMMENT ON COLUMN lesson_types.name IS 'Название типа урока';
COMMENT ON COLUMN lesson_types.color_code IS 'Цветовой код для типа урока';
COMMENT ON COLUMN lesson_types.duration_minutes IS 'Длительность урока в минутах';
COMMENT ON COLUMN equipments.id IS 'ID оборудования';
COMMENT ON COLUMN equipments.name IS 'Название оборудования';
COMMENT ON COLUMN equipments.description IS 'Описание оборудования';
COMMENT ON COLUMN equipments.inventory_number IS 'Инвентарный номер оборудования';
COMMENT ON COLUMN classrooms.id IS 'ID аудитории';
COMMENT ON COLUMN classrooms.name IS 'Название аудитории';
COMMENT ON COLUMN classrooms.capacity IS 'Вместимость аудитории';
COMMENT ON COLUMN classrooms.building IS 'Корпус';
COMMENT ON COLUMN classrooms.floor IS 'Этаж';
COMMENT ON COLUMN classrooms.is_available IS 'Доступность аудитории';
COMMENT ON COLUMN classrooms.description IS 'Описание аудитории';
COMMENT ON COLUMN classrooms.created_at IS 'Дата и время создания';
COMMENT ON COLUMN classrooms.status IS 'Жизненный цикл аудитории: ACTIVE или ARCHIVED';
COMMENT ON COLUMN classroom_equipments.classroom_id IS 'ID аудитории';
COMMENT ON COLUMN classroom_equipments.equipment_id IS 'ID оборудования';
COMMENT ON COLUMN classroom_equipments.quantity IS 'Количество единиц оборудования';
COMMENT ON COLUMN classroom_equipments.notes IS 'Примечания к записи';
COMMENT ON COLUMN teacher_subjects.user_id IS 'ID преподавателя';
COMMENT ON COLUMN teacher_subjects.subject_id IS 'ID предмета';
COMMENT ON COLUMN teacher_subjects.qualification_level IS 'Уровень квалификации преподавателя';
COMMENT ON COLUMN teacher_subjects.experience_years IS 'Опыт преподавания';
COMMENT ON COLUMN departments.id IS 'ID кафедры';
COMMENT ON COLUMN departments.name IS 'Название кафедры';
COMMENT ON COLUMN departments.code IS 'Код кафедры';
COMMENT ON COLUMN specialties.id IS 'ID специальности';
COMMENT ON COLUMN specialties.name IS 'Название специальности';
COMMENT ON COLUMN specialties.specialty_code IS 'Код специальности';
COMMENT ON COLUMN specialty_profiles.id IS 'ID профиля обучения';
COMMENT ON COLUMN specialty_profiles.specialty_id IS 'ID специальности';
COMMENT ON COLUMN specialty_profiles.name IS 'Название профиля обучения';
COMMENT ON COLUMN specialty_profiles.description IS 'Описание профиля обучения';
COMMENT ON COLUMN academic_calendar_activity_types.code IS 'Код активности из календарного графика';
COMMENT ON COLUMN academic_calendar_activity_types.name IS 'Название активности';
COMMENT ON COLUMN academic_calendar_activity_types.allow_schedule IS 'Разрешает ли код генерацию обычного расписания';
COMMENT ON COLUMN academic_calendars.title IS 'Название календарного учебного графика';
COMMENT ON COLUMN academic_calendars.academic_year_id IS 'ID учебного года';
COMMENT ON COLUMN academic_calendars.specialty_id IS 'ID специальности';
COMMENT ON COLUMN academic_calendars.specialty_profile_id IS 'ID профиля обучения';
COMMENT ON COLUMN academic_calendars.study_form_id IS 'ID формы обучения из education_forms';
COMMENT ON COLUMN academic_calendars.course_count IS 'Количество курсов в графике';
COMMENT ON COLUMN academic_calendar_days.calendar_id IS 'ID календарного учебного графика';
COMMENT ON COLUMN academic_calendar_days.course_number IS 'Номер курса';
COMMENT ON COLUMN academic_calendar_days.date IS 'Дата';
COMMENT ON COLUMN academic_calendar_days.week_number IS 'Номер недели в учебном году';
COMMENT ON COLUMN academic_calendar_days.day_of_week IS 'День недели ISO';
COMMENT ON COLUMN academic_calendar_days.activity_type_id IS 'ID кода активности';
COMMENT ON COLUMN student_group_calendar_assignments.group_id IS 'ID учебной группы';
COMMENT ON COLUMN student_group_calendar_assignments.academic_year_id IS 'ID учебного года';
COMMENT ON COLUMN student_group_calendar_assignments.calendar_id IS 'ID назначенного календарного графика';
COMMENT ON COLUMN teacher_lesson_types.user_id IS 'ID преподавателя';
COMMENT ON COLUMN teacher_lesson_types.subject_id IS 'ID предмета';
COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';