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.taskdef; 021 022import ca.uhn.fhir.i18n.Msg; 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(Msg.code(54) + "Can not rename " + getTableName() + "." + myOldName + " to " 088 + 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 093 // before the column itself is dropped. 094 logInfo( 095 ourLog, 096 "Table {} has columns {} and {} - Going to drop any foreign keys depending on column {} before renaming", 097 getTableName(), 098 myOldName, 099 myNewName, 100 myNewName); 101 Set<String> foreignKeys = 102 JdbcUtils.getForeignKeysForColumn(getConnectionProperties(), myNewName, getTableName()); 103 if (foreignKeys != null) { 104 for (String foreignKey : foreignKeys) { 105 List<String> dropFkSqls = 106 DropForeignKeyTask.generateSql(getTableName(), foreignKey, getDriverType()); 107 for (String dropFkSql : dropFkSqls) { 108 executeSql(getTableName(), dropFkSql); 109 } 110 } 111 } 112 } 113 114 logInfo( 115 ourLog, 116 "Table {} has columns {} and {} - Going to drop {} before renaming", 117 getTableName(), 118 myOldName, 119 myNewName, 120 myNewName); 121 String sql = DropColumnTask.createSql(getTableName(), myNewName); 122 executeSql(getTableName(), sql); 123 } else { 124 throw new SQLException(Msg.code(55) + "Can not rename " + getTableName() + "." + myOldName + " to " 125 + myNewName + " because both columns exist!"); 126 } 127 } else if (!haveOldName && !haveNewName) { 128 if (isOkayIfNeitherColumnExists()) { 129 return; 130 } 131 throw new SQLException(Msg.code(56) + "Can not rename " + getTableName() + "." + myOldName + " to " 132 + myNewName + " because neither column exists!"); 133 } else if (haveNewName) { 134 logInfo(ourLog, "Column {} already exists on table {} - No action performed", myNewName, getTableName()); 135 return; 136 } 137 138 String existingType; 139 String notNull; 140 try { 141 JdbcUtils.ColumnType existingColumnType = 142 JdbcUtils.getColumnType(getConnectionProperties(), getTableName(), myOldName); 143 existingType = getSqlType(existingColumnType.getColumnTypeEnum(), existingColumnType.getLength()); 144 notNull = JdbcUtils.isColumnNullable(getConnectionProperties(), getTableName(), myOldName) 145 ? " null " 146 : " not null"; 147 } catch (SQLException e) { 148 throw new InternalErrorException(Msg.code(57) + e); 149 } 150 String sql = buildRenameColumnSqlStatement(existingType, notNull); 151 152 logInfo(ourLog, "Renaming column {} on table {} to {}", myOldName, getTableName(), myNewName); 153 executeSql(getTableName(), sql); 154 } 155 156 String buildRenameColumnSqlStatement(String theExistingType, String theExistingNotNull) { 157 String sql; 158 switch (getDriverType()) { 159 case DERBY_EMBEDDED: 160 sql = "RENAME COLUMN " + getTableName() + "." + myOldName + " TO " + myNewName; 161 break; 162 case MYSQL_5_7: 163 case MARIADB_10_1: 164 // Quote the column names as "SYSTEM" is a reserved word in MySQL 165 sql = "ALTER TABLE " + getTableName() + " CHANGE COLUMN `" + myOldName + "` `" + myNewName + "` " 166 + theExistingType + " " + theExistingNotNull; 167 break; 168 case POSTGRES_9_4: 169 case ORACLE_12C: 170 case COCKROACHDB_21_1: 171 sql = "ALTER TABLE " + getTableName() + " RENAME COLUMN " + myOldName + " TO " + myNewName; 172 break; 173 case MSSQL_2012: 174 sql = "sp_rename '" + getTableName() + "." + myOldName + "', '" + myNewName + "', 'COLUMN'"; 175 break; 176 case H2_EMBEDDED: 177 sql = "ALTER TABLE " + getTableName() + " ALTER COLUMN " + myOldName + " RENAME TO " + myNewName; 178 break; 179 default: 180 throw new IllegalStateException(Msg.code(58)); 181 } 182 return sql; 183 } 184 185 public boolean isOkayIfNeitherColumnExists() { 186 return myIsOkayIfNeitherColumnExists; 187 } 188 189 public void setOkayIfNeitherColumnExists(boolean theOkayIfNeitherColumnExists) { 190 myIsOkayIfNeitherColumnExists = theOkayIfNeitherColumnExists; 191 } 192 193 @Override 194 protected void generateHashCode(HashCodeBuilder theBuilder) { 195 super.generateHashCode(theBuilder); 196 theBuilder.append(myOldName); 197 theBuilder.append(myNewName); 198 } 199 200 @VisibleForTesting 201 void setSimulateMySQLForTest(boolean theSimulateMySQLForTest) { 202 mySimulateMySQLForTest = theSimulateMySQLForTest; 203 } 204}