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}