001package ca.uhn.fhir.jpa.migrate; 002 003/*- 004 * #%L 005 * HAPI FHIR Server - SQL Migration 006 * %% 007 * Copyright (C) 2014 - 2023 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.migrate.dao.HapiMigrationDao; 025import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; 026import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask; 027import ca.uhn.fhir.system.HapiSystemProperties; 028import ca.uhn.fhir.util.StopWatch; 029import com.google.common.annotations.VisibleForTesting; 030import org.apache.commons.lang3.Validate; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034import javax.annotation.Nonnull; 035import javax.sql.DataSource; 036import java.sql.SQLException; 037import java.util.Collections; 038import java.util.List; 039import java.util.Objects; 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 myNoColumnShrink; 049 private final DriverTypeEnum myDriverType; 050 private final DataSource myDataSource; 051 private final HapiMigrationStorageSvc myHapiMigrationStorageSvc; 052 private List<IHapiMigrationCallback> myCallbacks = Collections.emptyList(); 053 054 public HapiMigrator(String theMigrationTableName, DataSource theDataSource, DriverTypeEnum theDriverType) { 055 myDriverType = theDriverType; 056 myDataSource = theDataSource; 057 myHapiMigrationStorageSvc = 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 085 protected StringBuilder buildExecutedStatementsString(MigrationResult theMigrationResult) { 086 StringBuilder statementBuilder = new StringBuilder(); 087 String lastTable = null; 088 for (BaseTask.ExecutedStatement next : theMigrationResult.executedStatements) { 089 if (!Objects.equals(lastTable, next.getTableName())) { 090 statementBuilder.append("\n\n-- Table: ").append(next.getTableName()).append("\n"); 091 lastTable = next.getTableName(); 092 } 093 094 statementBuilder.append(next.getSql()).append(";\n"); 095 096 for (Object nextArg : next.getArguments()) { 097 statementBuilder.append(" -- Arg: ").append(nextArg).append("\n"); 098 } 099 } 100 return statementBuilder; 101 } 102 103 /** 104 * Helper method to clear a lock with a given UUID. 105 * @param theUUID the 106 */ 107 public void clearMigrationLockWithUUID(String theUUID) { 108 ourLog.info("Attempting to remove lock entry. [uuid={}]", theUUID); 109 boolean success = myHapiMigrationStorageSvc.deleteLockRecord(theUUID); 110 if (success) { 111 ourLog.info("Successfully removed lock entry. [uuid={}]", theUUID); 112 } else { 113 ourLog.error("Did not successfully remove lock entry. [uuid={}]", theUUID); 114 } 115 } 116 117 public MigrationResult migrate() { 118 ourLog.info("Loaded {} migration tasks", myTaskList.size()); 119 MigrationResult retval = new MigrationResult(); 120 121 // Lock the migration table so only one server migrates the database at once 122 try (HapiMigrationLock ignored = new HapiMigrationLock(myHapiMigrationStorageSvc)) { 123 MigrationTaskList newTaskList = myHapiMigrationStorageSvc.diff(myTaskList); 124 ourLog.info("{} of these {} migration tasks are new. Executing them now.", newTaskList.size(), myTaskList.size()); 125 126 try (DriverTypeEnum.ConnectionProperties connectionProperties = getDriverType().newConnectionProperties(getDataSource())) { 127 128 newTaskList.forEach(next -> { 129 130 next.setDriverType(getDriverType()); 131 next.setDryRun(isDryRun()); 132 next.setNoColumnShrink(isNoColumnShrink()); 133 next.setConnectionProperties(connectionProperties); 134 135 executeTask(next, retval); 136 }); 137 } 138 } catch (Exception e) { 139 ourLog.error("Migration failed", e); 140 throw e; 141 } 142 143 ourLog.info(retval.summary()); 144 145 if (isDryRun()) { 146 StringBuilder statementBuilder = buildExecutedStatementsString(retval); 147 ourLog.info("SQL that would be executed:\n\n***********************************\n{}***********************************", statementBuilder); 148 } 149 150 return retval; 151 } 152 153 private void executeTask(BaseTask theTask, MigrationResult theMigrationResult) { 154 StopWatch sw = new StopWatch(); 155 try { 156 if (isDryRun()) { 157 ourLog.info("Dry run {} {}", theTask.getMigrationVersion(), theTask.getDescription()); 158 } else { 159 ourLog.info("Executing {} {}", theTask.getMigrationVersion(), theTask.getDescription()); 160 } 161 preExecute(theTask); 162 theTask.execute(); 163 postExecute(theTask, sw, true); 164 theMigrationResult.changes += theTask.getChangesCount(); 165 theMigrationResult.executedStatements.addAll(theTask.getExecutedStatements()); 166 theMigrationResult.succeededTasks.add(theTask); 167 } catch (SQLException | HapiMigrationException e) { 168 theMigrationResult.failedTasks.add(theTask); 169 postExecute(theTask, sw, false); 170 String description = theTask.getDescription(); 171 if (isBlank(description)) { 172 description = theTask.getClass().getSimpleName(); 173 } 174 String prefix = "Failure executing task \"" + description + "\", aborting! Cause: "; 175 throw new HapiMigrationException(Msg.code(47) + prefix + e, theMigrationResult, e); 176 } 177 } 178 179 private void preExecute(BaseTask theTask) { 180 myCallbacks.forEach(action -> action.preExecution(theTask)); 181 182 } 183 184 private void postExecute(BaseTask theNext, StopWatch theStopWatch, boolean theSuccess) { 185 myHapiMigrationStorageSvc.saveTask(theNext, Math.toIntExact(theStopWatch.getMillis()), theSuccess); 186 } 187 188 public void addTasks(Iterable<BaseTask> theMigrationTasks) { 189 if (HapiSystemProperties.isUnitTestModeEnabled()) { 190 // Tests only need to initialize the schemas. No need to run all the migrations for every test. 191 for (BaseTask task : theMigrationTasks) { 192 if (task instanceof InitializeSchemaTask) { 193 addTask(task); 194 } 195 } 196 } else { 197 myTaskList.append(theMigrationTasks); 198 } 199 } 200 201 public void addTask(BaseTask theTask) { 202 myTaskList.add(theTask); 203 } 204 205 public void setCallbacks(@Nonnull List<IHapiMigrationCallback> theCallbacks) { 206 Validate.notNull(theCallbacks); 207 myCallbacks = theCallbacks; 208 } 209 210 @VisibleForTesting 211 public void removeAllTasksForUnitTest() { 212 myTaskList.clear(); 213 } 214 215 public void createMigrationTableIfRequired() { 216 myHapiMigrationStorageSvc.createMigrationTableIfRequired(); 217 } 218}