Compare commits

...

3 Commits

Author SHA1 Message Date
Primož Delopst 4bb4bcaf05
Merge a6dea96fb5 into 6bb72e445d 2024-09-29 18:44:42 +02:00
volodymyr-korzh 6bb72e445d
HapiSchemaMigrationTest intermittent failure fix (#6329) 2024-09-27 16:59:19 -06:00
Primož Delopst a6dea96fb5 Fix primary key migration in case multiple schemas on the same database 2024-09-16 19:26:16 +02:00
6 changed files with 136 additions and 103 deletions

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.jpa.embedded; package ca.uhn.fhir.jpa.embedded;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
import ca.uhn.fhir.jpa.util.DatabaseSupportUtil;
import ca.uhn.fhir.test.utilities.docker.DockerRequiredCondition; import ca.uhn.fhir.test.utilities.docker.DockerRequiredCondition;
import ca.uhn.fhir.util.VersionEnum; import ca.uhn.fhir.util.VersionEnum;
import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterAllCallback;
@ -54,7 +55,7 @@ public class HapiEmbeddedDatabasesExtension implements AfterAllCallback {
myEmbeddedDatabases.add(new H2EmbeddedDatabase()); myEmbeddedDatabases.add(new H2EmbeddedDatabase());
myEmbeddedDatabases.add(new PostgresEmbeddedDatabase()); myEmbeddedDatabases.add(new PostgresEmbeddedDatabase());
myEmbeddedDatabases.add(new MsSqlEmbeddedDatabase()); myEmbeddedDatabases.add(new MsSqlEmbeddedDatabase());
if (OracleCondition.canUseOracle()) { if (DatabaseSupportUtil.canUseOracle()) {
myEmbeddedDatabases.add(new OracleEmbeddedDatabase()); myEmbeddedDatabases.add(new OracleEmbeddedDatabase());
} else { } else {
String message = String message =
@ -136,7 +137,7 @@ public class HapiEmbeddedDatabasesExtension implements AfterAllCallback {
arguments.add(Arguments.of(DriverTypeEnum.POSTGRES_9_4)); arguments.add(Arguments.of(DriverTypeEnum.POSTGRES_9_4));
arguments.add(Arguments.of(DriverTypeEnum.MSSQL_2012)); arguments.add(Arguments.of(DriverTypeEnum.MSSQL_2012));
if (OracleCondition.canUseOracle()) { if (DatabaseSupportUtil.canUseOracle()) {
arguments.add(Arguments.of(DriverTypeEnum.ORACLE_12C)); arguments.add(Arguments.of(DriverTypeEnum.ORACLE_12C));
} }

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.jpa.embedded; package ca.uhn.fhir.jpa.embedded;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
import ca.uhn.fhir.jpa.util.DatabaseSupportUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.testcontainers.containers.MSSQLServerContainer; import org.testcontainers.containers.MSSQLServerContainer;
@ -43,9 +44,19 @@ public class MsSqlEmbeddedDatabase extends JpaEmbeddedDatabase {
private final MSSQLServerContainer myContainer; private final MSSQLServerContainer myContainer;
public MsSqlEmbeddedDatabase() { public MsSqlEmbeddedDatabase() {
// azure-sql-edge docker image does not support kernel 6.7+
// as a result, mssql container fails to start most of the time
// mssql/server:2019 image support kernel 6.7+, so use it for amd64 architecture
// See: https://github.com/microsoft/mssql-docker/issues/868
if (DatabaseSupportUtil.canUseMsSql2019()) {
myContainer = new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2019-latest").acceptLicense();
} else {
DockerImageName msSqlImage = DockerImageName.parse("mcr.microsoft.com/azure-sql-edge:latest") DockerImageName msSqlImage = DockerImageName.parse("mcr.microsoft.com/azure-sql-edge:latest")
.asCompatibleSubstituteFor("mcr.microsoft.com/mssql/server"); .asCompatibleSubstituteFor("mcr.microsoft.com/mssql/server");
myContainer = new MSSQLServerContainer(msSqlImage).acceptLicense(); myContainer = new MSSQLServerContainer(msSqlImage).acceptLicense();
}
myContainer.start(); myContainer.start();
super.initialize( super.initialize(
DriverTypeEnum.MSSQL_2012, DriverTypeEnum.MSSQL_2012,

View File

@ -19,8 +19,7 @@
*/ */
package ca.uhn.fhir.jpa.embedded; package ca.uhn.fhir.jpa.embedded;
import org.apache.commons.lang3.StringUtils; import ca.uhn.fhir.jpa.util.DatabaseSupportUtil;
import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext;
@ -33,25 +32,8 @@ public class OracleCondition implements ExecutionCondition {
@Override @Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext theExtensionContext) { public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext theExtensionContext) {
return canUseOracle() return DatabaseSupportUtil.canUseOracle()
? ConditionEvaluationResult.enabled(ENABLED_MSG) ? ConditionEvaluationResult.enabled(ENABLED_MSG)
: ConditionEvaluationResult.disabled(DISABLED_MSG); : ConditionEvaluationResult.disabled(DISABLED_MSG);
} }
public static boolean canUseOracle() {
if (!isMac()) {
return true;
}
return isColimaConfigured();
}
private static boolean isMac() {
return SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_MAC_OSX;
}
private static boolean isColimaConfigured() {
return StringUtils.isNotBlank(System.getenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"))
&& StringUtils.isNotBlank(System.getenv("DOCKER_HOST"))
&& System.getenv("DOCKER_HOST").contains("colima");
}
} }

View File

@ -0,0 +1,34 @@
package ca.uhn.fhir.jpa.util;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
public final class DatabaseSupportUtil {
private DatabaseSupportUtil() {}
public static boolean canUseMsSql2019() {
return isSupportAmd64Architecture();
}
public static boolean canUseOracle() {
return isSupportAmd64Architecture();
}
private static boolean isSupportAmd64Architecture() {
if (!isMac()) {
return true;
}
return isColimaConfigured();
}
private static boolean isMac() {
return SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_MAC_OSX;
}
private static boolean isColimaConfigured() {
return StringUtils.isNotBlank(System.getenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"))
&& StringUtils.isNotBlank(System.getenv("DOCKER_HOST"))
&& System.getenv("DOCKER_HOST").contains("colima");
}
}

View File

@ -24,9 +24,11 @@ import jakarta.annotation.Nonnull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Migration task that handles cross-database logic for adding a new primary key. * Migration task that handles cross-database logic for adding a new primary key.
@ -46,6 +48,7 @@ public class AddPrimaryKeyTask extends BaseTableTask {
@Nonnull @Nonnull
private String generateSql() { private String generateSql() {
try (Connection connection = getConnectionProperties().getDataSource().getConnection()) {
switch (getDriverType()) { switch (getDriverType()) {
case MYSQL_5_7: case MYSQL_5_7:
case MARIADB_10_1: case MARIADB_10_1:
@ -57,12 +60,18 @@ public class AddPrimaryKeyTask extends BaseTableTask {
case COCKROACHDB_21_1: case COCKROACHDB_21_1:
return String.format( return String.format(
"ALTER TABLE %s ADD PRIMARY KEY (%s)", "ALTER TABLE %s ADD PRIMARY KEY (%s)",
getTableName(), String.join(", ", myPrimaryKeyColumnsInOrder)); Optional.of(connection.getSchema())
.map(schema -> String.format("%s.%s", schema, getTableName()))
.orElse(getTableName()),
String.join(", ", myPrimaryKeyColumnsInOrder));
default: default:
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"%s Unknown driver type. Cannot add primary key for task %s", "%s Unknown driver type. Cannot add primary key for task %s",
Msg.code(2531), getMigrationVersion())); Msg.code(2531), getMigrationVersion()));
} }
} catch (SQLException e) {
throw new IllegalStateException(e);
}
} }
@Override @Override

View File

@ -20,14 +20,17 @@
package ca.uhn.fhir.jpa.migrate.taskdef; package ca.uhn.fhir.jpa.migrate.taskdef;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import com.google.common.collect.ImmutableList;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import org.intellij.lang.annotations.Language; import org.intellij.lang.annotations.Language;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Optional;
/** /**
* Migration task that handles cross-database logic for dropping a primary key. * Migration task that handles cross-database logic for dropping a primary key.
@ -52,27 +55,13 @@ public class DropPrimaryKeyTask extends BaseTableTask {
@Nullable @Nullable
@Language("SQL") @Language("SQL")
final String primaryKeyNameSql = generatePrimaryKeyNameSql(); final String primaryKeyName = getPrimaryKeyName();
@Nullable
final String primaryKeyName = primaryKeyNameSql != null
? newJdbcTemplate()
.queryForObject(primaryKeyNameSql, String.class, getTableNameWithDatabaseExpectedCase())
: null;
ourLog.debug("primaryKeyName: {} for driver: {}", primaryKeyName, getDriverType()); ourLog.debug("primaryKeyName: {} for driver: {}", primaryKeyName, getDriverType());
return generateDropPrimaryKeySql(primaryKeyName); return generateDropPrimaryKeySql(primaryKeyName);
} }
private String getTableNameWithDatabaseExpectedCase() {
if (DriverTypeEnum.ORACLE_12C == getDriverType()) {
return getTableName().toUpperCase();
}
return getTableName().toLowerCase();
}
@Override @Override
protected void doExecute() throws SQLException { protected void doExecute() throws SQLException {
logInfo(ourLog, "Going to DROP the PRIMARY KEY on table {}", getTableName()); logInfo(ourLog, "Going to DROP the PRIMARY KEY on table {}", getTableName());
@ -81,13 +70,18 @@ public class DropPrimaryKeyTask extends BaseTableTask {
} }
private String generateDropPrimaryKeySql(@Nullable String thePrimaryKeyName) { private String generateDropPrimaryKeySql(@Nullable String thePrimaryKeyName) {
try (Connection connection = getConnectionProperties().getDataSource().getConnection()) {
switch (getDriverType()) { switch (getDriverType()) {
case MARIADB_10_1: case MARIADB_10_1:
case DERBY_EMBEDDED: case DERBY_EMBEDDED:
case H2_EMBEDDED: case H2_EMBEDDED:
@Language("SQL") @Language("SQL")
final String sqlH2 = "ALTER TABLE %s DROP PRIMARY KEY"; final String sqlH2 = "ALTER TABLE %s DROP PRIMARY KEY";
return String.format(sqlH2, getTableName()); return String.format(
sqlH2,
Optional.of(connection.getSchema())
.map(schema -> String.format("%s.%s", schema, getTableName()))
.orElse(getTableName()));
case POSTGRES_9_4: case POSTGRES_9_4:
case ORACLE_12C: case ORACLE_12C:
case MSSQL_2012: case MSSQL_2012:
@ -95,42 +89,44 @@ public class DropPrimaryKeyTask extends BaseTableTask {
assert thePrimaryKeyName != null; assert thePrimaryKeyName != null;
@Language("SQL") @Language("SQL")
final String sql = "ALTER TABLE %s DROP CONSTRAINT %s"; final String sql = "ALTER TABLE %s DROP CONSTRAINT %s";
return String.format(sql, getTableName(), thePrimaryKeyName); return String.format(
sql,
Optional.of(connection.getSchema())
.map(schema -> String.format("%s.%s", schema, getTableName()))
.orElse(getTableName()),
thePrimaryKeyName);
default: default:
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"%s Unknown driver type: %s. Cannot drop primary key: %s for task %s", "%s Unknown driver type: %s. Cannot drop primary key: %s for task %s",
Msg.code(2529), getDriverType(), getMigrationVersion(), getTableName())); Msg.code(2529), getDriverType(), getMigrationVersion(), getTableName()));
} }
} catch (SQLException e) {
throw new IllegalStateException(e);
}
} }
@Language("SQL") @SuppressWarnings({"NestedTryStatement", "MethodWithMultipleLoops"})
@Nullable @Nullable
private String generatePrimaryKeyNameSql() { private String getPrimaryKeyName() {
switch (getDriverType()) { String primaryKey = null;
case MYSQL_5_7: try (Connection connection = getConnectionProperties().getDataSource().getConnection()) {
case MARIADB_10_1: for (String tableName : ImmutableList.of(
case DERBY_EMBEDDED: getTableName().toLowerCase(), getTableName().toUpperCase())) {
case COCKROACHDB_21_1: try (ResultSet resultSet = connection
case H2_EMBEDDED: .getMetaData()
return null; // Irrelevant: We don't need to run the SQL for these databases. .getPrimaryKeys(connection.getCatalog(), connection.getSchema(), tableName)) {
case POSTGRES_9_4: while (resultSet.next()) {
return "SELECT constraint_name " + "FROM information_schema.table_constraints " primaryKey = resultSet.getString(6);
+ "WHERE table_schema = 'public' "
+ "AND constraint_type = 'PRIMARY KEY' "
+ "AND table_name = ?";
case ORACLE_12C:
return "SELECT constraint_name " + "FROM user_constraints "
+ "WHERE constraint_type = 'P' "
+ "AND table_name = ?";
case MSSQL_2012:
return "SELECT tc.constraint_name " + "FROM information_schema.table_constraints tc "
+ "JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name "
+ "WHERE tc.constraint_type = 'PRIMARY KEY' "
+ "AND tc.table_name = ?";
default:
throw new IllegalStateException(String.format(
"%s Unknown driver type: %s Cannot find primary key to drop for task %s",
Msg.code(2530), getDriverType(), getMigrationVersion()));
} }
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
} catch (SQLException e) {
throw new IllegalStateException(e);
}
return primaryKey;
} }
} }