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.system.HapiSystemProperties;
027import ca.uhn.fhir.util.StopWatch;
028import com.google.common.annotations.VisibleForTesting;
029import jakarta.annotation.Nonnull;
030import org.apache.commons.lang3.Validate;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import java.sql.SQLException;
035import java.util.Collections;
036import java.util.List;
037import java.util.Objects;
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}