001package ca.uhn.fhir.jpa.migrate.taskdef; 002 003/*- 004 * #%L 005 * HAPI FHIR Server - SQL Migration 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; 024import ca.uhn.fhir.jpa.migrate.JdbcUtils; 025import org.apache.commons.lang3.Validate; 026import org.apache.commons.lang3.builder.EqualsBuilder; 027import org.apache.commons.lang3.builder.HashCodeBuilder; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031import javax.annotation.Nonnull; 032import java.sql.SQLException; 033import java.util.Arrays; 034import java.util.Collections; 035import java.util.List; 036import java.util.Locale; 037import java.util.Set; 038 039public class AddIndexTask extends BaseTableTask { 040 041 private static final Logger ourLog = LoggerFactory.getLogger(AddIndexTask.class); 042 043 private String myIndexName; 044 private List<String> myColumns; 045 private Boolean myUnique; 046 private List<String> myIncludeColumns = Collections.emptyList(); 047 /** Should the operation avoid taking a lock on the table */ 048 private boolean myOnline; 049 050 private MetadataSource myMetadataSource = new MetadataSource(); 051 052 public AddIndexTask(String theProductVersion, String theSchemaVersion) { 053 super(theProductVersion, theSchemaVersion); 054 } 055 056 public void setIndexName(String theIndexName) { 057 myIndexName = theIndexName.toUpperCase(Locale.US); 058 } 059 060 public void setColumns(List<String> theColumns) { 061 myColumns = theColumns; 062 } 063 064 public void setUnique(boolean theUnique) { 065 myUnique = theUnique; 066 } 067 068 @Override 069 public void validate() { 070 super.validate(); 071 Validate.notBlank(myIndexName, "Index name not specified"); 072 Validate.isTrue(myColumns.size() > 0, "Columns not specified for AddIndexTask " + myIndexName + " on table " + getTableName()); 073 Validate.notNull(myUnique, "Uniqueness not specified"); 074 setDescription("Add " + myIndexName + " index to table " + getTableName()); 075 } 076 077 @Override 078 public void doExecute() throws SQLException { 079 Set<String> indexNames = JdbcUtils.getIndexNames(getConnectionProperties(), getTableName()); 080 if (indexNames.contains(myIndexName)) { 081 logInfo(ourLog, "Index {} already exists on table {} - No action performed", myIndexName, getTableName()); 082 return; 083 } 084 085 logInfo(ourLog, "Going to add a {} index named {} on table {} for columns {}", (myUnique ? "UNIQUE" : "NON-UNIQUE"), myIndexName, getTableName(), myColumns); 086 087 String sql = generateSql(); 088 String tableName = getTableName(); 089 090 try { 091 executeSql(tableName, sql); 092 } catch (Exception e) { 093 if (e.toString().contains("already exists")) { 094 ourLog.warn("Index {} already exists", myIndexName); 095 } else { 096 throw e; 097 } 098 } 099 } 100 101 @Nonnull 102 String generateSql() { 103 String unique = myUnique ? "unique " : ""; 104 String columns = String.join(", ", myColumns); 105 String includeClause = ""; 106 String mssqlWhereClause = ""; 107 if (!myIncludeColumns.isEmpty()) { 108 switch (getDriverType()) { 109 case POSTGRES_9_4: 110 case MSSQL_2012: 111 case COCKROACHDB_21_1: 112 includeClause = " INCLUDE (" + String.join(", ", myIncludeColumns) + ")"; 113 break; 114 case H2_EMBEDDED: 115 case DERBY_EMBEDDED: 116 case MARIADB_10_1: 117 case MYSQL_5_7: 118 case ORACLE_12C: 119 // These platforms don't support the include clause 120 // Per: 121 // https://use-the-index-luke.com/blog/2019-04/include-columns-in-btree-indexes#postgresql-limitations 122 break; 123 } 124 } 125 if (myUnique && getDriverType() == DriverTypeEnum.MSSQL_2012) { 126 mssqlWhereClause = buildMSSqlNotNullWhereClause(); 127 } 128 // Should we do this non-transactionally? Avoids a write-lock, but introduces weird failure modes. 129 String postgresOnlineClause = ""; 130 String msSqlOracleOnlineClause = ""; 131 if (myOnline) { 132 switch (getDriverType()) { 133 case POSTGRES_9_4: 134 case COCKROACHDB_21_1: 135 postgresOnlineClause = "CONCURRENTLY "; 136 // This runs without a lock, and can't be done transactionally. 137 setTransactional(false); 138 break; 139 case ORACLE_12C: 140 if (myMetadataSource.isOnlineIndexSupported(getConnectionProperties())) { 141 msSqlOracleOnlineClause = " ONLINE DEFERRED INVALIDATION"; 142 } 143 break; 144 case MSSQL_2012: 145 if (myMetadataSource.isOnlineIndexSupported(getConnectionProperties())) { 146 msSqlOracleOnlineClause = " WITH (ONLINE = ON)"; 147 } 148 break; 149 default: 150 } 151 } 152 153 154 String sql = 155 "create " + unique + "index " + postgresOnlineClause + myIndexName + 156 " on " + getTableName() + "(" + columns + ")" + includeClause + mssqlWhereClause + msSqlOracleOnlineClause; 157 return sql; 158 } 159 160 @Nonnull 161 private String buildMSSqlNotNullWhereClause() { 162 String mssqlWhereClause; 163 mssqlWhereClause = " WHERE ("; 164 for (int i = 0; i < myColumns.size(); i++) { 165 mssqlWhereClause += myColumns.get(i) + " IS NOT NULL "; 166 if (i < myColumns.size() - 1) { 167 mssqlWhereClause += "AND "; 168 } 169 } 170 mssqlWhereClause += ")"; 171 return mssqlWhereClause; 172 } 173 174 public void setColumns(String... theColumns) { 175 setColumns(Arrays.asList(theColumns)); 176 } 177 178 public void setIncludeColumns(String... theIncludeColumns) { 179 setIncludeColumns(Arrays.asList(theIncludeColumns)); 180 } 181 182 private void setIncludeColumns(List<String> theIncludeColumns) { 183 Validate.notNull(theIncludeColumns); 184 myIncludeColumns = theIncludeColumns; 185 } 186 187 /** 188 * Add Index without locking the table. 189 */ 190 public void setOnline(boolean theFlag) { 191 myOnline = theFlag; 192 } 193 @Override 194 protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) { 195 super.generateEquals(theBuilder, theOtherObject); 196 197 AddIndexTask otherObject = (AddIndexTask) theOtherObject; 198 theBuilder.append(myIndexName, otherObject.myIndexName); 199 theBuilder.append(myColumns, otherObject.myColumns); 200 theBuilder.append(myUnique, otherObject.myUnique); 201 theBuilder.append(myIncludeColumns, otherObject.myIncludeColumns); 202 theBuilder.append(myOnline, otherObject.myOnline); 203 204 } 205 206 @Override 207 protected void generateHashCode(HashCodeBuilder theBuilder) { 208 super.generateHashCode(theBuilder); 209 theBuilder.append(myIndexName); 210 theBuilder.append(myColumns); 211 theBuilder.append(myUnique); 212 theBuilder.append(myOnline); 213 } 214 215 public void setMetadataSource(MetadataSource theMetadataSource) { 216 myMetadataSource = theMetadataSource; 217 } 218}