Skip to content

Commit 8d2b210

Browse files
strehleCopilot
andauthored
Prevent issue with SPRING_JDBC in Flyway (#3949)
* Prevent issue with SPRING_JDBC in Flyway * Guard SPRING_JDBC normalization and add tests * Apply suggestions from code review log again the exception as copilot want, but now we have a check if inital setup or not Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent e56cf4e commit 8d2b210

2 files changed

Lines changed: 118 additions & 4 deletions

File tree

server/src/main/java/org/cloudfoundry/identity/uaa/db/beans/FlywayConfiguration.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cloudfoundry.identity.uaa.db.beans;
22

3+
import lombok.extern.slf4j.Slf4j;
34
import org.cloudfoundry.identity.uaa.db.FixFailedBackportMigrations_4_0_4;
45
import org.flywaydb.core.Flyway;
56
import org.springframework.context.ApplicationContext;
@@ -11,8 +12,14 @@
1112
import org.springframework.core.type.AnnotatedTypeMetadata;
1213

1314
import javax.sql.DataSource;
15+
import java.sql.Connection;
16+
import java.sql.DatabaseMetaData;
17+
import java.sql.ResultSet;
18+
import java.sql.SQLException;
19+
import java.sql.Statement;
1420

1521
@Configuration
22+
@Slf4j
1623
public class FlywayConfiguration {
1724

1825
/**
@@ -56,13 +63,50 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)
5663

5764
@Bean
5865
public Flyway flyway(Flyway baseFlyway) {
59-
baseFlyway.repair();
60-
baseFlyway.migrate();
6166
org.apache.tomcat.jdbc.pool.DataSource ds =
6267
(org.apache.tomcat.jdbc.pool.DataSource) baseFlyway.getConfiguration().getDataSource();
68+
updateSpringJdbcMigrationTypes(ds);
69+
baseFlyway.repair();
70+
baseFlyway.migrate();
6371
ds.purge();
6472
return baseFlyway;
6573
}
74+
75+
/**
76+
* Normalizes legacy Flyway schema history rows by rewriting the migration {@code type}
77+
* from {@code SPRING_JDBC} to {@code JDBC}. This avoids the startup failure
78+
* "Unknown migration type 'SPRING_JDBC' found in schema history" before {@code repair()}/{@code migrate()}.
79+
* <p>
80+
* The update is only executed when the version table already exists; on a fresh install
81+
* there is nothing to normalize. Failures are logged but otherwise intentionally ignored.
82+
*/
83+
static void updateSpringJdbcMigrationTypes(DataSource ds) {
84+
try (Connection conn = ds.getConnection()) {
85+
if (!versionTableExists(conn)) {
86+
return;
87+
}
88+
try (Statement stmt = conn.createStatement()) {
89+
stmt.executeUpdate("UPDATE %s SET type = 'JDBC' WHERE type = 'SPRING_JDBC'".formatted(VERSION_TABLE));
90+
if (!conn.getAutoCommit()) {
91+
conn.commit();
92+
}
93+
}
94+
} catch (SQLException e) {
95+
log.warn("Failed to normalize SPRING_JDBC migration types in {}", VERSION_TABLE, e);
96+
}
97+
}
98+
99+
private static boolean versionTableExists(Connection conn) throws SQLException {
100+
DatabaseMetaData metaData = conn.getMetaData();
101+
for (String tableName : new String[]{VERSION_TABLE, VERSION_TABLE.toUpperCase()}) {
102+
try (ResultSet tables = metaData.getTables(conn.getCatalog(), null, tableName, new String[]{"TABLE"})) {
103+
if (tables.next()) {
104+
return true;
105+
}
106+
}
107+
}
108+
return false;
109+
}
66110
}
67111

68112
@Configuration

server/src/test/java/org/cloudfoundry/identity/uaa/db/beans/FlywayConfigurationTest.java

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package org.cloudfoundry.identity.uaa.db.beans;
22

3+
import org.cloudfoundry.identity.uaa.db.beans.FlywayConfiguration.FlywayConfigurationWithMigration;
34
import org.cloudfoundry.identity.uaa.db.beans.FlywayConfiguration.FlywayConfigurationWithMigration.ConfiguredWithMigrations;
45
import org.cloudfoundry.identity.uaa.db.beans.FlywayConfiguration.FlywayConfigurationWithoutMigrations.ConfiguredWithoutMigrations;
6+
import org.junit.jupiter.api.AfterEach;
57
import org.junit.jupiter.api.BeforeEach;
68
import org.junit.jupiter.api.Test;
79
import org.junit.jupiter.api.extension.ExtendWith;
810
import org.mockito.Mock;
911
import org.mockito.junit.jupiter.MockitoExtension;
1012
import org.springframework.context.annotation.ConditionContext;
13+
import org.springframework.jdbc.datasource.DriverManagerDataSource;
1114
import org.springframework.mock.env.MockEnvironment;
1215

16+
import javax.sql.DataSource;
17+
import java.sql.Connection;
18+
import java.sql.ResultSet;
19+
import java.sql.SQLException;
20+
import java.sql.Statement;
21+
import java.util.UUID;
22+
1323
import static org.assertj.core.api.Assertions.assertThat;
14-
import static org.mockito.Mockito.when;
24+
import static org.mockito.Mockito.lenient;
1525

1626
@ExtendWith(MockitoExtension.class)
1727
class FlywayConfigurationTest {
@@ -28,7 +38,7 @@ class FlywayConfigurationTest {
2838
@BeforeEach
2939
void setUp() {
3040
mockEnvironment = new MockEnvironment();
31-
when(mockConditionContext.getEnvironment()).thenReturn(mockEnvironment);
41+
lenient().when(mockConditionContext.getEnvironment()).thenReturn(mockEnvironment);
3242
configuredWithMigrations = new ConfiguredWithMigrations();
3343
configuredWithoutMigrations = new ConfiguredWithoutMigrations();
3444
}
@@ -62,4 +72,64 @@ void flywayConfiguration_RunsMigration_WhenInvalidConfiguration() {
6272
assertThat(configuredWithMigrations.matches(mockConditionContext, null)).isTrue();
6373
assertThat(configuredWithoutMigrations.matches(mockConditionContext, null)).isFalse();
6474
}
75+
76+
private DataSource dataSource;
77+
78+
@AfterEach
79+
void tearDown() throws SQLException {
80+
if (dataSource != null) {
81+
try (Connection conn = dataSource.getConnection();
82+
Statement stmt = conn.createStatement()) {
83+
stmt.execute("SHUTDOWN");
84+
}
85+
}
86+
}
87+
88+
private DataSource inMemoryDataSource() {
89+
DriverManagerDataSource ds = new DriverManagerDataSource();
90+
ds.setDriverClassName("org.hsqldb.jdbc.JDBCDriver");
91+
ds.setUrl("jdbc:hsqldb:mem:" + UUID.randomUUID());
92+
ds.setUsername("sa");
93+
ds.setPassword("");
94+
return ds;
95+
}
96+
97+
private void createSchemaVersionTable(DataSource ds) throws SQLException {
98+
try (Connection conn = ds.getConnection();
99+
Statement stmt = conn.createStatement()) {
100+
stmt.execute("CREATE TABLE %s (installed_rank INT, type VARCHAR(20))".formatted(FlywayConfiguration.VERSION_TABLE));
101+
}
102+
}
103+
104+
private String typeForRank(DataSource ds, int rank) throws SQLException {
105+
try (Connection conn = ds.getConnection();
106+
Statement stmt = conn.createStatement();
107+
ResultSet rs = stmt.executeQuery("SELECT type FROM %s WHERE installed_rank = %d".formatted(FlywayConfiguration.VERSION_TABLE, rank))) {
108+
return rs.next() ? rs.getString("type") : null;
109+
}
110+
}
111+
112+
@Test
113+
void updateSpringJdbcMigrationTypes_RewritesSpringJdbcToJdbc() throws SQLException {
114+
dataSource = inMemoryDataSource();
115+
createSchemaVersionTable(dataSource);
116+
try (Connection conn = dataSource.getConnection();
117+
Statement stmt = conn.createStatement()) {
118+
stmt.execute("INSERT INTO %s (installed_rank, type) VALUES (1, 'SPRING_JDBC')".formatted(FlywayConfiguration.VERSION_TABLE));
119+
stmt.execute("INSERT INTO %s (installed_rank, type) VALUES (2, 'SQL')".formatted(FlywayConfiguration.VERSION_TABLE));
120+
}
121+
122+
FlywayConfigurationWithMigration.updateSpringJdbcMigrationTypes(dataSource);
123+
124+
assertThat(typeForRank(dataSource, 1)).isEqualTo("JDBC");
125+
assertThat(typeForRank(dataSource, 2)).isEqualTo("SQL");
126+
}
127+
128+
@Test
129+
void updateSpringJdbcMigrationTypes_DoesNotThrow_WhenVersionTableMissing() {
130+
dataSource = inMemoryDataSource();
131+
132+
org.assertj.core.api.Assertions.assertThatCode(() -> FlywayConfigurationWithMigration.updateSpringJdbcMigrationTypes(dataSource))
133+
.doesNotThrowAnyException();
134+
}
65135
}

0 commit comments

Comments
 (0)