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}