fix(progress): перенести профильную миграцию на V13 после аттестаций

This commit is contained in:
2026-10-27 13:29:50 +03:00
parent 07a223b759
commit cbd98f55f0
8 changed files with 298 additions and 22 deletions

View File

@@ -1,7 +0,0 @@
-- Автор комментария и последнего изменения оценки — для профиля студента курса.
-- Старые записи остаются без автора и отображаются как «автор не указан».
ALTER TABLE grade_comments ADD COLUMN author_account_id uuid;
ALTER TABLE lab_grades ADD COLUMN updated_by_account_id uuid;
-- Комментарии уже индексированы grade_comments_grade_idx (student_id, lab_id).
CREATE INDEX lab_grades_lab_idx ON lab_grades (lab_id);

View File

@@ -0,0 +1,12 @@
-- V13: миграция профиля студента курса.
-- Номер V13, потому что V11 (attestations) и V12 (attestation_semester_and_publication)
-- уже существуют в master и применены в production; их порядок нельзя нарушать,
-- а V10 не может быть вставлена перед ними.
-- Автор комментария и последнего изменения оценки — для профиля студента курса.
-- Старые записи остаются без автора и отображаются как «автор не указан».
-- IF NOT EXISTS: безопасно, если старая feature-миграция V10 уже была применена вручную/dev.
ALTER TABLE grade_comments ADD COLUMN IF NOT EXISTS author_account_id uuid;
ALTER TABLE lab_grades ADD COLUMN IF NOT EXISTS updated_by_account_id uuid;
-- Комментарии уже индексированы grade_comments_grade_idx (student_id, lab_id).
CREATE INDEX IF NOT EXISTS lab_grades_lab_idx ON lab_grades (lab_id);

View File

@@ -0,0 +1,115 @@
package ru.ysu.lms.progress.migration
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.jdbc.datasource.init.ScriptUtils
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.postgresql.PostgreSQLContainer
import java.sql.PreparedStatement
import java.sql.Statement
import javax.sql.DataSource
/**
* V13 идемпотентна: повторный запуск не должен падать, а колонки и индекс
* остаются в ожидаемом состоянии. Реальная PostgreSQL 18 (Testcontainers).
*/
@Testcontainers
@SpringBootTest
class ProfileAuthorsMigrationTest {
companion object {
@Container
@ServiceConnection
@JvmStatic
val postgres = PostgreSQLContainer("postgres:18")
private const val COLUMN_QUERY = """
SELECT data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'progress'
AND table_name = ?
AND column_name = ?
"""
private const val INDEX_QUERY = """
SELECT i.indisvalid, pg_get_indexdef(i.indexrelid) AS definition
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'progress'
AND c.relname = 'lab_grades_lab_idx'
"""
}
@Autowired
private lateinit var dataSource: DataSource
@Test
fun `V13 profile authors migration is idempotent`() {
val migration = ClassPathResource("db/migration/V13__profile_authors.sql")
repeat(2) { applyMigration(migration) }
assertThat(column("grade_comments", "author_account_id"))
.isEqualTo("uuid" to "YES")
assertThat(column("lab_grades", "updated_by_account_id"))
.isEqualTo("uuid" to "YES")
val index = labIndex()
assertThat(index).isNotNull
assertThat(index!!.first).isTrue()
assertThat(index.second).contains("lab_grades").contains("lab_id")
}
private fun applyMigration(migration: Resource) {
dataSource.connection.use { connection ->
// Flyway работает в схеме progress; сырой JDBC-коннект её не видит.
connection.createStatement().use { statement ->
statement.execute("SET search_path TO progress, public")
}
ScriptUtils.executeSqlScript(connection, migration)
}
}
private fun column(
table: String,
column: String,
): Pair<String, String>? =
dataSource.connection.use { connection ->
connection.prepareStatement(COLUMN_QUERY.trimIndent()).use { statement ->
statement.setString(1, table)
statement.setString(2, column)
readColumn(statement)
}
}
private fun readColumn(statement: PreparedStatement): Pair<String, String>? =
statement.executeQuery().use { rows ->
if (rows.next()) {
rows.getString("data_type") to rows.getString("is_nullable")
} else {
null
}
}
private fun labIndex(): Pair<Boolean, String>? =
dataSource.connection.use { connection ->
connection.createStatement().use { statement ->
readIndex(statement)
}
}
private fun readIndex(statement: Statement): Pair<Boolean, String>? =
statement.executeQuery(INDEX_QUERY.trimIndent()).use { rows ->
if (rows.next()) {
rows.getBoolean("indisvalid") to rows.getString("definition")
} else {
null
}
}
}