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.jpa.migrate.DriverTypeEnum; 023import ca.uhn.fhir.jpa.migrate.JdbcUtils; 024import jakarta.annotation.Nonnull; 025import org.apache.commons.lang3.Validate; 026import org.apache.commons.lang3.builder.EqualsBuilder; 027import org.apache.commons.lang3.builder.HashCodeBuilder; 028import org.intellij.lang.annotations.Language; 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031import org.springframework.jdbc.core.JdbcTemplate; 032import org.springframework.jdbc.core.RowMapperResultSetExtractor; 033import org.springframework.jdbc.core.SingleColumnRowMapper; 034 035import java.sql.SQLException; 036import java.util.ArrayList; 037import java.util.Collections; 038import java.util.List; 039import java.util.Objects; 040import java.util.Set; 041import javax.sql.DataSource; 042 043public class DropIndexTask extends BaseTableTask { 044 045 private static final Logger ourLog = LoggerFactory.getLogger(DropIndexTask.class); 046 private String myIndexName; 047 private boolean myOnline; 048 049 public DropIndexTask(String theProductVersion, String theSchemaVersion) { 050 super(theProductVersion, theSchemaVersion); 051 } 052 053 List<String> generateSql() throws SQLException { 054 Validate.notBlank(myIndexName, "indexName must not be blank"); 055 Validate.notBlank(getTableName(), "tableName must not be blank"); 056 057 if (!JdbcUtils.getIndexNames(getConnectionProperties(), getTableName()).contains(myIndexName)) { 058 return Collections.emptyList(); 059 } 060 boolean isUnique = JdbcUtils.isIndexUnique(getConnectionProperties(), getTableName(), myIndexName); 061 062 return doGenerateSql(isUnique); 063 } 064 065 // testable without jdbc 066 @Nonnull 067 List<String> doGenerateSql(boolean isUnique) { 068 DriverTypeEnum driverType = getDriverType(); 069 List<String> sql = new ArrayList<>(); 070 071 if (isUnique) { 072 // Drop constraint 073 switch (driverType) { 074 case MYSQL_5_7: 075 case MARIADB_10_1: 076 // Need to quote the index name as the word "PRIMARY" is reserved in MySQL 077 sql.add("alter table " + getTableName() + " drop index `" + myIndexName + "`"); 078 break; 079 case H2_EMBEDDED: 080 sql.add("drop index " + myIndexName); 081 break; 082 case DERBY_EMBEDDED: 083 sql.add("alter table " + getTableName() + " drop constraint " + myIndexName); 084 break; 085 case ORACLE_12C: 086 sql.add("drop index " + myIndexName + (myOnline ? " ONLINE" : "")); 087 break; 088 case MSSQL_2012: 089 sql.add("drop index " + myIndexName + " on " + getTableName() 090 + (myOnline ? " WITH (ONLINE = ON)" : "")); 091 break; 092 case POSTGRES_9_4: 093 sql.add("alter table " + getTableName() + " drop constraint if exists " + myIndexName + " cascade"); 094 sql.add("drop index " + (myOnline ? "CONCURRENTLY " : "") + "if exists " + myIndexName 095 + " cascade"); 096 setTransactional(!myOnline); 097 break; 098 case COCKROACHDB_21_1: 099 sql.add("drop index if exists " + getTableName() + "@" + myIndexName + " cascade"); 100 break; 101 } 102 } else { 103 // Drop index 104 switch (driverType) { 105 case MYSQL_5_7: 106 case MARIADB_10_1: 107 sql.add("alter table " + getTableName() + " drop index " + myIndexName); 108 break; 109 case POSTGRES_9_4: 110 sql.add("drop index " + (myOnline ? "CONCURRENTLY " : "") + myIndexName); 111 setTransactional(!myOnline); 112 break; 113 case DERBY_EMBEDDED: 114 case H2_EMBEDDED: 115 sql.add("drop index " + myIndexName); 116 break; 117 case ORACLE_12C: 118 sql.add("drop index " + myIndexName + (myOnline ? " ONLINE" : "")); 119 break; 120 case MSSQL_2012: 121 // use a try-catch to try online first, and fail over to lock path. 122 String sqlServerDrop = "drop index " + getTableName() + "." + myIndexName; 123 if (myOnline) { 124 sqlServerDrop = AddIndexTask.buildOnlineCreateWithTryCatchFallback(sqlServerDrop); 125 } 126 sql.add(sqlServerDrop); 127 break; 128 case COCKROACHDB_21_1: 129 sql.add("drop index " + getTableName() + "@" + myIndexName); 130 break; 131 } 132 } 133 return sql; 134 } 135 136 @Override 137 public void validate() { 138 super.validate(); 139 Validate.notBlank(myIndexName, "The index name must not be blank"); 140 141 setDescription("Drop index " + myIndexName + " from table " + getTableName()); 142 } 143 144 @Override 145 public void doExecute() throws SQLException { 146 /* 147 * Derby and H2 both behave a bit weirdly if you create a unique constraint 148 * using the @UniqueConstraint annotation in hibernate - They will create a 149 * constraint with that name, but will then create a shadow index with a different 150 * name, and it's that different name that gets reported when you query for the 151 * list of indexes. 152 * 153 * For example, on H2 if you create a constraint named "IDX_FOO", the system 154 * will create an index named "IDX_FOO_INDEX_A" and a constraint named "IDX_FOO". 155 * 156 * The following is a solution that uses appropriate native queries to detect 157 * on the given platforms whether an index name actually corresponds to a 158 * constraint, and delete that constraint. 159 */ 160 161 if (getDriverType() == DriverTypeEnum.H2_EMBEDDED) { 162 @Language("SQL") 163 String findConstraintSql = 164 "SELECT DISTINCT constraint_name FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE constraint_name = ? AND table_name = ?"; 165 @Language("SQL") 166 String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?"; 167 findAndDropConstraint(findConstraintSql, dropConstraintSql); 168 } else if (getDriverType() == DriverTypeEnum.DERBY_EMBEDDED) { 169 @Language("SQL") 170 String findConstraintSql = 171 "SELECT c.constraintname FROM sys.sysconstraints c, sys.systables t WHERE c.tableid = t.tableid AND c.constraintname = ? AND t.tablename = ?"; 172 @Language("SQL") 173 String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?"; 174 findAndDropConstraint(findConstraintSql, dropConstraintSql); 175 } else if (getDriverType() == DriverTypeEnum.ORACLE_12C) { 176 @Language("SQL") 177 String findConstraintSql = 178 "SELECT constraint_name FROM user_constraints WHERE constraint_name = ? AND table_name = ?"; 179 @Language("SQL") 180 String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?"; 181 findAndDropConstraint(findConstraintSql, dropConstraintSql); 182 } else if (getDriverType() == DriverTypeEnum.MSSQL_2012) { 183 // Legacy deletion for SQL Server unique indexes 184 @Language("SQL") 185 String findConstraintSql = 186 "SELECT tc.CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc WHERE tc.CONSTRAINT_NAME = ? AND tc.TABLE_NAME = ?"; 187 @Language("SQL") 188 String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?"; 189 findAndDropConstraint(findConstraintSql, dropConstraintSql); 190 } 191 192 Set<String> indexNames = JdbcUtils.getIndexNames(getConnectionProperties(), getTableName()); 193 194 if (!indexNames.contains(myIndexName)) { 195 logInfo(ourLog, "Index {} does not exist on table {} - No action needed", myIndexName, getTableName()); 196 return; 197 } 198 199 boolean isUnique = JdbcUtils.isIndexUnique(getConnectionProperties(), getTableName(), myIndexName); 200 String uniquenessString = isUnique ? "unique" : "non-unique"; 201 202 List<String> sqls = generateSql(); 203 if (!sqls.isEmpty()) { 204 logInfo(ourLog, "Dropping {} index {} on table {}", uniquenessString, myIndexName, getTableName()); 205 } 206 for (@Language("SQL") String sql : sqls) { 207 executeSql(getTableName(), sql); 208 } 209 } 210 211 public void findAndDropConstraint(String theFindConstraintSql, String theDropConstraintSql) { 212 DataSource dataSource = Objects.requireNonNull(getConnectionProperties().getDataSource()); 213 getConnectionProperties().getTxTemplate().executeWithoutResult(t -> { 214 JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); 215 RowMapperResultSetExtractor<String> resultSetExtractor = 216 new RowMapperResultSetExtractor<>(new SingleColumnRowMapper<>(String.class)); 217 List<String> outcome = jdbcTemplate.query( 218 theFindConstraintSql, new Object[] {myIndexName, getTableName()}, resultSetExtractor); 219 assert outcome != null; 220 for (String next : outcome) { 221 String sql = theDropConstraintSql.replace("?", next); 222 executeSql(getTableName(), sql); 223 } 224 }); 225 } 226 227 public DropIndexTask setIndexName(String theIndexName) { 228 myIndexName = theIndexName; 229 return this; 230 } 231 232 @Override 233 protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) { 234 DropIndexTask otherObject = (DropIndexTask) theOtherObject; 235 super.generateEquals(theBuilder, otherObject); 236 theBuilder.append(myIndexName, otherObject.myIndexName); 237 theBuilder.append(myOnline, otherObject.myOnline); 238 } 239 240 @Override 241 protected void generateHashCode(HashCodeBuilder theBuilder) { 242 super.generateHashCode(theBuilder); 243 theBuilder.append(myIndexName); 244 theBuilder.append(myOnline); 245 } 246 247 public void setOnline(boolean theFlag) { 248 this.myOnline = theFlag; 249 } 250}