001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2023 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 " + 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(Msg.code(55) + "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(Msg.code(56) + "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(Msg.code(57) + 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 case COCKROACHDB_21_1: 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}