diff --git a/src/main/resources/db/migration/R__ensure_notification_fcm_columns.sql b/src/main/resources/db/migration/R__ensure_notification_fcm_columns.sql new file mode 100644 index 000000000..f635592f9 --- /dev/null +++ b/src/main/resources/db/migration/R__ensure_notification_fcm_columns.sql @@ -0,0 +1,38 @@ +SET @notification_fcm_add_clauses = ( + SELECT GROUP_CONCAT(required.column_ddl ORDER BY required.ordinal SEPARATOR ', ') + FROM ( + SELECT + 1 AS ordinal, + 'is_push_success' AS column_name, + 'ADD COLUMN is_push_success TINYINT(1) NULL COMMENT ''FCM 전송 성공 여부''' AS column_ddl + UNION ALL + SELECT + 2, + 'fcm_error_code', + 'ADD COLUMN fcm_error_code VARCHAR(100) NULL COMMENT ''FCM 에러 코드''' + UNION ALL + SELECT + 3, + 'fcm_messaging_error_code', + 'ADD COLUMN fcm_messaging_error_code VARCHAR(100) NULL COMMENT ''FCM 메시징 에러 코드''' + ) AS required + LEFT JOIN information_schema.COLUMNS AS existing + ON existing.TABLE_SCHEMA = DATABASE() + AND existing.TABLE_NAME = 'notification' + AND existing.COLUMN_NAME = required.column_name + WHERE existing.COLUMN_NAME IS NULL +); + +SET @notification_fcm_ddl = IF( + @notification_fcm_add_clauses IS NULL, + 'DO 0', + CONCAT( + 'ALTER TABLE notification ', + @notification_fcm_add_clauses, + ', ALGORITHM=INSTANT' + ) +); + +PREPARE notification_fcm_stmt FROM @notification_fcm_ddl; +EXECUTE notification_fcm_stmt; +DEALLOCATE PREPARE notification_fcm_stmt; diff --git a/src/test/java/in/koreatech/koin/acceptance/migration/NotificationFcmMigrationTest.java b/src/test/java/in/koreatech/koin/acceptance/migration/NotificationFcmMigrationTest.java new file mode 100644 index 000000000..a0783c65a --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/migration/NotificationFcmMigrationTest.java @@ -0,0 +1,105 @@ +package in.koreatech.koin.acceptance.migration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MySQLContainer; + +class NotificationFcmMigrationTest { + + @Test + void 운영_baseline_누락_스키마와_신규_스키마에서_FCM_컬럼을_보장한다() throws SQLException { + try (MySQLContainer mysql = new MySQLContainer<>("mysql:8.0.29") + .withDatabaseName("notification_fcm_migration") + .withUsername("test") + .withPassword("test")) { + mysql.start(); + + createProductionLikeNotificationTable(mysql); + + Flyway baselineRepairFlyway = flywayConfiguration(mysql) + .target("1") + .load(); + baselineRepairFlyway.migrate(); + baselineRepairFlyway.migrate(); + + assertFcmColumnsAndHistory(mysql, "BASELINE"); + + baselineRepairFlyway.clean(); + + Flyway fullFlyway = flywayConfiguration(mysql).load(); + fullFlyway.migrate(); + fullFlyway.migrate(); + + assertFcmColumnsAndHistory(mysql, "SQL"); + } + } + + private FluentConfiguration flywayConfiguration(MySQLContainer mysql) { + return Flyway.configure() + .dataSource(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()) + .locations("classpath:db/migration") + .baselineOnMigrate(true) + .cleanDisabled(false); + } + + private void createProductionLikeNotificationTable(MySQLContainer mysql) throws SQLException { + try (Connection connection = connect(mysql); Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE notification (id BIGINT NOT NULL PRIMARY KEY) ENGINE=InnoDB"); + } + } + + private void assertFcmColumnsAndHistory(MySQLContainer mysql, String versionOneType) throws SQLException { + try (Connection connection = connect(mysql)) { + assertThat(queryInt(connection, """ + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'notification' + AND ( + (COLUMN_NAME = 'is_push_success' + AND COLUMN_TYPE = 'tinyint(1)' + AND IS_NULLABLE = 'YES') + OR (COLUMN_NAME IN ('fcm_error_code', 'fcm_messaging_error_code') + AND DATA_TYPE = 'varchar' + AND CHARACTER_MAXIMUM_LENGTH = 100 + AND IS_NULLABLE = 'YES') + ) + """)).isEqualTo(3); + assertThat(queryInt(connection, """ + SELECT COUNT(*) + FROM flyway_schema_history + WHERE version = '1' + AND type = '%s' + AND success = 1 + """.formatted(versionOneType))).isEqualTo(1); + assertThat(queryInt(connection, """ + SELECT COUNT(*) + FROM flyway_schema_history + WHERE version IS NULL + AND type = 'SQL' + AND script = 'R__ensure_notification_fcm_columns.sql' + AND success = 1 + """)).isEqualTo(1); + } + } + + private Connection connect(MySQLContainer mysql) throws SQLException { + return DriverManager.getConnection(mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword()); + } + + private int queryInt(Connection connection, String query) throws SQLException { + try (Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery(query)) { + result.next(); + return result.getInt(1); + } + } +}