001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2024 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 getWhereClauseByDBType() 104 + 105 // chunk range. 106 " and res_id >= ? and res_id < ?", 107 batchStart, 108 batchEnd); 109 } 110 } 111 112 private String getWhereClauseByDBType() { 113 switch (getDriverType()) { 114 case MSSQL_2012: 115 return " where (fhir_id is null or DATALENGTH(fhir_id) > LEN(fhir_id)) "; 116 case H2_EMBEDDED: 117 case DERBY_EMBEDDED: 118 case MARIADB_10_1: 119 case MYSQL_5_7: 120 case POSTGRES_9_4: 121 case ORACLE_12C: 122 case COCKROACHDB_21_1: 123 default: 124 return " where (fhir_id is null or fhir_id <> trim(fhir_id)) "; 125 } 126 } 127 128 @Override 129 protected void generateHashCode(HashCodeBuilder theBuilder) { 130 // no-op - this is a singleton. 131 } 132 133 @Override 134 protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) { 135 // no-op - this is a singleton. 136 } 137}