Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.sql.DataSource;

import org.apache.logging.log4j.LogManager;
Expand All @@ -20,52 +21,73 @@ public class DatabaseMigration {

private static final Logger LOGGER = (Logger) LogManager.getLogger(DatabaseMigration.class);
private static final String DATABASE_TARGET_SERVICE_KEY = "DATABASE_TARGET_SERVICE_KEY";
private static final String TARGET_DATABASE_SERVICE_NAME = "deploy-service-database";

private static final List<String> SEQUENCES_TO_MIGRATE = List.of("configuration_entry_sequence",
"configuration_subscription_sequence",
"backup_descriptor_sequence");

private static final List<String> TABLES_TO_MIGRATE = List.of("configuration_registry",
"configuration_subscription",
"backup_descriptor");

public static void main(String[] args) {
configureLogger();
DatabaseServiceKey databaseServiceKey = getServiceKeyFromEnvironment();
DataSourceEnvironmentExtractor environmentExtractor = new DataSourceEnvironmentExtractor();
DataSource targetDataSource = environmentExtractor.extractDataSource("deploy-service-database");
DataSource sourceDataSource = environmentExtractor.extractDataSource(databaseServiceKey);

DatabaseSequenceMigrationExecutor sequenceMigrationExecutor = ImmutableDatabaseSequenceMigrationExecutor.builder()
.sourceDataSource(
sourceDataSource)
.targetDataSource(
targetDataSource)
.build();

DatabaseTableMigrationExecutor tableMigrationExecutor = ImmutableDatabaseTableMigrationExecutor.builder()
.sourceDataSource(sourceDataSource)
.targetDataSource(targetDataSource)
.build();
sequenceMigrationExecutor.executeMigration("configuration_entry_sequence");
sequenceMigrationExecutor.executeMigration("configuration_subscription_sequence");
tableMigrationExecutor.executeMigration("configuration_registry");
tableMigrationExecutor.executeMigration("configuration_subscription");

DataSource sourceDataSource = extractSourceDataSource();
DataSource targetDataSource = extractTargetDataSource();

migrateSequences(sourceDataSource, targetDataSource);
migrateTables(sourceDataSource, targetDataSource);

LOGGER.info("Database migration completed.");
}

private static DataSource extractSourceDataSource() {
DatabaseServiceKey serviceKey = getServiceKeyFromEnvironment();
return new DataSourceEnvironmentExtractor().extractDataSource(serviceKey);
}

private static DataSource extractTargetDataSource() {
return new DataSourceEnvironmentExtractor().extractDataSource(TARGET_DATABASE_SERVICE_NAME);
}

private static void migrateSequences(DataSource sourceDataSource, DataSource targetDataSource) {
DatabaseSequenceMigrationExecutor executor = ImmutableDatabaseSequenceMigrationExecutor.builder()
.sourceDataSource(sourceDataSource)
.targetDataSource(targetDataSource)
.build();
SEQUENCES_TO_MIGRATE.forEach(executor::executeMigration);
}

private static void migrateTables(DataSource sourceDataSource, DataSource targetDataSource) {
DatabaseTableMigrationExecutor executor = ImmutableDatabaseTableMigrationExecutor.builder()
.sourceDataSource(sourceDataSource)
.targetDataSource(targetDataSource)
.build();
TABLES_TO_MIGRATE.forEach(executor::executeMigration);
}

private static DatabaseServiceKey getServiceKeyFromEnvironment() {
String databaseTargetServiceKey = System.getenv(DATABASE_TARGET_SERVICE_KEY);
return new DatabaseServiceKey(JsonUtil.convertJsonToMap(databaseTargetServiceKey));
}

private static void configureLogger() {
ClassLoader classLoader = DatabaseMigration.class.getClassLoader();
if (classLoader != null) {
try (InputStream inputStream = classLoader.getResourceAsStream("console-logger.properties");
LoggerContext loggerContext = new LoggerContext("DatabaseMigration")) {
if (inputStream != null) {
ConfigurationSource configSource = new ConfigurationSource(inputStream);
loggerContext.setConfigLocation(configSource.getURI());
}
} catch (IOException e) {
// Using System.out.println() instead of LOGGER.warn(), because logging is likely not configured due to the exception.
System.out.println("An error occurred while trying to configure logging: " + e.getMessage());
e.printStackTrace();
if (classLoader == null) {
return;
}
try (InputStream inputStream = classLoader.getResourceAsStream("console-logger.properties");
LoggerContext loggerContext = new LoggerContext("DatabaseMigration")) {
if (inputStream != null) {
ConfigurationSource configSource = new ConfigurationSource(inputStream);
loggerContext.setConfigLocation(configSource.getURI());
}
} catch (IOException e) {
// Using System.out.println() instead of LOGGER.warn(), because logging is likely not configured due to the exception.
System.out.println("An error occurred while trying to configure logging: " + e.getMessage());
e.printStackTrace();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.cloudfoundry.multiapps.controller.database.migration.executor.type;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

public class BytesDatabaseTypeSetter implements DatabaseTypeSetter {

private static final String BYTEA = "bytea";

@Override
public List<String> getSupportedTypes() {
return List.of(BYTEA);
}

@Override
public void setType(int columnIndex, PreparedStatement insertStatement, Object value) throws SQLException {
insertStatement.setBytes(columnIndex, (byte[]) value);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public class DatabaseTypeSetterFactory {

private static final List<DatabaseTypeSetter> DEFAULT_REGISTERED_TYPE_SETTERS = Arrays.asList(new StringDatabaseTypeSetter(),
new BooleanDatabaseTypeSetter(),
new LongDatabaseTypeSetter());
new LongDatabaseTypeSetter(),
new BytesDatabaseTypeSetter(),
new TimestampDatabaseTypeSetter());
private final List<DatabaseTypeSetter> registeredTypeSetters;

public DatabaseTypeSetterFactory() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.cloudfoundry.multiapps.controller.database.migration.executor.type;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;

public class TimestampDatabaseTypeSetter implements DatabaseTypeSetter {

private static final String TIMESTAMP = "timestamp";

@Override
public List<String> getSupportedTypes() {
return List.of(TIMESTAMP);
}

@Override
public void setType(int columnIndex, PreparedStatement insertStatement, Object value) throws SQLException {
insertStatement.setTimestamp(columnIndex, (Timestamp) value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.cloudfoundry.multiapps.controller.database.migration.executor.type;

import java.nio.charset.StandardCharsets;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

class BytesDatabaseTypeSetterTest {

private static final int COLUMN_INDEX = 1;

@Mock
private PreparedStatement preparedStatement;

private BytesDatabaseTypeSetter setter;

@BeforeEach
void setUp() throws Exception {
MockitoAnnotations.openMocks(this)
.close();
setter = new BytesDatabaseTypeSetter();
}

@Test
void testGetSupportedTypesContainsBytea() {
List<String> supportedTypes = setter.getSupportedTypes();

Assertions.assertEquals(List.of("bytea"), supportedTypes);
}

@Test
void testSetTypeDelegatesToSetBytes() throws SQLException {
byte[] value = "descriptor-payload".getBytes(StandardCharsets.UTF_8);

setter.setType(COLUMN_INDEX, preparedStatement, value);

Mockito.verify(preparedStatement)
.setBytes(COLUMN_INDEX, value);
}

@Test
void testSetTypeWithNullValue() throws SQLException {
setter.setType(COLUMN_INDEX, preparedStatement, null);

Mockito.verify(preparedStatement)
.setBytes(COLUMN_INDEX, null);
}

@Test
void testSetTypeWithEmptyByteArray() throws SQLException {
byte[] value = new byte[0];

setter.setType(COLUMN_INDEX, preparedStatement, value);

Mockito.verify(preparedStatement)
.setBytes(COLUMN_INDEX, value);
}

@Test
void testSetTypeWithNonByteArrayValueThrowsClassCastException() {
Assertions.assertThrows(ClassCastException.class,
() -> setter.setType(COLUMN_INDEX, preparedStatement, "not-a-byte-array"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class DatabaseTypeSetterFactoryTest {
private static final String BOOL_TYPE = "bool";
private static final String LONG_TYPE = "int8";
private static final String STRING_TYPE = "varchar";
private static final String BYTES_TYPE = "bytea";
private static final String TIMESTAMP_TYPE = "timestamp";

@Test
void testGetWithNullStringParameter() {
Expand Down Expand Up @@ -67,6 +69,16 @@ void testGetWithDefaultRegisteredTypeSettersWhenMatchingDefaultTypeSetters() {
resultDatabaseTypeSetter = databaseTypeSetterFactory.get(STRING_TYPE);
Assertions.assertTrue(resultDatabaseTypeSetter.getSupportedTypes()
.contains(STRING_TYPE));

resultDatabaseTypeSetter = databaseTypeSetterFactory.get(BYTES_TYPE);
Assertions.assertTrue(resultDatabaseTypeSetter.getSupportedTypes()
.contains(BYTES_TYPE));
Assertions.assertInstanceOf(BytesDatabaseTypeSetter.class, resultDatabaseTypeSetter);

resultDatabaseTypeSetter = databaseTypeSetterFactory.get(TIMESTAMP_TYPE);
Assertions.assertTrue(resultDatabaseTypeSetter.getSupportedTypes()
.contains(TIMESTAMP_TYPE));
Assertions.assertInstanceOf(TimestampDatabaseTypeSetter.class, resultDatabaseTypeSetter);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.cloudfoundry.multiapps.controller.database.migration.executor.type;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

class TimestampDatabaseTypeSetterTest {

private static final int COLUMN_INDEX = 1;

@Mock
private PreparedStatement preparedStatement;

private TimestampDatabaseTypeSetter setter;

@BeforeEach
void setUp() throws Exception {
MockitoAnnotations.openMocks(this)
.close();
setter = new TimestampDatabaseTypeSetter();
}

@Test
void testGetSupportedTypesContainsTimestamp() {
List<String> supportedTypes = setter.getSupportedTypes();

Assertions.assertEquals(List.of("timestamp"), supportedTypes);
}

@Test
void testSetTypeDelegatesToSetTimestamp() throws SQLException {
Timestamp value = Timestamp.valueOf("2026-06-26 13:16:22.060");

setter.setType(COLUMN_INDEX, preparedStatement, value);

Mockito.verify(preparedStatement)
.setTimestamp(COLUMN_INDEX, value);
}

@Test
void testSetTypeWithNullValue() throws SQLException {
setter.setType(COLUMN_INDEX, preparedStatement, null);

Mockito.verify(preparedStatement)
.setTimestamp(COLUMN_INDEX, null);
}

@Test
void testSetTypeWithNonTimestampValueThrowsClassCastException() {
Assertions.assertThrows(ClassCastException.class,
() -> setter.setType(COLUMN_INDEX, preparedStatement, "2026-06-26"));
}

}
Loading