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}