001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.migrate; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.jpa.migrate.dao.HapiMigrationDao; 024import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; 025import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask; 026import ca.uhn.fhir.jpa.migrate.tasks.api.TaskFlagEnum; 027import ca.uhn.fhir.system.HapiSystemProperties; 028import ca.uhn.fhir.util.StopWatch; 029import com.google.common.annotations.VisibleForTesting; 030import jakarta.annotation.Nonnull; 031import org.apache.commons.lang3.Validate; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import java.sql.SQLException; 036import java.util.Collections; 037import java.util.List; 038import java.util.Objects; 039import javax.sql.DataSource; 040 041import static org.apache.commons.lang3.StringUtils.isBlank; 042 043public class HapiMigrator { 044 045 private static final Logger ourLog = LoggerFactory.getLogger(HapiMigrator.class); 046 private final MigrationTaskList myTaskList = new MigrationTaskList(); 047 private boolean myDryRun; 048 private boolean myRunHeavyweightSkippableTasks; 049 private boolean myNoColumnShrink; 050 private final DriverTypeEnum myDriverType; 051 private final DataSource myDataSource; 052 private final HapiMigrationStorageSvc myHapiMigrationStorageSvc; 053 private List<IHapiMigrationCallback> myCallbacks = Collections.emptyList(); 054 055 public HapiMigrator(String theMigrationTableName, DataSource theDataSource, DriverTypeEnum theDriverType) { 056 myDriverType = theDriverType; 057 myDataSource = theDataSource; 058 myHapiMigrationStorageSvc = 059 new HapiMigrationStorageSvc(new HapiMigrationDao(theDataSource, theDriverType, theMigrationTableName)); 060 } 061 062 public DataSource getDataSource() { 063 return myDataSource; 064 } 065 066 public boolean isDryRun() { 067 return myDryRun; 068 } 069 070 public void setDryRun(boolean theDryRun) { 071 myDryRun = theDryRun; 072 } 073 074 /** 075 * Should we run the tasks marked with {@link ca.uhn.fhir.jpa.migrate.tasks.api.TaskFlagEnum#HEAVYWEIGHT_SKIP_BY_DEFAULT} 076 * 077 * @since 7.4.0 078 */ 079 public boolean isRunHeavyweightSkippableTasks() { 080 return myRunHeavyweightSkippableTasks; 081 } 082 083 /** 084 * Should we run the tasks marked with {@link ca.uhn.fhir.jpa.migrate.tasks.api.TaskFlagEnum#HEAVYWEIGHT_SKIP_BY_DEFAULT} 085 * 086 * @since 7.4.0 087 */ 088 public void setRunHeavyweightSkippableTasks(boolean theRunHeavyweightSkippableTasks) { 089 myRunHeavyweightSkippableTasks = theRunHeavyweightSkippableTasks; 090 } 091 092 public boolean isNoColumnShrink() { 093 return myNoColumnShrink; 094 } 095 096 public void setNoColumnShrink(boolean theNoColumnShrink) { 097 myNoColumnShrink = theNoColumnShrink; 098 } 099 100 public DriverTypeEnum getDriverType() { 101 return myDriverType; 102 } 103 104 protected StringBuilder buildExecutedStatementsString(MigrationResult theMigrationResult) { 105 StringBuilder statementBuilder = new StringBuilder(); 106 String lastTable = null; 107 for (BaseTask.ExecutedStatement next : theMigrationResult.executedStatements) { 108 if (!Objects.equals(lastTable, next.getTableName())) { 109 statementBuilder 110 .append("\n\n-- Table: ") 111 .append(next.getTableName()) 112 .append("\n"); 113 lastTable = next.getTableName(); 114 } 115 116 statementBuilder.append(next.getSql()).append(";\n"); 117 118 for (Object nextArg : next.getArguments()) { 119 statementBuilder.append(" -- Arg: ").append(nextArg).append("\n"); 120 } 121 } 122 return statementBuilder; 123 } 124 125 /** 126 * Helper method to clear a lock with a given UUID. 127 * @param theUUID the 128 */ 129 public void clearMigrationLockWithUUID(String theUUID) { 130 ourLog.info("Attempting to remove lock entry. [uuid={}]", theUUID); 131 boolean success = myHapiMigrationStorageSvc.deleteLockRecord(theUUID); 132 if (success) { 133 ourLog.info("Successfully removed lock entry. [uuid={}]", theUUID); 134 } else { 135 ourLog.error("Did not successfully remove lock entry. [uuid={}]", theUUID); 136 } 137 } 138 139 public MigrationResult migrate() { 140 ourLog.info("Loaded {} migration tasks", myTaskList.size()); 141 MigrationResult retval = new MigrationResult(); 142 143 // Lock the migration table so only one server migrates the database at once 144 try (HapiMigrationLock ignored = new HapiMigrationLock(myHapiMigrationStorageSvc)) { 145 MigrationTaskList newTaskList = myHapiMigrationStorageSvc.diff(myTaskList); 146 ourLog.info( 147 "{} of these {} migration tasks are new. Executing them now.", 148 newTaskList.size(), 149 myTaskList.size()); 150 151 try (DriverTypeEnum.ConnectionProperties connectionProperties = 152 getDriverType().newConnectionProperties(getDataSource())) { 153 154 if (!isRunHeavyweightSkippableTasks()) { 155 newTaskList.removeIf(BaseTask::isHeavyweightSkippableTask); 156 } 157 158 boolean initializedSchema = false; 159 for (BaseTask next : newTaskList) { 160 if (initializedSchema && !next.hasFlag(TaskFlagEnum.RUN_DURING_SCHEMA_INITIALIZATION)) { 161 ourLog.info("Skipping task {} because schema is being initialized", next.getMigrationVersion()); 162 recordTaskAsCompletedIfNotDryRun(next, 0L, true); 163 continue; 164 } 165 166 next.setDriverType(getDriverType()); 167 next.setDryRun(isDryRun()); 168 next.setNoColumnShrink(isNoColumnShrink()); 169 next.setConnectionProperties(connectionProperties); 170 171 executeTask(next, retval); 172 173 initializedSchema |= next.initializedSchema(); 174 } 175 } 176 } catch (Exception e) { 177 ourLog.error("Migration failed", e); 178 throw e; 179 } 180 181 ourLog.info(retval.summary()); 182 183 if (isDryRun()) { 184 StringBuilder statementBuilder = buildExecutedStatementsString(retval); 185 ourLog.info( 186 "SQL that would be executed:\n\n***********************************\n{}***********************************", 187 statementBuilder); 188 } 189 190 return retval; 191 } 192 193 private void executeTask(BaseTask theTask, MigrationResult theMigrationResult) { 194 StopWatch sw = new StopWatch(); 195 try { 196 if (isDryRun()) { 197 ourLog.info("Dry run {} {}", theTask.getMigrationVersion(), theTask.getDescription()); 198 } else { 199 ourLog.info("Executing {} {}", theTask.getMigrationVersion(), theTask.getDescription()); 200 } 201 preExecute(theTask); 202 theTask.execute(); 203 recordTaskAsCompletedIfNotDryRun(theTask, sw.getMillis(), true); 204 theMigrationResult.changes += theTask.getChangesCount(); 205 theMigrationResult.executedStatements.addAll(theTask.getExecutedStatements()); 206 theMigrationResult.succeededTasks.add(theTask); 207 } catch (SQLException | HapiMigrationException e) { 208 theMigrationResult.failedTasks.add(theTask); 209 recordTaskAsCompletedIfNotDryRun(theTask, sw.getMillis(), false); 210 String description = theTask.getDescription(); 211 if (isBlank(description)) { 212 description = theTask.getClass().getSimpleName(); 213 } 214 String prefix = String.format( 215 "Failure executing task '%s', for driver: %s, aborting! Cause: ", description, getDriverType()); 216 throw new HapiMigrationException(Msg.code(47) + prefix + e, theMigrationResult, e); 217 } 218 } 219 220 private void preExecute(BaseTask theTask) { 221 myCallbacks.forEach(action -> action.preExecution(theTask)); 222 } 223 224 private void recordTaskAsCompletedIfNotDryRun(BaseTask theNext, long theExecutionMillis, boolean theSuccess) { 225 if (!theNext.isDryRun()) { 226 myHapiMigrationStorageSvc.saveTask(theNext, Math.toIntExact(theExecutionMillis), theSuccess); 227 } 228 } 229 230 public void addTasks(Iterable<BaseTask> theMigrationTasks) { 231 if (HapiSystemProperties.isUnitTestModeEnabled()) { 232 // Tests only need to initialize the schemas. No need to run all the migrations for every test. 233 for (BaseTask task : theMigrationTasks) { 234 if (task instanceof InitializeSchemaTask) { 235 addTask(task); 236 } 237 } 238 } else { 239 myTaskList.append(theMigrationTasks); 240 } 241 } 242 243 public void addTask(BaseTask theTask) { 244 myTaskList.add(theTask); 245 } 246 247 public void setCallbacks(@Nonnull List<IHapiMigrationCallback> theCallbacks) { 248 Validate.notNull(theCallbacks, "theCallbacks must not be null"); 249 myCallbacks = theCallbacks; 250 } 251 252 @VisibleForTesting 253 public void removeAllTasksForUnitTest() { 254 myTaskList.clear(); 255 } 256 257 public void createMigrationTableIfRequired() { 258 if (!myDryRun) { 259 myHapiMigrationStorageSvc.createMigrationTableIfRequired(); 260 } 261 } 262}