Подготовить безопасный production rollout
This commit is contained in:
@@ -731,12 +731,31 @@ CREATE TABLE IF NOT EXISTS academic_calendar_days (
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup
|
||||
ON academic_calendar_days(calendar_id, course_number, date);
|
||||
|
||||
CREATE OR REPLACE FUNCTION calculate_academic_calendar_week_number(
|
||||
academic_year_start DATE,
|
||||
calendar_date DATE
|
||||
)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $$
|
||||
SELECT (
|
||||
(
|
||||
calendar_date
|
||||
- academic_year_start
|
||||
+ EXTRACT(ISODOW FROM academic_year_start)::INTEGER
|
||||
- 1
|
||||
) / 7
|
||||
) + 1
|
||||
$$;
|
||||
|
||||
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,
|
||||
calculate_academic_calendar_week_number(ay.start_date, days.date::DATE),
|
||||
EXTRACT(ISODOW FROM days.date::DATE)::INT,
|
||||
activity.id
|
||||
FROM academic_calendars calendar
|
||||
@@ -1262,6 +1281,8 @@ 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 FUNCTION calculate_academic_calendar_week_number(DATE, DATE)
|
||||
IS 'Номер недели календарного учебного графика по периодам понедельник–воскресенье';
|
||||
|
||||
COMMENT ON COLUMN academic_calendar_subjects.calendar_id IS 'ID календарного учебного графика';
|
||||
COMMENT ON COLUMN academic_calendar_subjects.semester_number IS 'Номер учебного семестра внутри графика: 1, 2, 3 ...';
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
-- Недели календарного учебного графика выровнены по ISO-неделе:
|
||||
-- понедельник — первый день, воскресенье — последний.
|
||||
UPDATE academic_calendar_days AS calendar_day
|
||||
SET week_number = (
|
||||
(
|
||||
calendar_day.date
|
||||
- academic_year.start_date
|
||||
+ EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER
|
||||
- 1
|
||||
) / 7
|
||||
) + 1
|
||||
FROM academic_calendars AS calendar
|
||||
JOIN academic_years AS academic_year
|
||||
ON academic_year.id = calendar.academic_year_id
|
||||
WHERE calendar_day.calendar_id = calendar.id
|
||||
AND calendar_day.week_number IS DISTINCT FROM (
|
||||
(
|
||||
calendar_day.date
|
||||
- academic_year.start_date
|
||||
+ EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER
|
||||
- 1
|
||||
) / 7
|
||||
) + 1;
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.magistr.app.migration;
|
||||
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@DisplayName("Baseline нумерации недель календарного графика")
|
||||
class AcademicCalendarWeekNumberBaselineIntegrationTest {
|
||||
|
||||
@Container
|
||||
static final PostgreSQLContainer<?> POSTGRES =
|
||||
new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES);
|
||||
|
||||
@Test
|
||||
@DisplayName("V1 считает недели по периодам понедельник–воскресенье")
|
||||
void baselineAlignsWeeksToMonday() throws SQLException {
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
|
||||
flyway.clean();
|
||||
flyway.migrate();
|
||||
|
||||
assertThat(flyway.info().current()).isNotNull();
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("1");
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 1)))
|
||||
.isEqualTo(1);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 6)))
|
||||
.isEqualTo(1);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 7)))
|
||||
.isEqualTo(2);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2027, 9, 1), LocalDate.of(2027, 9, 6)))
|
||||
.isEqualTo(2);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2025, 9, 1), LocalDate.of(2025, 9, 8)))
|
||||
.isEqualTo(2);
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateWeek(Connection connection,
|
||||
LocalDate academicYearStart,
|
||||
LocalDate calendarDate) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT calculate_academic_calendar_week_number(?, ?)"
|
||||
)) {
|
||||
statement.setDate(1, Date.valueOf(academicYearStart));
|
||||
statement.setDate(2, Date.valueOf(calendarDate));
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package com.magistr.app.migration;
|
||||
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.MigrationVersion;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@DisplayName("Миграция нумерации недель календарного графика")
|
||||
class AcademicCalendarWeekNumberMigrationIntegrationTest {
|
||||
|
||||
@Container
|
||||
static final PostgreSQLContainer<?> POSTGRES =
|
||||
new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES);
|
||||
|
||||
@Test
|
||||
@DisplayName("V2 переносит первый понедельник после неполной недели в неделю 2")
|
||||
void migrationAlignsWeeksToMondayFor2026And2027() throws SQLException {
|
||||
Flyway baseline = flyway("1");
|
||||
baseline.clean();
|
||||
baseline.migrate();
|
||||
|
||||
long calendar2026;
|
||||
long calendar2027;
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
SeedReferences references = loadSeedReferences(connection);
|
||||
|
||||
long year2026 = insertAcademicYear(
|
||||
connection,
|
||||
"2026-2027",
|
||||
LocalDate.of(2026, 9, 1),
|
||||
LocalDate.of(2027, 6, 30)
|
||||
);
|
||||
calendar2026 = insertCalendar(connection, year2026, references, "График 2026-2027");
|
||||
insertCalendarDay(connection, calendar2026, references.activityTypeId(),
|
||||
LocalDate.of(2026, 9, 1), 1, 2);
|
||||
insertCalendarDay(connection, calendar2026, references.activityTypeId(),
|
||||
LocalDate.of(2026, 9, 7), 1, 1);
|
||||
|
||||
long year2027 = insertAcademicYear(
|
||||
connection,
|
||||
"2027-2028",
|
||||
LocalDate.of(2027, 9, 1),
|
||||
LocalDate.of(2028, 6, 30)
|
||||
);
|
||||
calendar2027 = insertCalendar(connection, year2027, references, "График 2027-2028");
|
||||
insertCalendarDay(connection, calendar2027, references.activityTypeId(),
|
||||
LocalDate.of(2027, 9, 1), 1, 3);
|
||||
insertCalendarDay(connection, calendar2027, references.activityTypeId(),
|
||||
LocalDate.of(2027, 9, 6), 1, 1);
|
||||
}
|
||||
|
||||
Flyway latest = latestFlyway();
|
||||
latest.migrate();
|
||||
|
||||
assertThat(latest.info().current()).isNotNull();
|
||||
assertThat(latest.info().current().getVersion().getVersion()).isEqualTo("2");
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 1))).isEqualTo(1);
|
||||
assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 7))).isEqualTo(2);
|
||||
assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 1))).isEqualTo(1);
|
||||
assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 6))).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
|
||||
private Flyway flyway(String targetVersion) {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion(targetVersion))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
|
||||
private Flyway latestFlyway() {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
|
||||
private SeedReferences loadSeedReferences(Connection connection) throws SQLException {
|
||||
long specialtyId = queryLong(connection, "SELECT id FROM specialties ORDER BY id LIMIT 1");
|
||||
long profileId = queryLong(
|
||||
connection,
|
||||
"SELECT id FROM specialty_profiles WHERE specialty_id = ? ORDER BY id LIMIT 1",
|
||||
specialtyId
|
||||
);
|
||||
long studyFormId = queryLong(connection, "SELECT id FROM education_forms ORDER BY id LIMIT 1");
|
||||
long activityTypeId = queryLong(
|
||||
connection,
|
||||
"SELECT id FROM academic_calendar_activity_types WHERE code = 'Т'"
|
||||
);
|
||||
return new SeedReferences(specialtyId, profileId, studyFormId, activityTypeId);
|
||||
}
|
||||
|
||||
private long insertAcademicYear(Connection connection,
|
||||
String title,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO academic_years (title, start_date, end_date)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id
|
||||
""")) {
|
||||
statement.setString(1, title);
|
||||
statement.setDate(2, Date.valueOf(startDate));
|
||||
statement.setDate(3, Date.valueOf(endDate));
|
||||
return requiredLong(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private long insertCalendar(Connection connection,
|
||||
long academicYearId,
|
||||
SeedReferences references,
|
||||
String title) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO academic_calendars (
|
||||
title,
|
||||
academic_year_id,
|
||||
specialty_id,
|
||||
specialty_profile_id,
|
||||
study_form_id,
|
||||
course_count
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
RETURNING id
|
||||
""")) {
|
||||
statement.setString(1, title);
|
||||
statement.setLong(2, academicYearId);
|
||||
statement.setLong(3, references.specialtyId());
|
||||
statement.setLong(4, references.profileId());
|
||||
statement.setLong(5, references.studyFormId());
|
||||
return requiredLong(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertCalendarDay(Connection connection,
|
||||
long calendarId,
|
||||
long activityTypeId,
|
||||
LocalDate date,
|
||||
int weekNumber,
|
||||
int dayOfWeek) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO academic_calendar_days (
|
||||
calendar_id,
|
||||
course_number,
|
||||
date,
|
||||
week_number,
|
||||
day_of_week,
|
||||
activity_type_id
|
||||
)
|
||||
VALUES (?, 1, ?, ?, ?, ?)
|
||||
""")) {
|
||||
statement.setLong(1, calendarId);
|
||||
statement.setDate(2, Date.valueOf(date));
|
||||
statement.setInt(3, weekNumber);
|
||||
statement.setInt(4, dayOfWeek);
|
||||
statement.setLong(5, activityTypeId);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private int findWeekNumber(Connection connection,
|
||||
long calendarId,
|
||||
LocalDate date) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT week_number
|
||||
FROM academic_calendar_days
|
||||
WHERE calendar_id = ?
|
||||
AND date = ?
|
||||
""")) {
|
||||
statement.setLong(1, calendarId);
|
||||
statement.setDate(2, Date.valueOf(date));
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long queryLong(Connection connection, String sql, Object... arguments) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
for (int index = 0; index < arguments.length; index++) {
|
||||
statement.setObject(index + 1, arguments[index]);
|
||||
}
|
||||
return requiredLong(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private long requiredLong(PreparedStatement statement) throws SQLException {
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
return resultSet.getLong(1);
|
||||
}
|
||||
}
|
||||
|
||||
private record SeedReferences(
|
||||
long specialtyId,
|
||||
long profileId,
|
||||
long studyFormId,
|
||||
long activityTypeId
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user