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