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 private boolean myOnline; 048 049 public AddIndexTask(String theProductVersion, String theSchemaVersion) { 050 super(theProductVersion, theSchemaVersion); 051 } 052 053 public void setIndexName(String theIndexName) { 054 myIndexName = theIndexName.toUpperCase(Locale.US); 055 } 056 057 public void setColumns(List<String> theColumns) { 058 myColumns = theColumns; 059 } 060 061 public void setUnique(boolean theUnique) { 062 myUnique = theUnique; 063 } 064 065 @Override 066 public void validate() { 067 super.validate(); 068 Validate.notBlank(myIndexName, "Index name not specified"); 069 Validate.isTrue(myColumns.size() > 0, "Columns not specified for AddIndexTask " + myIndexName + " on table " + getTableName()); 070 Validate.notNull(myUnique, "Uniqueness not specified"); 071 setDescription("Add " + myIndexName + " index to table " + getTableName()); 072 } 073 074 @Override 075 public void doExecute() throws SQLException { 076 Set<String> indexNames = JdbcUtils.getIndexNames(getConnectionProperties(), getTableName()); 077 if (indexNames.contains(myIndexName)) { 078 logInfo(ourLog, "Index {} already exists on table {} - No action performed", myIndexName, getTableName()); 079 return; 080 } 081 082 logInfo(ourLog, "Going to add a {} index named {} on table {} for columns {}", (myUnique ? "UNIQUE" : "NON-UNIQUE"), myIndexName, getTableName(), myColumns); 083 084 String sql = generateSql(); 085 String tableName = getTableName(); 086 087 try { 088 executeSql(tableName, sql); 089 } catch (Exception e) { 090 if (e.toString().contains("already exists")) { 091 ourLog.warn("Index {} already exists", myIndexName); 092 } else { 093 throw e; 094 } 095 } 096 } 097 098 @Nonnull 099 String generateSql() { 100 String unique = myUnique ? "unique " : ""; 101 String columns = String.join(", ", myColumns); 102 String includeClause = ""; 103 String mssqlWhereClause = ""; 104 if (!myIncludeColumns.isEmpty()) { 105 switch (getDriverType()) { 106 case POSTGRES_9_4: 107 case MSSQL_2012: 108 includeClause = " INCLUDE (" + String.join(", ", myIncludeColumns) + ")"; 109 break; 110 case H2_EMBEDDED: 111 case DERBY_EMBEDDED: 112 case MARIADB_10_1: 113 case MYSQL_5_7: 114 case ORACLE_12C: 115 // These platforms don't support the include clause 116 // Per: 117 // https://use-the-index-luke.com/blog/2019-04/include-columns-in-btree-indexes#postgresql-limitations 118 break; 119 } 120 } 121 if (myUnique && getDriverType() == DriverTypeEnum.MSSQL_2012) { 122 mssqlWhereClause = " WHERE ("; 123 for (int i = 0; i < myColumns.size(); i++) { 124 mssqlWhereClause += myColumns.get(i) + " IS NOT NULL "; 125 if (i < myColumns.size() - 1) { 126 mssqlWhereClause += "AND "; 127 } 128 } 129 mssqlWhereClause += ")"; 130 } 131 String postgresOnline = ""; 132 String oracleOnlineDeferred = ""; 133 if (myOnline) { 134 switch (getDriverType()) { 135 case POSTGRES_9_4: 136 postgresOnline = "CONCURRENTLY "; 137 // This runs without a lock, and can't be done transactionally. 138 setTransactional(false); 139 break; 140 case ORACLE_12C: 141 oracleOnlineDeferred = " ONLINE DEFERRED INVALIDATION"; 142 break; 143 case MSSQL_2012: 144 oracleOnlineDeferred = " WITH (ONLINE = ON)"; 145 break; 146 default: 147 } 148 } 149 150 151 String sql = 152 "create " + unique + "index " + postgresOnline + myIndexName + 153 " on " + getTableName() + "(" + columns + ")" + includeClause + mssqlWhereClause + oracleOnlineDeferred; 154 return sql; 155 } 156 157 public void setColumns(String... theColumns) { 158 setColumns(Arrays.asList(theColumns)); 159 } 160 161 public void setIncludeColumns(String... theIncludeColumns) { 162 setIncludeColumns(Arrays.asList(theIncludeColumns)); 163 } 164 165 private void setIncludeColumns(List<String> theIncludeColumns) { 166 Validate.notNull(theIncludeColumns); 167 myIncludeColumns = theIncludeColumns; 168 } 169 170 /** 171 * Add Index without locking the table. 172 */ 173 public void setOnline(boolean theFlag) { 174 myOnline = theFlag; 175 } 176 @Override 177 protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) { 178 super.generateEquals(theBuilder, theOtherObject); 179 180 AddIndexTask otherObject = (AddIndexTask) theOtherObject; 181 theBuilder.append(myIndexName, otherObject.myIndexName); 182 theBuilder.append(myColumns, otherObject.myColumns); 183 theBuilder.append(myUnique, otherObject.myUnique); 184 theBuilder.append(myIncludeColumns, otherObject.myIncludeColumns); 185 theBuilder.append(myOnline, otherObject.myOnline); 186 187 } 188 189 @Override 190 protected void generateHashCode(HashCodeBuilder theBuilder) { 191 super.generateHashCode(theBuilder); 192 theBuilder.append(myIndexName); 193 theBuilder.append(myColumns); 194 theBuilder.append(myUnique); 195 theBuilder.append(myOnline); 196 } 197 198}