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