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 "
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}