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.intellij.lang.annotations.Language;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import org.springframework.jdbc.core.JdbcTemplate;
032import org.springframework.jdbc.core.RowMapperResultSetExtractor;
033import org.springframework.jdbc.core.SingleColumnRowMapper;
034
035import javax.sql.DataSource;
036import java.sql.SQLException;
037import java.util.ArrayList;
038import java.util.Collections;
039import java.util.List;
040import java.util.Objects;
041import java.util.Set;
042
043public class DropIndexTask extends BaseTableTask {
044
045        private static final Logger ourLog = LoggerFactory.getLogger(DropIndexTask.class);
046        private String myIndexName;
047
048        public DropIndexTask(String theProductVersion, String theSchemaVersion) {
049                super(theProductVersion, theSchemaVersion);
050        }
051
052        static List<String> createDropIndexSql(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, String theIndexName, DriverTypeEnum theDriverType) throws SQLException {
053                Validate.notBlank(theIndexName, "theIndexName must not be blank");
054                Validate.notBlank(theTableName, "theTableName must not be blank");
055
056                if (!JdbcUtils.getIndexNames(theConnectionProperties, theTableName).contains(theIndexName)) {
057                        return Collections.emptyList();
058                }
059
060                boolean isUnique = JdbcUtils.isIndexUnique(theConnectionProperties, theTableName, theIndexName);
061
062                List<String> sql = new ArrayList<>();
063
064                if (isUnique) {
065                        // Drop constraint
066                        switch (theDriverType) {
067                                case MYSQL_5_7:
068                                case MARIADB_10_1:
069                                        // Need to quote the index name as the word "PRIMARY" is reserved in MySQL
070                                        sql.add("alter table " + theTableName + " drop index `" + theIndexName + "`");
071                                        break;
072                                case H2_EMBEDDED:
073                                        sql.add("drop index " + theIndexName);
074                                        break;
075                                case DERBY_EMBEDDED:
076                                        sql.add("alter table " + theTableName + " drop constraint " + theIndexName);
077                                        break;
078                                case ORACLE_12C:
079                                        sql.add("drop index " + theIndexName);
080                                        break;
081                                case MSSQL_2012:
082                                        sql.add("drop index " + theIndexName + " on " + theTableName);
083                                        break;
084                                case POSTGRES_9_4:
085                                        sql.add("alter table " + theTableName + " drop constraint if exists " + theIndexName + " cascade");
086                                        sql.add("drop index if exists " + theIndexName + " cascade");
087                                        break;
088                        }
089                } else {
090                        // Drop index
091                        switch (theDriverType) {
092                                case MYSQL_5_7:
093                                case MARIADB_10_1:
094                                        sql.add("alter table " + theTableName + " drop index " + theIndexName);
095                                        break;
096                                case POSTGRES_9_4:
097                                case DERBY_EMBEDDED:
098                                case H2_EMBEDDED:
099                                case ORACLE_12C:
100                                        sql.add("drop index " + theIndexName);
101                                        break;
102                                case MSSQL_2012:
103                                        sql.add("drop index " + theTableName + "." + theIndexName);
104                                        break;
105                        }
106                }
107                return sql;
108        }
109
110        @Override
111        public void validate() {
112                super.validate();
113                Validate.notBlank(myIndexName, "The index name must not be blank");
114
115                setDescription("Drop index " + myIndexName + " from table " + getTableName());
116        }
117
118        @Override
119        public void doExecute() throws SQLException {
120                /*
121                 * Derby and H2 both behave a bit weirdly if you create a unique constraint
122                 * using the @UniqueConstraint annotation in hibernate - They will create a
123                 * constraint with that name, but will then create a shadow index with a different
124                 * name, and it's that different name that gets reported when you query for the
125                 * list of indexes.
126                 *
127                 * For example, on H2 if you create a constraint named "IDX_FOO", the system
128                 * will create an index named "IDX_FOO_INDEX_A" and a constraint named "IDX_FOO".
129                 *
130                 * The following is a solution that uses appropriate native queries to detect
131                 * on the given platforms whether an index name actually corresponds to a
132                 * constraint, and delete that constraint.
133                 */
134
135                if (getDriverType() == DriverTypeEnum.H2_EMBEDDED) {
136                        @Language("SQL") String findConstraintSql = "SELECT DISTINCT constraint_name FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE constraint_name = ? AND table_name = ?";
137                        @Language("SQL") String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?";
138                        findAndDropConstraint(findConstraintSql, dropConstraintSql);
139                } else if (getDriverType() == DriverTypeEnum.DERBY_EMBEDDED) {
140                        @Language("SQL") String findConstraintSql = "SELECT c.constraintname FROM sys.sysconstraints c, sys.systables t WHERE c.tableid = t.tableid AND c.constraintname = ? AND t.tablename = ?";
141                        @Language("SQL") String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?";
142                        findAndDropConstraint(findConstraintSql, dropConstraintSql);
143                } else if (getDriverType() == DriverTypeEnum.ORACLE_12C) {
144                        @Language("SQL") String findConstraintSql = "SELECT DISTINCT constraint_name FROM user_cons_columns WHERE constraint_name = ? AND table_name = ?";
145                        @Language("SQL") String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?";
146                        findAndDropConstraint(findConstraintSql, dropConstraintSql);
147                        findConstraintSql = "SELECT DISTINCT constraint_name FROM all_constraints WHERE index_name = ? AND table_name = ?";
148                        findAndDropConstraint(findConstraintSql, dropConstraintSql);
149                } else if (getDriverType() == DriverTypeEnum.MSSQL_2012) {
150                        // Legacy deletion for SQL Server unique indexes
151                        @Language("SQL") String findConstraintSql = "SELECT tc.CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc WHERE tc.CONSTRAINT_NAME = ? AND tc.TABLE_NAME = ?";
152                        @Language("SQL") String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?";
153                        findAndDropConstraint(findConstraintSql, dropConstraintSql);
154                }
155
156                Set<String> indexNames = JdbcUtils.getIndexNames(getConnectionProperties(), getTableName());
157
158                if (!indexNames.contains(myIndexName)) {
159                        logInfo(ourLog, "Index {} does not exist on table {} - No action needed", myIndexName, getTableName());
160                        return;
161                }
162
163                boolean isUnique = JdbcUtils.isIndexUnique(getConnectionProperties(), getTableName(), myIndexName);
164                String uniquenessString = isUnique ? "unique" : "non-unique";
165
166                List<String> sqls = createDropIndexSql(getConnectionProperties(), getTableName(), myIndexName, getDriverType());
167                if (!sqls.isEmpty()) {
168                        logInfo(ourLog, "Dropping {} index {} on table {}", uniquenessString, myIndexName, getTableName());
169                }
170                for (@Language("SQL") String sql : sqls) {
171                        executeSql(getTableName(), sql);
172                }
173        }
174
175        public void findAndDropConstraint(String theFindConstraintSql, String theDropConstraintSql) {
176                DataSource dataSource = Objects.requireNonNull(getConnectionProperties().getDataSource());
177                getConnectionProperties().getTxTemplate().executeWithoutResult(t -> {
178                        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
179                        RowMapperResultSetExtractor<String> resultSetExtractor = new RowMapperResultSetExtractor<>(new SingleColumnRowMapper<>(String.class));
180                        List<String> outcome = jdbcTemplate.query(theFindConstraintSql, new Object[]{myIndexName, getTableName()}, resultSetExtractor);
181                        assert outcome != null;
182                        for (String next : outcome) {
183                                String sql = theDropConstraintSql.replace("?", next);
184                                executeSql(getTableName(), sql);
185                        }
186                });
187        }
188
189        public DropIndexTask setIndexName(String theIndexName) {
190                myIndexName = theIndexName;
191                return this;
192        }
193
194        @Override
195        protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) {
196                DropIndexTask otherObject = (DropIndexTask) theOtherObject;
197                super.generateEquals(theBuilder, otherObject);
198                theBuilder.append(myIndexName, otherObject.myIndexName);
199        }
200
201        @Override
202        protected void generateHashCode(HashCodeBuilder theBuilder) {
203                super.generateHashCode(theBuilder);
204                theBuilder.append(myIndexName);
205        }
206}