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 org.apache.commons.lang3.builder.EqualsBuilder;
023import org.apache.commons.lang3.builder.HashCodeBuilder;
024import org.apache.commons.lang3.tuple.Pair;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027import org.springframework.jdbc.core.JdbcTemplate;
028
029import java.sql.SQLException;
030
031/**
032 * Fix for bad version of {@link ForceIdMigrationCopyTask}
033 * The earlier migration had used at cast to char instead of varchar, which is space-padded on Oracle.
034 * This migration includes the copy action, but also adds a trim() call to fixup the bad server-assigned ids.
035 */
036public class ForceIdMigrationFixTask extends BaseTask {
037        private static final Logger ourLog = LoggerFactory.getLogger(ForceIdMigrationFixTask.class);
038
039        public ForceIdMigrationFixTask(String theProductVersion, String theSchemaVersion) {
040                super(theProductVersion, theSchemaVersion);
041        }
042
043        @Override
044        public void validate() {
045                // no-op
046        }
047
048        @Override
049        protected void doExecute() throws SQLException {
050                logInfo(ourLog, "Starting: migrate fhir_id from hfj_forced_id to hfj_resource.fhir_id");
051
052                JdbcTemplate jdbcTemplate = newJdbcTemplate();
053
054                Pair<Long, Long> range = jdbcTemplate.queryForObject(
055                                "select min(RES_ID), max(RES_ID) from HFJ_RESOURCE",
056                                (rs, rowNum) -> Pair.of(rs.getLong(1), rs.getLong(2)));
057
058                if (range == null || range.getLeft() == null) {
059                        logInfo(ourLog, "HFJ_RESOURCE is empty.  No work to do.");
060                        return;
061                }
062
063                // run update in batches.
064                int rowsPerBlock = 50; // hfj_resource has roughly 50 rows per 8k block.
065                int batchSize = rowsPerBlock * 2000; // a few thousand IOPS gives a batch size around a second.
066                ourLog.info(
067                                "About to migrate ids from {} to {} in batches of size {}",
068                                range.getLeft(),
069                                range.getRight(),
070                                batchSize);
071                for (long batchStart = range.getLeft(); batchStart <= range.getRight(); batchStart = batchStart + batchSize) {
072                        long batchEnd = batchStart + batchSize;
073                        ourLog.info("Migrating client-assigned ids for pids: {}-{}", batchStart, batchEnd);
074
075                        /*
076                        We have several cases.  Two require no action:
077                        1. client-assigned id, with correct value in fhir_id and row in hfj_forced_id
078                        2. server-assigned id, with correct value in fhir_id, no row in hfj_forced_id
079                        And three require action:
080                        3. client-assigned id, no value in fhir_id, but row in hfj_forced_id
081                        4. server-assigned id, no value in fhir_id, and row in hfj_forced_id
082                        5. bad migration - server-assigned id, with wrong space-padded value in fhir_id, no row in hfj_forced_id
083                         */
084
085                        executeSql(
086                                        "hfj_resource",
087                                        "update hfj_resource " +
088                                                        // coalesce is varargs and chooses the first non-null value, like ||
089                                                        " set fhir_id = coalesce( "
090                                                        +
091                                                        // case 5.
092                                                        " trim(fhir_id), "
093                                                        +
094                                                        // case 3
095                                                        " (select f.forced_id from hfj_forced_id f where f.resource_pid = res_id), "
096                                                        +
097                                                        // case 4 - use pid as fhir_id
098                                                        "   cast(res_id as varchar(64)) "
099                                                        + "  ) "
100                                                        +
101                                                        // avoid useless updates on engines that don't check
102                                                        // skip case 1, 2.  Only check 3,4,5
103                                                        " where (fhir_id is null or fhir_id <> trim(fhir_id)) "
104                                                        +
105                                                        // chunk range.
106                                                        " and res_id >= ? and res_id < ?",
107                                        batchStart,
108                                        batchEnd);
109                }
110        }
111
112        @Override
113        protected void generateHashCode(HashCodeBuilder theBuilder) {
114                // no-op - this is a singleton.
115        }
116
117        @Override
118        protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) {
119                // no-op - this is a singleton.
120        }
121}