001/*-
002 * #%L
003 * HAPI FHIR Server - SQL Migration
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.migrate.taskdef;
021
022import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
023import ca.uhn.fhir.jpa.migrate.JdbcUtils;
024import org.apache.commons.lang3.Validate;
025import org.apache.commons.lang3.builder.EqualsBuilder;
026import org.apache.commons.lang3.builder.HashCodeBuilder;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import java.sql.SQLException;
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.List;
034import java.util.Locale;
035import java.util.Set;
036import javax.annotation.Nonnull;
037
038public class AddIndexTask extends BaseTableTask {
039
040        static final Logger ourLog = LoggerFactory.getLogger(AddIndexTask.class);
041
042        private String myIndexName;
043        private List<String> myColumns;
044        private Boolean myUnique;
045        private List<String> myIncludeColumns = Collections.emptyList();
046        /** Should the operation avoid taking a lock on the table */
047        private boolean myOnline;
048
049        private MetadataSource myMetadataSource = new MetadataSource();
050
051        public AddIndexTask(String theProductVersion, String theSchemaVersion) {
052                super(theProductVersion, theSchemaVersion);
053        }
054
055        public void setIndexName(String theIndexName) {
056                myIndexName = theIndexName.toUpperCase(Locale.US);
057        }
058
059        public void setColumns(List<String> theColumns) {
060                myColumns = theColumns;
061        }
062
063        public void setUnique(boolean theUnique) {
064                myUnique = theUnique;
065        }
066
067        @Override
068        public void validate() {
069                super.validate();
070                Validate.notBlank(myIndexName, "Index name not specified");
071                Validate.isTrue(
072                                myColumns.size() > 0,
073                                "Columns not specified for AddIndexTask " + myIndexName + " on table " + getTableName());
074                Validate.notNull(myUnique, "Uniqueness not specified");
075                setDescription("Add " + myIndexName + " index to table " + getTableName());
076        }
077
078        @Override
079        public void doExecute() throws SQLException {
080                Set<String> indexNames = JdbcUtils.getIndexNames(getConnectionProperties(), getTableName());
081                if (indexNames.contains(myIndexName)) {
082                        logInfo(ourLog, "Index {} already exists on table {} - No action performed", myIndexName, getTableName());
083                        return;
084                }
085
086                logInfo(
087                                ourLog,
088                                "Going to add a {} index named {} on table {} for columns {}",
089                                (myUnique ? "UNIQUE" : "NON-UNIQUE"),
090                                myIndexName,
091                                getTableName(),
092                                myColumns);
093
094                String sql = generateSql();
095                String tableName = getTableName();
096
097                try {
098                        executeSql(tableName, sql);
099                } catch (Exception e) {
100                        String message = e.toString();
101                        if (message.contains("already exists")
102                                        ||
103                                        // The Oracle message is ORA-01408: such column list already indexed
104                                        // TODO KHS consider db-specific handling here that uses the error code instead of the message so
105                                        // this is language independent
106                                        //  e.g. if the db is Oracle than checking e.getErrorCode() == 1408 should detect this case
107                                        message.contains("already indexed")) {
108                                ourLog.warn("Index {} already exists: {}", myIndexName, e.getMessage());
109                        } else {
110                                throw e;
111                        }
112                }
113        }
114
115        @Nonnull
116        String generateSql() {
117                String unique = myUnique ? "unique " : "";
118                String columns = String.join(", ", myColumns);
119                String includeClause = "";
120                String mssqlWhereClause = "";
121                if (!myIncludeColumns.isEmpty()) {
122                        switch (getDriverType()) {
123                                case POSTGRES_9_4:
124                                case MSSQL_2012:
125                                case COCKROACHDB_21_1:
126                                        includeClause = " INCLUDE (" + String.join(", ", myIncludeColumns) + ")";
127                                        break;
128                                case H2_EMBEDDED:
129                                case DERBY_EMBEDDED:
130                                case MARIADB_10_1:
131                                case MYSQL_5_7:
132                                case ORACLE_12C:
133                                        // These platforms don't support the include clause
134                                        // Per:
135                                        // https://use-the-index-luke.com/blog/2019-04/include-columns-in-btree-indexes#postgresql-limitations
136                                        break;
137                        }
138                }
139                if (myUnique && getDriverType() == DriverTypeEnum.MSSQL_2012) {
140                        mssqlWhereClause = buildMSSqlNotNullWhereClause();
141                }
142                // Should we do this non-transactionally?  Avoids a write-lock, but introduces weird failure modes.
143                String postgresOnlineClause = "";
144                String msSqlOracleOnlineClause = "";
145                if (myOnline) {
146                        switch (getDriverType()) {
147                                case POSTGRES_9_4:
148                                case COCKROACHDB_21_1:
149                                        postgresOnlineClause = "CONCURRENTLY ";
150                                        // This runs without a lock, and can't be done transactionally.
151                                        setTransactional(false);
152                                        break;
153                                case ORACLE_12C:
154                                        if (myMetadataSource.isOnlineIndexSupported(getConnectionProperties())) {
155                                                msSqlOracleOnlineClause = " ONLINE DEFERRED INVALIDATION";
156                                        }
157                                        break;
158                                case MSSQL_2012:
159                                        if (myMetadataSource.isOnlineIndexSupported(getConnectionProperties())) {
160                                                msSqlOracleOnlineClause = " WITH (ONLINE = ON)";
161                                        }
162                                        break;
163                                default:
164                        }
165                }
166
167                String sql = "create " + unique + "index " + postgresOnlineClause + myIndexName + " on " + getTableName() + "("
168                                + columns + ")" + includeClause + mssqlWhereClause + msSqlOracleOnlineClause;
169                return sql;
170        }
171
172        @Nonnull
173        private String buildMSSqlNotNullWhereClause() {
174                String mssqlWhereClause;
175                mssqlWhereClause = " WHERE (";
176                for (int i = 0; i < myColumns.size(); i++) {
177                        mssqlWhereClause += myColumns.get(i) + " IS NOT NULL ";
178                        if (i < myColumns.size() - 1) {
179                                mssqlWhereClause += "AND ";
180                        }
181                }
182                mssqlWhereClause += ")";
183                return mssqlWhereClause;
184        }
185
186        public void setColumns(String... theColumns) {
187                setColumns(Arrays.asList(theColumns));
188        }
189
190        public void setIncludeColumns(String... theIncludeColumns) {
191                setIncludeColumns(Arrays.asList(theIncludeColumns));
192        }
193
194        private void setIncludeColumns(List<String> theIncludeColumns) {
195                Validate.notNull(theIncludeColumns);
196                myIncludeColumns = theIncludeColumns;
197        }
198
199        /**
200         * Add Index without locking the table.
201         */
202        public void setOnline(boolean theFlag) {
203                myOnline = theFlag;
204        }
205
206        @Override
207        protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) {
208                super.generateEquals(theBuilder, theOtherObject);
209
210                AddIndexTask otherObject = (AddIndexTask) theOtherObject;
211                theBuilder.append(myIndexName, otherObject.myIndexName);
212                theBuilder.append(myColumns, otherObject.myColumns);
213                theBuilder.append(myUnique, otherObject.myUnique);
214                theBuilder.append(myIncludeColumns, otherObject.myIncludeColumns);
215                theBuilder.append(myOnline, otherObject.myOnline);
216        }
217
218        @Override
219        protected void generateHashCode(HashCodeBuilder theBuilder) {
220                super.generateHashCode(theBuilder);
221                theBuilder.append(myIndexName);
222                theBuilder.append(myColumns);
223                theBuilder.append(myUnique);
224                theBuilder.append(myOnline);
225        }
226
227        public void setMetadataSource(MetadataSource theMetadataSource) {
228                myMetadataSource = theMetadataSource;
229        }
230}