001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2023 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.system.HapiSystemProperties; 027import ca.uhn.fhir.util.StopWatch; 028import com.google.common.annotations.VisibleForTesting; 029import org.apache.commons.lang3.Validate; 030import org.slf4j.Logger; 031import org.slf4j.LoggerFactory; 032 033import java.sql.SQLException; 034import java.util.Collections; 035import java.util.List; 036import java.util.Objects; 037import javax.annotation.Nonnull; 038import javax.sql.DataSource; 039 040import static org.apache.commons.lang3.StringUtils.isBlank; 041 042public class HapiMigrator { 043 044 private static final Logger ourLog = LoggerFactory.getLogger(HapiMigrator.class); 045 private final MigrationTaskList myTaskList = new MigrationTaskList(); 046 private boolean myDryRun; 047 private boolean myNoColumnShrink; 048 private final DriverTypeEnum myDriverType; 049 private final DataSource myDataSource; 050 private final HapiMigrationStorageSvc myHapiMigrationStorageSvc; 051 private List<IHapiMigrationCallback> myCallbacks = Collections.emptyList(); 052 053 public HapiMigrator(String theMigrationTableName, DataSource theDataSource, DriverTypeEnum theDriverType) { 054 myDriverType = theDriverType; 055 myDataSource = theDataSource; 056 myHapiMigrationStorageSvc = 057 new HapiMigrationStorageSvc(new HapiMigrationDao(theDataSource, theDriverType, theMigrationTableName)); 058 } 059 060 public DataSource getDataSource() { 061 return myDataSource; 062 } 063 064 public boolean isDryRun() { 065 return myDryRun; 066 } 067 068 public void setDryRun(boolean theDryRun) { 069 myDryRun = theDryRun; 070 } 071 072 public boolean isNoColumnShrink() { 073 return myNoColumnShrink; 074 } 075 076 public void setNoColumnShrink(boolean theNoColumnShrink) { 077 myNoColumnShrink = theNoColumnShrink; 078 } 079 080 public DriverTypeEnum getDriverType() { 081 return myDriverType; 082 } 083 084 protected StringBuilder buildExecutedStatementsString(MigrationResult theMigrationResult) { 085 StringBuilder statementBuilder = new StringBuilder(); 086 String lastTable = null; 087 for (BaseTask.ExecutedStatement next : theMigrationResult.executedStatements) { 088 if (!Objects.equals(lastTable, next.getTableName())) { 089 statementBuilder 090 .append("\n\n-- Table: ") 091 .append(next.getTableName()) 092 .append("\n"); 093 lastTable = next.getTableName(); 094 } 095 096 statementBuilder.append(next.getSql()).append(";\n"); 097 098 for (Object nextArg : next.getArguments()) { 099 statementBuilder.append(" -- Arg: ").append(nextArg).append("\n"); 100 } 101 } 102 return statementBuilder; 103 } 104 105 /** 106 * Helper method to clear a lock with a given UUID. 107 * @param theUUID the 108 */ 109 public void clearMigrationLockWithUUID(String theUUID) { 110 ourLog.info("Attempting to remove lock entry. [uuid={}]", theUUID); 111 boolean success = myHapiMigrationStorageSvc.deleteLockRecord(theUUID); 112 if (success) { 113 ourLog.info("Successfully removed lock entry. [uuid={}]", theUUID); 114 } else { 115 ourLog.error("Did not successfully remove lock entry. [uuid={}]", theUUID); 116 } 117 } 118 119 public MigrationResult migrate() { 120 ourLog.info("Loaded {} migration tasks", myTaskList.size()); 121 MigrationResult retval = new MigrationResult(); 122 123 // Lock the migration table so only one server migrates the database at once 124 try (HapiMigrationLock ignored = new HapiMigrationLock(myHapiMigrationStorageSvc)) { 125 MigrationTaskList newTaskList = myHapiMigrationStorageSvc.diff(myTaskList); 126 ourLog.info( 127 "{} of these {} migration tasks are new. Executing them now.", 128 newTaskList.size(), 129 myTaskList.size()); 130 131 try (DriverTypeEnum.ConnectionProperties connectionProperties = 132 getDriverType().newConnectionProperties(getDataSource())) { 133 134 newTaskList.forEach(next -> { 135 next.setDriverType(getDriverType()); 136 next.setDryRun(isDryRun()); 137 next.setNoColumnShrink(isNoColumnShrink()); 138 next.setConnectionProperties(connectionProperties); 139 140 executeTask(next, retval); 141 }); 142 } 143 } catch (Exception e) { 144 ourLog.error("Migration failed", e); 145 throw e; 146 } 147 148 ourLog.info(retval.summary()); 149 150 if (isDryRun()) { 151 StringBuilder statementBuilder = buildExecutedStatementsString(retval); 152 ourLog.info( 153 "SQL that would be executed:\n\n***********************************\n{}***********************************", 154 statementBuilder); 155 } 156 157 return retval; 158 } 159 160 private void executeTask(BaseTask theTask, MigrationResult theMigrationResult) { 161 StopWatch sw = new StopWatch(); 162 try { 163 if (isDryRun()) { 164 ourLog.info("Dry run {} {}", theTask.getMigrationVersion(), theTask.getDescription()); 165 } else { 166 ourLog.info("Executing {} {}", theTask.getMigrationVersion(), theTask.getDescription()); 167 } 168 preExecute(theTask); 169 theTask.execute(); 170 postExecute(theTask, sw, true); 171 theMigrationResult.changes += theTask.getChangesCount(); 172 theMigrationResult.executedStatements.addAll(theTask.getExecutedStatements()); 173 theMigrationResult.succeededTasks.add(theTask); 174 } catch (SQLException | HapiMigrationException e) { 175 theMigrationResult.failedTasks.add(theTask); 176 postExecute(theTask, sw, false); 177 String description = theTask.getDescription(); 178 if (isBlank(description)) { 179 description = theTask.getClass().getSimpleName(); 180 } 181 String prefix = "Failure executing task \"" + description + "\", aborting! Cause: "; 182 throw new HapiMigrationException(Msg.code(47) + prefix + e, theMigrationResult, e); 183 } 184 } 185 186 private void preExecute(BaseTask theTask) { 187 myCallbacks.forEach(action -> action.preExecution(theTask)); 188 } 189 190 private void postExecute(BaseTask theNext, StopWatch theStopWatch, boolean theSuccess) { 191 if (!theNext.isDryRun()) { 192 myHapiMigrationStorageSvc.saveTask(theNext, Math.toIntExact(theStopWatch.getMillis()), theSuccess); 193 } 194 } 195 196 public void addTasks(Iterable<BaseTask> theMigrationTasks) { 197 if (HapiSystemProperties.isUnitTestModeEnabled()) { 198 // Tests only need to initialize the schemas. No need to run all the migrations for every test. 199 for (BaseTask task : theMigrationTasks) { 200 if (task instanceof InitializeSchemaTask) { 201 addTask(task); 202 } 203 } 204 } else { 205 myTaskList.append(theMigrationTasks); 206 } 207 } 208 209 public void addTask(BaseTask theTask) { 210 myTaskList.add(theTask); 211 } 212 213 public void setCallbacks(@Nonnull List<IHapiMigrationCallback> theCallbacks) { 214 Validate.notNull(theCallbacks); 215 myCallbacks = theCallbacks; 216 } 217 218 @VisibleForTesting 219 public void removeAllTasksForUnitTest() { 220 myTaskList.clear(); 221 } 222 223 public void createMigrationTableIfRequired() { 224 if (!myDryRun) { 225 myHapiMigrationStorageSvc.createMigrationTableIfRequired(); 226 } 227 } 228}