001package ca.uhn.fhir.jpa.migrate.taskdef;
002
003/*-
004 * #%L
005 * HAPI FHIR Server - SQL Migration
006 * %%
007 * Copyright (C) 2014 - 2021 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.jpa.migrate.DriverTypeEnum;
024import ca.uhn.fhir.jpa.migrate.JdbcUtils;
025import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
026import com.google.common.annotations.VisibleForTesting;
027import org.apache.commons.lang3.Validate;
028import org.apache.commons.lang3.builder.HashCodeBuilder;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import org.springframework.jdbc.core.ColumnMapRowMapper;
032import org.springframework.jdbc.core.JdbcTemplate;
033
034import java.sql.SQLException;
035import java.util.List;
036import java.util.Set;
037
038public class RenameColumnTask extends BaseTableTask {
039
040        private static final Logger ourLog = LoggerFactory.getLogger(RenameColumnTask.class);
041        private String myOldName;
042        private String myNewName;
043        private boolean myIsOkayIfNeitherColumnExists;
044        private boolean myDeleteTargetColumnFirstIfBothExist;
045
046        private boolean mySimulateMySQLForTest = false;
047
048        public RenameColumnTask(String theProductVersion, String theSchemaVersion) {
049                super(theProductVersion, theSchemaVersion);
050        }
051
052        public void setDeleteTargetColumnFirstIfBothExist(boolean theDeleteTargetColumnFirstIfBothExist) {
053                myDeleteTargetColumnFirstIfBothExist = theDeleteTargetColumnFirstIfBothExist;
054        }
055
056        @Override
057        public void validate() {
058                super.validate();
059                setDescription("Rename column " + myOldName + " to " + myNewName + " on table " + getTableName());
060        }
061
062        public void setOldName(String theOldName) {
063                Validate.notBlank(theOldName);
064                myOldName = theOldName;
065        }
066
067        public void setNewName(String theNewName) {
068                Validate.notBlank(theNewName);
069                myNewName = theNewName;
070        }
071
072        @Override
073        public void doExecute() throws SQLException {
074                Set<String> columnNames = JdbcUtils.getColumnNames(getConnectionProperties(), getTableName());
075                boolean haveOldName = columnNames.contains(myOldName.toUpperCase());
076                boolean haveNewName = columnNames.contains(myNewName.toUpperCase());
077                if (haveOldName && haveNewName) {
078                        if (myDeleteTargetColumnFirstIfBothExist) {
079
080                                Integer rowsWithData = getConnectionProperties().getTxTemplate().execute(t -> {
081                                        String sql = "SELECT * FROM " + getTableName() + " WHERE " + myNewName + " IS NOT NULL";
082                                        JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate();
083                                        jdbcTemplate.setMaxRows(1);
084                                        return jdbcTemplate.query(sql, new ColumnMapRowMapper()).size();
085                                });
086                                if (rowsWithData != null && rowsWithData > 0) {
087                                        throw new SQLException("Can not rename " + getTableName() + "." + myOldName + " to " + myNewName + " because both columns exist and data exists in " + myNewName);
088                                }
089
090                                if (getDriverType().equals(DriverTypeEnum.MYSQL_5_7) || mySimulateMySQLForTest) {
091                                        // Some DBs such as MYSQL require that foreign keys depending on the column be explicitly dropped before the column itself is dropped.
092                                        logInfo(ourLog, "Table {} has columns {} and {} - Going to drop any foreign keys depending on column {} before renaming", getTableName(), myOldName, myNewName, myNewName);
093                                        Set<String> foreignKeys = JdbcUtils.getForeignKeysForColumn(getConnectionProperties(), myNewName, getTableName());
094                                        if (foreignKeys != null) {
095                                                for (String foreignKey : foreignKeys) {
096                                                        List<String> dropFkSqls = DropForeignKeyTask.generateSql(getTableName(), foreignKey, getDriverType());
097                                                        for (String dropFkSql : dropFkSqls) {
098                                                                executeSql(getTableName(), dropFkSql);
099                                                        }
100                                                }
101                                        }
102                                }
103
104                                logInfo(ourLog, "Table {} has columns {} and {} - Going to drop {} before renaming", getTableName(), myOldName, myNewName, myNewName);
105                                String sql = DropColumnTask.createSql(getTableName(), myNewName);
106                                executeSql(getTableName(), sql);
107                        } else {
108                                throw new SQLException("Can not rename " + getTableName() + "." + myOldName + " to " + myNewName + " because both columns exist!");
109                        }
110                } else if (!haveOldName && !haveNewName) {
111                        if (isOkayIfNeitherColumnExists()) {
112                                return;
113                        }
114                        throw new SQLException("Can not rename " + getTableName() + "." + myOldName + " to " + myNewName + " because neither column exists!");
115                } else if (haveNewName) {
116                        logInfo(ourLog, "Column {} already exists on table {} - No action performed", myNewName, getTableName());
117                        return;
118                }
119
120                String existingType;
121                String notNull;
122                try {
123                        JdbcUtils.ColumnType existingColumnType = JdbcUtils.getColumnType(getConnectionProperties(), getTableName(), myOldName);
124                        existingType = getSqlType(existingColumnType.getColumnTypeEnum(), existingColumnType.getLength());
125                        notNull = JdbcUtils.isColumnNullable(getConnectionProperties(), getTableName(), myOldName) ? " null " : " not null";
126                } catch (SQLException e) {
127                        throw new InternalErrorException(e);
128                }
129                String sql = buildRenameColumnSqlStatement(existingType, notNull);
130
131                logInfo(ourLog, "Renaming column {} on table {} to {}", myOldName, getTableName(), myNewName);
132                executeSql(getTableName(), sql);
133
134        }
135
136        String buildRenameColumnSqlStatement(String theExistingType, String theExistingNotNull) {
137                String sql;
138                switch (getDriverType()) {
139                        case DERBY_EMBEDDED:
140                                sql = "RENAME COLUMN " + getTableName() + "." + myOldName + " TO " + myNewName;
141                                break;
142                        case MYSQL_5_7:
143                        case MARIADB_10_1:
144                                // Quote the column names as "SYSTEM" is a reserved word in MySQL
145                                sql = "ALTER TABLE " + getTableName() + " CHANGE COLUMN `" + myOldName + "` `" + myNewName + "` " + theExistingType + " " + theExistingNotNull;
146                                break;
147                        case POSTGRES_9_4:
148                        case ORACLE_12C:
149                                sql = "ALTER TABLE " + getTableName() + " RENAME COLUMN " + myOldName + " TO " + myNewName;
150                                break;
151                        case MSSQL_2012:
152                                sql = "sp_rename '" + getTableName() + "." + myOldName + "', '" + myNewName + "', 'COLUMN'";
153                                break;
154                        case H2_EMBEDDED:
155                                sql = "ALTER TABLE " + getTableName() + " ALTER COLUMN " + myOldName + " RENAME TO " + myNewName;
156                                break;
157                        default:
158                                throw new IllegalStateException();
159                }
160                return sql;
161        }
162
163        public boolean isOkayIfNeitherColumnExists() {
164                return myIsOkayIfNeitherColumnExists;
165        }
166
167        public void setOkayIfNeitherColumnExists(boolean theOkayIfNeitherColumnExists) {
168                myIsOkayIfNeitherColumnExists = theOkayIfNeitherColumnExists;
169        }
170
171        @Override
172        protected void generateHashCode(HashCodeBuilder theBuilder) {
173                super.generateHashCode(theBuilder);
174                theBuilder.append(myOldName);
175                theBuilder.append(myNewName);
176        }
177
178        @VisibleForTesting
179        void setSimulateMySQLForTest(boolean theSimulateMySQLForTest) {
180                mySimulateMySQLForTest = theSimulateMySQLForTest;
181        }
182}