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}