001/*-
002 * #%L
003 * HAPI FHIR Server - SQL Migration
004 * %%
005 * Copyright (C) 2014 - 2025 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.JdbcUtils;
024import ca.uhn.fhir.jpa.migrate.tasks.api.TaskFlagEnum;
025import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
026import org.intellij.lang.annotations.Language;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029import org.springframework.jdbc.core.ColumnMapRowMapper;
030
031import java.sql.SQLException;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038public class ModifyColumnTask extends BaseTableColumnTypeTask {
039
040        private static final Logger ourLog = LoggerFactory.getLogger(ModifyColumnTask.class);
041
042        public ModifyColumnTask(String theProductVersion, String theSchemaVersion) {
043                super(theProductVersion, theSchemaVersion);
044        }
045
046        @Override
047        public void validate() {
048                super.validate();
049                setDescription("Modify column " + getColumnName() + " on table " + getTableName());
050        }
051
052        @Override
053        public void doExecute() throws SQLException {
054                JdbcUtils.ColumnType existingType;
055                boolean nullable;
056
057                Set<String> columnNames = JdbcUtils.getColumnNames(getConnectionProperties(), getTableName());
058                if (!columnNames.contains(getColumnName())) {
059                        logInfo(
060                                        ourLog,
061                                        "Column {} doesn't exist on table {} - No action performed",
062                                        getColumnName(),
063                                        getTableName());
064                        return;
065                }
066
067                try {
068                        existingType = JdbcUtils.getColumnType(getConnectionProperties(), getTableName(), getColumnName());
069                        nullable = isColumnNullable(getTableName(), getColumnName());
070                } catch (SQLException e) {
071                        throw new InternalErrorException(Msg.code(66) + e);
072                }
073
074                List<String> sqlStringToExecute = generateSql(existingType, nullable);
075
076                sqlStringToExecute.forEach(s -> executeSql(getTableName(), s));
077        }
078
079        List<String> generateSql(JdbcUtils.ColumnType theColumnType, boolean theIsNullable) {
080                List<String> reVal = new ArrayList<>();
081
082                Long taskColumnLength = getColumnLength();
083                boolean isShrinkOnly = false;
084                if (taskColumnLength != null) {
085                        long existingLength = theColumnType.getLength() != null ? theColumnType.getLength() : 0;
086                        if (existingLength > taskColumnLength) {
087                                if (isNoColumnShrink()) {
088                                        taskColumnLength = existingLength;
089                                } else {
090                                        if (theColumnType.getColumnTypeEnum() == getColumnType()) {
091                                                isShrinkOnly = true;
092                                        }
093                                }
094                        }
095                }
096
097                boolean alreadyOfCorrectType = theColumnType.equals(getColumnType(), taskColumnLength);
098                boolean alreadyCorrectNullable = isNullable() == theIsNullable;
099                if (alreadyOfCorrectType && alreadyCorrectNullable) {
100                        logInfo(
101                                        ourLog,
102                                        "Column {} on table {} is already of type {} and has nullable {} - No action performed",
103                                        getColumnName(),
104                                        getTableName(),
105                                        theColumnType,
106                                        theIsNullable);
107                        return Collections.emptyList();
108                }
109
110                String type = getSqlType(taskColumnLength);
111                String notNull = getSqlNotNull();
112
113                String sql = null;
114                String sqlNotNull = null;
115                switch (getDriverType()) {
116                        case DERBY_EMBEDDED:
117                                if (!alreadyOfCorrectType) {
118                                        sql = "alter table " + getTableName() + " alter column " + getColumnName() + " set data type "
119                                                        + type;
120                                }
121                                if (!alreadyCorrectNullable) {
122                                        sqlNotNull = "alter table " + getTableName() + " alter column " + getColumnName() + notNull;
123                                }
124                                break;
125                        case MARIADB_10_1:
126                        case MYSQL_5_7:
127                                // Quote the column name as "SYSTEM" is a reserved word in MySQL
128                                sql = "alter table " + getTableName() + " modify column `" + getColumnName() + "` " + type + notNull;
129                                break;
130                        case POSTGRES_9_4:
131                        case COCKROACHDB_21_1:
132                                if (!alreadyOfCorrectType) {
133                                        sql = "alter table " + getTableName() + " alter column " + getColumnName() + " type " + type;
134                                }
135                                if (!alreadyCorrectNullable) {
136                                        if (isNullable()) {
137                                                sqlNotNull =
138                                                                "alter table " + getTableName() + " alter column " + getColumnName() + " drop not null";
139                                        } else {
140                                                sqlNotNull =
141                                                                "alter table " + getTableName() + " alter column " + getColumnName() + " set not null";
142                                        }
143                                }
144                                break;
145                        case ORACLE_12C:
146                                String oracleNullableStmt = alreadyCorrectNullable ? "" : notNull;
147                                String oracleTypeStmt = alreadyOfCorrectType ? "" : type;
148                                sql = "alter table " + getTableName() + " modify ( " + getColumnName() + " " + oracleTypeStmt + " "
149                                                + oracleNullableStmt + " )";
150                                break;
151                        case MSSQL_2012:
152                                sql = "alter table " + getTableName() + " alter column " + getColumnName() + " " + type + notNull;
153                                break;
154                        case H2_EMBEDDED:
155                                if (!alreadyOfCorrectType) {
156                                        sql = "alter table " + getTableName() + " alter column " + getColumnName() + " type " + type;
157                                }
158                                if (!alreadyCorrectNullable) {
159                                        if (isNullable()) {
160                                                sqlNotNull =
161                                                                "alter table " + getTableName() + " alter column " + getColumnName() + " drop not null";
162                                        } else {
163                                                sqlNotNull =
164                                                                "alter table " + getTableName() + " alter column " + getColumnName() + " set not null";
165                                        }
166                                }
167                                break;
168                        default:
169                                throw new IllegalStateException(Msg.code(67) + "Dont know how to handle " + getDriverType());
170                }
171
172                if (isShrinkOnly) {
173                        addFlag(TaskFlagEnum.FAILURE_ALLOWED);
174                }
175
176                if (sql != null) {
177                        logInfo(ourLog, "Will Update column {} on table {} to type {}", getColumnName(), getTableName(), type);
178                        reVal.add(sql);
179                }
180
181                if (sqlNotNull != null) {
182                        logInfo(ourLog, "Update column {} on table {} to not null", getColumnName(), getTableName());
183                        reVal.add(sqlNotNull);
184                }
185
186                return reVal;
187        }
188
189        private boolean isColumnNullable(String tableName, String columnName) throws SQLException {
190                boolean result = JdbcUtils.isColumnNullable(getConnectionProperties(), tableName, columnName);
191                // Oracle sometimes stores the NULLABLE property in a Constraint, so override the result if this is an Oracle DB
192                switch (getDriverType()) {
193                        case ORACLE_12C:
194                                @Language("SQL")
195                                String findNullableConstraintSql =
196                                                "SELECT acc.owner, acc.table_name, acc.column_name, search_condition_vc "
197                                                                + "FROM all_cons_columns acc, user_constraints uc "
198                                                                + "WHERE acc.constraint_name = uc.constraint_name "
199                                                                + "AND acc.table_name = uc.table_name "
200                                                                + "AND uc.constraint_type = ? "
201                                                                + "AND acc.table_name = ? "
202                                                                + "AND acc.column_name = ? "
203                                                                + "AND search_condition_vc = ? ";
204                                String[] params = new String[4];
205                                params[0] = "C";
206                                params[1] = tableName.toUpperCase();
207                                params[2] = columnName.toUpperCase();
208                                params[3] = "\"" + columnName.toUpperCase() + "\" IS NOT NULL";
209                                List<Map<String, Object>> queryResults = getConnectionProperties()
210                                                .getTxTemplate()
211                                                .execute(t -> getConnectionProperties()
212                                                                .newJdbcTemplate()
213                                                                .query(findNullableConstraintSql, params, new ColumnMapRowMapper()));
214                                // If this query returns a row then the existence of that row indicates that a NOT NULL constraint
215                                // exists
216                                // on this Column and we must override whatever result was previously calculated and set it to false
217                                if (queryResults != null
218                                                && queryResults.size() > 0
219                                                && queryResults.get(0) != null
220                                                && !queryResults.get(0).isEmpty()) {
221                                        result = false;
222                                }
223                                break;
224                        default:
225                                // Do nothing since we already initialized the variable above
226                                break;
227                }
228                return result;
229        }
230}