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