Merge remote-tracking branch 'origin/master' into 4634-rule-builder
This commit is contained in:
commit
f22c7c100f
|
@ -0,0 +1,31 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class for thread sleeps.
|
||||||
|
* Uses non-static methods for easier mocking and unnecessary waits in unit tests
|
||||||
|
*/
|
||||||
|
public class SleepUtil {
|
||||||
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SleepUtil.class);
|
||||||
|
|
||||||
|
public void sleepAtLeast(long theMillis) {
|
||||||
|
sleepAtLeast(theMillis, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("BusyWait")
|
||||||
|
public void sleepAtLeast(long theMillis, boolean theLogProgress) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (System.currentTimeMillis() <= start + theMillis) {
|
||||||
|
try {
|
||||||
|
long timeSinceStarted = System.currentTimeMillis() - start;
|
||||||
|
long timeToSleep = Math.max(0, theMillis - timeSinceStarted);
|
||||||
|
if (theLogProgress) {
|
||||||
|
ourLog.info("Sleeping for {}ms", timeToSleep);
|
||||||
|
}
|
||||||
|
Thread.sleep(timeToSleep);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
ourLog.error("Interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,9 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||||
|
|
||||||
public class TestUtil {
|
public class TestUtil {
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class);
|
||||||
|
|
||||||
|
private static SleepUtil ourSleepUtil = new SleepUtil();
|
||||||
|
|
||||||
private static boolean ourShouldRandomizeTimezones = true;
|
private static boolean ourShouldRandomizeTimezones = true;
|
||||||
|
|
||||||
public static void setShouldRandomizeTimezones(boolean theShouldRandomizeTimezones) {
|
public static void setShouldRandomizeTimezones(boolean theShouldRandomizeTimezones) {
|
||||||
|
@ -135,25 +138,22 @@ public class TestUtil {
|
||||||
return stripReturns(theString).replace(" ", "");
|
return stripReturns(theString).replace(" ", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* In production code, instead of this static method, it is better to use an instance of SleepUtil.
|
||||||
|
* Since SleepUtil isn't using static methods, it is easier to mock for unit test and avoid unnecessary waits in
|
||||||
|
* unit tests
|
||||||
|
*/
|
||||||
public static void sleepAtLeast(long theMillis) {
|
public static void sleepAtLeast(long theMillis) {
|
||||||
sleepAtLeast(theMillis, true);
|
ourSleepUtil.sleepAtLeast(theMillis);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("BusyWait")
|
/**
|
||||||
|
* In production code, instead of this static method, it is better to use an instance of SleepUtil.
|
||||||
|
* Since SleepUtil isn't using static methods, it is easier to mock for unit test and avoid unnecessary waits in
|
||||||
|
* unit tests
|
||||||
|
*/
|
||||||
public static void sleepAtLeast(long theMillis, boolean theLogProgress) {
|
public static void sleepAtLeast(long theMillis, boolean theLogProgress) {
|
||||||
long start = System.currentTimeMillis();
|
ourSleepUtil.sleepAtLeast(theMillis, theLogProgress);
|
||||||
while (System.currentTimeMillis() <= start + theMillis) {
|
|
||||||
try {
|
|
||||||
long timeSinceStarted = System.currentTimeMillis() - start;
|
|
||||||
long timeToSleep = Math.max(0, theMillis - timeSinceStarted);
|
|
||||||
if (theLogProgress) {
|
|
||||||
ourLog.info("Sleeping for {}ms", timeToSleep);
|
|
||||||
}
|
|
||||||
Thread.sleep(timeToSleep);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
ourLog.error("Interrupted", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class SleepUtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSleepAtLeast() {
|
||||||
|
SleepUtil sleepUtil = new SleepUtil();
|
||||||
|
long amountToSleepMs = 10;
|
||||||
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
sleepUtil.sleepAtLeast(amountToSleepMs);
|
||||||
|
long stop = System.currentTimeMillis();
|
||||||
|
|
||||||
|
long actualSleepDurationMs = stop - start;
|
||||||
|
assertTrue(actualSleepDurationMs >= amountToSleepMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testZeroMs() {
|
||||||
|
// 0 is a valid input
|
||||||
|
SleepUtil sleepUtil = new SleepUtil();
|
||||||
|
sleepUtil.sleepAtLeast(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 5553
|
||||||
|
title: "mdm-clear jobs are prone to failing because of deadlocks when running on SQL Server. Such job failures have
|
||||||
|
been mitigated to some extent by increasing the retries on deadlocks."
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
type: perf
|
||||||
|
issue: 5555
|
||||||
|
title: "Previously, resource body content went into one of 2 columns on the HFJ_RES_VER table:
|
||||||
|
RES_TEXT if the size was above a configurable threshold, or RES_TEXT_VC if it was below that
|
||||||
|
threshold. Performance testing has shown that the latter is always faster, and that on
|
||||||
|
Postgres the use of the latter is particularly problematic since it maps to the
|
||||||
|
largeobject table which isn't the recommended way of storing high frequency objects.
|
||||||
|
The configurable threshold is now ignored, and the latter column is always used. Any legacy
|
||||||
|
data in the former column will still be read however."
|
|
@ -19,7 +19,6 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.jpa.model.dialect;
|
package ca.uhn.fhir.jpa.model.dialect;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
|
||||||
import org.hibernate.dialect.DatabaseVersion;
|
import org.hibernate.dialect.DatabaseVersion;
|
||||||
import org.hibernate.dialect.H2Dialect;
|
import org.hibernate.dialect.H2Dialect;
|
||||||
|
|
||||||
|
@ -38,7 +37,7 @@ public class HapiFhirH2Dialect extends H2Dialect {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As of Hibernate 6, generated schemas include a column level check constraint that enforces valid values
|
* As of Hibernate 6, generated schemas include a column level check constraint that enforces valid values
|
||||||
* for columns that back an Enum type. For example, the column definition for {@link ResourceTable#getFhirVersion()}
|
* for columns that back an Enum type. For example, the column definition for <code>ResourceTable#getFhirVersion()</code>
|
||||||
* would look like:
|
* would look like:
|
||||||
* <pre>
|
* <pre>
|
||||||
* RES_VERSION varchar(7) check (RES_VERSION in ('DSTU2','DSTU2_HL7ORG','DSTU2_1','DSTU3','R4','R4B','R5')),
|
* RES_VERSION varchar(7) check (RES_VERSION in ('DSTU2','DSTU2_HL7ORG','DSTU2_1','DSTU3','R4','R4B','R5')),
|
|
@ -48,8 +48,10 @@ public final class HapiEntityManagerFactoryUtil {
|
||||||
ConfigurableListableBeanFactory myConfigurableListableBeanFactory,
|
ConfigurableListableBeanFactory myConfigurableListableBeanFactory,
|
||||||
FhirContext theFhirContext,
|
FhirContext theFhirContext,
|
||||||
JpaStorageSettings theStorageSettings) {
|
JpaStorageSettings theStorageSettings) {
|
||||||
|
|
||||||
LocalContainerEntityManagerFactoryBean retVal =
|
LocalContainerEntityManagerFactoryBean retVal =
|
||||||
new HapiFhirLocalContainerEntityManagerFactoryBean(myConfigurableListableBeanFactory);
|
new HapiFhirLocalContainerEntityManagerFactoryBean(myConfigurableListableBeanFactory);
|
||||||
|
|
||||||
configureEntityManagerFactory(retVal, theFhirContext, theStorageSettings);
|
configureEntityManagerFactory(retVal, theFhirContext, theStorageSettings);
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,9 +148,7 @@ import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -645,7 +643,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
theEntity.setResourceType(toResourceName(theResource));
|
theEntity.setResourceType(toResourceName(theResource));
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] resourceBinary;
|
|
||||||
String resourceText;
|
String resourceText;
|
||||||
ResourceEncodingEnum encoding;
|
ResourceEncodingEnum encoding;
|
||||||
boolean changed = false;
|
boolean changed = false;
|
||||||
|
@ -662,7 +659,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
if (address != null) {
|
if (address != null) {
|
||||||
|
|
||||||
encoding = ResourceEncodingEnum.ESR;
|
encoding = ResourceEncodingEnum.ESR;
|
||||||
resourceBinary = null;
|
|
||||||
resourceText = address.getProviderId() + ":" + address.getLocation();
|
resourceText = address.getProviderId() + ":" + address.getLocation();
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
||||||
|
@ -680,19 +676,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
theEntity.setFhirVersion(myContext.getVersion().getVersion());
|
theEntity.setFhirVersion(myContext.getVersion().getVersion());
|
||||||
|
|
||||||
HashFunction sha256 = Hashing.sha256();
|
HashFunction sha256 = Hashing.sha256();
|
||||||
HashCode hashCode;
|
resourceText = encodeResource(theResource, encoding, excludeElements, myContext);
|
||||||
String encodedResource = encodeResource(theResource, encoding, excludeElements, myContext);
|
|
||||||
if (myStorageSettings.getInlineResourceTextBelowSize() > 0
|
|
||||||
&& encodedResource.length() < myStorageSettings.getInlineResourceTextBelowSize()) {
|
|
||||||
resourceText = encodedResource;
|
|
||||||
resourceBinary = null;
|
|
||||||
encoding = ResourceEncodingEnum.JSON;
|
encoding = ResourceEncodingEnum.JSON;
|
||||||
hashCode = sha256.hashUnencodedChars(encodedResource);
|
HashCode hashCode = sha256.hashUnencodedChars(resourceText);
|
||||||
} else {
|
|
||||||
resourceText = null;
|
|
||||||
resourceBinary = getResourceBinary(encoding, encodedResource);
|
|
||||||
hashCode = sha256.hashBytes(resourceBinary);
|
|
||||||
}
|
|
||||||
|
|
||||||
String hashSha256 = hashCode.toString();
|
String hashSha256 = hashCode.toString();
|
||||||
if (!hashSha256.equals(theEntity.getHashSha256())) {
|
if (!hashSha256.equals(theEntity.getHashSha256())) {
|
||||||
|
@ -710,7 +696,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
encoding = null;
|
encoding = null;
|
||||||
resourceBinary = null;
|
|
||||||
resourceText = null;
|
resourceText = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -728,7 +713,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceBinary = null;
|
|
||||||
resourceText = null;
|
resourceText = null;
|
||||||
encoding = ResourceEncodingEnum.DEL;
|
encoding = ResourceEncodingEnum.DEL;
|
||||||
}
|
}
|
||||||
|
@ -753,46 +737,19 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
if (currentHistoryVersion == null || !currentHistoryVersion.hasResource()) {
|
if (currentHistoryVersion == null || !currentHistoryVersion.hasResource()) {
|
||||||
changed = true;
|
changed = true;
|
||||||
} else {
|
} else {
|
||||||
changed = !Arrays.equals(currentHistoryVersion.getResource(), resourceBinary);
|
changed = !StringUtils.equals(currentHistoryVersion.getResourceTextVc(), resourceText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EncodedResource retVal = new EncodedResource();
|
EncodedResource retVal = new EncodedResource();
|
||||||
retVal.setEncoding(encoding);
|
retVal.setEncoding(encoding);
|
||||||
retVal.setResourceBinary(resourceBinary);
|
|
||||||
retVal.setResourceText(resourceText);
|
retVal.setResourceText(resourceText);
|
||||||
retVal.setChanged(changed);
|
retVal.setChanged(changed);
|
||||||
|
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* helper for returning the encoded byte array of the input resource string based on the encoding.
|
|
||||||
*
|
|
||||||
* @param encoding the encoding to used
|
|
||||||
* @param encodedResource the resource to encode
|
|
||||||
* @return byte array of the resource
|
|
||||||
*/
|
|
||||||
@Nonnull
|
|
||||||
private byte[] getResourceBinary(ResourceEncodingEnum encoding, String encodedResource) {
|
|
||||||
byte[] resourceBinary;
|
|
||||||
switch (encoding) {
|
|
||||||
case JSON:
|
|
||||||
resourceBinary = encodedResource.getBytes(StandardCharsets.UTF_8);
|
|
||||||
break;
|
|
||||||
case JSONC:
|
|
||||||
resourceBinary = GZipUtil.compress(encodedResource);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
case DEL:
|
|
||||||
case ESR:
|
|
||||||
resourceBinary = new byte[0];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return resourceBinary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* helper to format the meta element for serialization of the resource.
|
* helper to format the meta element for serialization of the resource.
|
||||||
*
|
*
|
||||||
|
@ -1437,8 +1394,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
List<String> excludeElements = new ArrayList<>(8);
|
List<String> excludeElements = new ArrayList<>(8);
|
||||||
getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta());
|
getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta());
|
||||||
String encodedResourceString = encodeResource(theResource, encoding, excludeElements, myContext);
|
String encodedResourceString = encodeResource(theResource, encoding, excludeElements, myContext);
|
||||||
byte[] resourceBinary = getResourceBinary(encoding, encodedResourceString);
|
boolean changed = !StringUtils.equals(historyEntity.getResourceTextVc(), encodedResourceString);
|
||||||
boolean changed = !Arrays.equals(historyEntity.getResource(), resourceBinary);
|
|
||||||
|
|
||||||
historyEntity.setUpdated(theTransactionDetails.getTransactionDate());
|
historyEntity.setUpdated(theTransactionDetails.getTransactionDate());
|
||||||
|
|
||||||
|
@ -1450,19 +1406,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
return historyEntity;
|
return historyEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getStorageSettings().getInlineResourceTextBelowSize() > 0
|
populateEncodedResource(encodedResource, encodedResourceString, ResourceEncodingEnum.JSON);
|
||||||
&& encodedResourceString.length() < getStorageSettings().getInlineResourceTextBelowSize()) {
|
|
||||||
populateEncodedResource(encodedResource, encodedResourceString, null, ResourceEncodingEnum.JSON);
|
|
||||||
} else {
|
|
||||||
populateEncodedResource(encodedResource, null, resourceBinary, encoding);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Save the resource itself to the resourceHistoryTable
|
* Save the resource itself to the resourceHistoryTable
|
||||||
*/
|
*/
|
||||||
historyEntity = myEntityManager.merge(historyEntity);
|
historyEntity = myEntityManager.merge(historyEntity);
|
||||||
historyEntity.setEncoding(encodedResource.getEncoding());
|
historyEntity.setEncoding(encodedResource.getEncoding());
|
||||||
historyEntity.setResource(encodedResource.getResourceBinary());
|
|
||||||
historyEntity.setResourceTextVc(encodedResource.getResourceText());
|
historyEntity.setResourceTextVc(encodedResource.getResourceText());
|
||||||
myResourceHistoryTableDao.save(historyEntity);
|
myResourceHistoryTableDao.save(historyEntity);
|
||||||
|
|
||||||
|
@ -1472,12 +1423,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateEncodedResource(
|
private void populateEncodedResource(
|
||||||
EncodedResource encodedResource,
|
EncodedResource encodedResource, String encodedResourceString, ResourceEncodingEnum theEncoding) {
|
||||||
String encodedResourceString,
|
|
||||||
byte[] theResourceBinary,
|
|
||||||
ResourceEncodingEnum theEncoding) {
|
|
||||||
encodedResource.setResourceText(encodedResourceString);
|
encodedResource.setResourceText(encodedResourceString);
|
||||||
encodedResource.setResourceBinary(theResourceBinary);
|
|
||||||
encodedResource.setEncoding(theEncoding);
|
encodedResource.setEncoding(theEncoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1542,7 +1489,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
|
||||||
}
|
}
|
||||||
|
|
||||||
historyEntry.setEncoding(theChanged.getEncoding());
|
historyEntry.setEncoding(theChanged.getEncoding());
|
||||||
historyEntry.setResource(theChanged.getResourceBinary());
|
|
||||||
historyEntry.setResourceTextVc(theChanged.getResourceText());
|
historyEntry.setResourceTextVc(theChanged.getResourceText());
|
||||||
|
|
||||||
ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId());
|
ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId());
|
||||||
|
|
|
@ -1689,21 +1689,19 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
if (historyEntity.getEncoding() == ResourceEncodingEnum.JSONC
|
if (historyEntity.getEncoding() == ResourceEncodingEnum.JSONC
|
||||||
|| historyEntity.getEncoding() == ResourceEncodingEnum.JSON) {
|
|| historyEntity.getEncoding() == ResourceEncodingEnum.JSON) {
|
||||||
byte[] resourceBytes = historyEntity.getResource();
|
byte[] resourceBytes = historyEntity.getResource();
|
||||||
|
|
||||||
|
// Always migrate data out of the bytes column
|
||||||
if (resourceBytes != null) {
|
if (resourceBytes != null) {
|
||||||
String resourceText = decodeResource(resourceBytes, historyEntity.getEncoding());
|
String resourceText = decodeResource(resourceBytes, historyEntity.getEncoding());
|
||||||
if (myStorageSettings.getInlineResourceTextBelowSize() > 0
|
|
||||||
&& resourceText.length() < myStorageSettings.getInlineResourceTextBelowSize()) {
|
|
||||||
ourLog.debug(
|
ourLog.debug(
|
||||||
"Storing text of resource {} version {} as inline VARCHAR",
|
"Storing text of resource {} version {} as inline VARCHAR",
|
||||||
entity.getResourceId(),
|
entity.getResourceId(),
|
||||||
historyEntity.getVersion());
|
historyEntity.getVersion());
|
||||||
historyEntity.setResourceTextVc(resourceText);
|
historyEntity.setResourceTextVc(resourceText);
|
||||||
historyEntity.setResource(null);
|
|
||||||
historyEntity.setEncoding(ResourceEncodingEnum.JSON);
|
historyEntity.setEncoding(ResourceEncodingEnum.JSON);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (isBlank(historyEntity.getSourceUri()) && isBlank(historyEntity.getRequestId())) {
|
if (isBlank(historyEntity.getSourceUri()) && isBlank(historyEntity.getRequestId())) {
|
||||||
if (historyEntity.getProvenance() != null) {
|
if (historyEntity.getProvenance() != null) {
|
||||||
historyEntity.setSourceUri(historyEntity.getProvenance().getSourceUri());
|
historyEntity.setSourceUri(historyEntity.getProvenance().getSourceUri());
|
||||||
|
@ -2071,6 +2069,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public <PID extends IResourcePersistentId<?>> Stream<PID> searchForIdStream(
|
public <PID extends IResourcePersistentId<?>> Stream<PID> searchForIdStream(
|
||||||
SearchParameterMap theParams,
|
SearchParameterMap theParams,
|
||||||
RequestDetails theRequest,
|
RequestDetails theRequest,
|
||||||
|
|
|
@ -24,7 +24,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
|
||||||
class EncodedResource {
|
class EncodedResource {
|
||||||
|
|
||||||
private boolean myChanged;
|
private boolean myChanged;
|
||||||
private byte[] myResource;
|
|
||||||
private ResourceEncodingEnum myEncoding;
|
private ResourceEncodingEnum myEncoding;
|
||||||
private String myResourceText;
|
private String myResourceText;
|
||||||
|
|
||||||
|
@ -36,14 +35,6 @@ class EncodedResource {
|
||||||
myEncoding = theEncoding;
|
myEncoding = theEncoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getResourceBinary() {
|
|
||||||
return myResource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setResourceBinary(byte[] theResource) {
|
|
||||||
myResource = theResource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isChanged() {
|
public boolean isChanged() {
|
||||||
return myChanged;
|
return myChanged;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,4 +79,16 @@ public interface IResourceHistoryTableDao extends JpaRepository<ResourceHistoryT
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("DELETE FROM ResourceHistoryTable t WHERE t.myId = :pid")
|
@Query("DELETE FROM ResourceHistoryTable t WHERE t.myId = :pid")
|
||||||
void deleteByPid(@Param("pid") Long theId);
|
void deleteByPid(@Param("pid") Long theId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is only for use in unit tests - It is used to move the stored resource body contents from the new
|
||||||
|
* <code>RES_TEXT_VC</code> column to the legacy <code>RES_TEXT</code> column, which is where data may have
|
||||||
|
* been stored by versions of HAPI FHIR prior to 7.0.0
|
||||||
|
*
|
||||||
|
* @since 7.0.0
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query(
|
||||||
|
"UPDATE ResourceHistoryTable r SET r.myResourceTextVc = null, r.myResource = :text, r.myEncoding = 'JSONC' WHERE r.myId = :pid")
|
||||||
|
void updateNonInlinedContents(@Param("text") byte[] theText, @Param("pid") long thePid);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1555,11 +1555,12 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
|
||||||
Builder.BuilderWithTableName nrmlTable = version.onTable("HFJ_SPIDX_QUANTITY_NRML");
|
Builder.BuilderWithTableName nrmlTable = version.onTable("HFJ_SPIDX_QUANTITY_NRML");
|
||||||
nrmlTable.addColumn("20210111.1", "PARTITION_ID").nullable().type(ColumnTypeEnum.INT);
|
nrmlTable.addColumn("20210111.1", "PARTITION_ID").nullable().type(ColumnTypeEnum.INT);
|
||||||
nrmlTable.addColumn("20210111.2", "PARTITION_DATE").nullable().type(ColumnTypeEnum.DATE_ONLY);
|
nrmlTable.addColumn("20210111.2", "PARTITION_DATE").nullable().type(ColumnTypeEnum.DATE_ONLY);
|
||||||
// - The fk name is generated from Hibernate, have to use this name here
|
// Disabled - superceded by 20220304.33
|
||||||
nrmlTable
|
nrmlTable
|
||||||
.addForeignKey("20210111.3", "FKRCJOVMUH5KC0O6FVBLE319PYV")
|
.addForeignKey("20210111.3", "FKRCJOVMUH5KC0O6FVBLE319PYV")
|
||||||
.toColumn("RES_ID")
|
.toColumn("RES_ID")
|
||||||
.references("HFJ_RESOURCE", "RES_ID");
|
.references("HFJ_RESOURCE", "RES_ID")
|
||||||
|
.doNothing();
|
||||||
|
|
||||||
Builder.BuilderWithTableName quantityTable = version.onTable("HFJ_SPIDX_QUANTITY");
|
Builder.BuilderWithTableName quantityTable = version.onTable("HFJ_SPIDX_QUANTITY");
|
||||||
quantityTable
|
quantityTable
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package ca.uhn.fhir.jpa.entity;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.util.ClasspathUtil;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
public class GeneratedSchemaTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure that the RES_TEXT_VC column, which is supposed to be an unlimited-length
|
||||||
|
* string datatype, actually uses an appropriate datatype on the various databases
|
||||||
|
* we care about.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testVerifyLongVarcharColumnDefinition() {
|
||||||
|
validateLongVarcharDatatype("cockroachdb.sql", "varchar(2147483647)");
|
||||||
|
validateLongVarcharDatatype("derby.sql", "clob");
|
||||||
|
validateLongVarcharDatatype("mysql.sql", "longtext");
|
||||||
|
validateLongVarcharDatatype("mariadb.sql", "longtext");
|
||||||
|
|
||||||
|
validateLongVarcharDatatype("h2.sql", "clob");
|
||||||
|
validateLongVarcharDatatype("postgres.sql", "text");
|
||||||
|
validateLongVarcharDatatype("oracle.sql", "clob");
|
||||||
|
validateLongVarcharDatatype("sqlserver.sql", "varchar(max)");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateLongVarcharDatatype(String schemaName, String expectedDatatype) {
|
||||||
|
String schema = ClasspathUtil.loadResource("ca/uhn/hapi/fhir/jpa/docs/database/" + schemaName);
|
||||||
|
String[] lines = StringUtils.split(schema, '\n');
|
||||||
|
String resTextVc = Arrays.stream(lines).filter(t -> t.contains("RES_TEXT_VC ")).findFirst().orElseThrow();
|
||||||
|
assertThat("Wrong type in " + schemaName, resTextVc, containsString("RES_TEXT_VC " + expectedDatatype));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,14 +25,15 @@ import ca.uhn.fhir.rest.api.Constants;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||||
import org.hibernate.annotations.JdbcTypeCode;
|
import org.hibernate.Length;
|
||||||
import org.hibernate.annotations.OptimisticLock;
|
import org.hibernate.annotations.OptimisticLock;
|
||||||
import org.hibernate.type.SqlTypes;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = ResourceHistoryTable.HFJ_RES_VER,
|
name = ResourceHistoryTable.HFJ_RES_VER,
|
||||||
|
@ -57,7 +58,6 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
|
||||||
public static final int ENCODING_COL_LENGTH = 5;
|
public static final int ENCODING_COL_LENGTH = 5;
|
||||||
|
|
||||||
public static final String HFJ_RES_VER = "HFJ_RES_VER";
|
public static final String HFJ_RES_VER = "HFJ_RES_VER";
|
||||||
public static final int RES_TEXT_VC_MAX_LENGTH = 4000;
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
@ -86,13 +86,15 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
|
||||||
@OneToMany(mappedBy = "myResourceHistory", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
|
@OneToMany(mappedBy = "myResourceHistory", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
|
||||||
private Collection<ResourceHistoryTag> myTags;
|
private Collection<ResourceHistoryTag> myTags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: No setter for this field because it's only a legacy way of storing data now.
|
||||||
|
*/
|
||||||
@Column(name = "RES_TEXT", length = Integer.MAX_VALUE - 1, nullable = true)
|
@Column(name = "RES_TEXT", length = Integer.MAX_VALUE - 1, nullable = true)
|
||||||
@Lob()
|
@Lob()
|
||||||
@OptimisticLock(excluded = true)
|
@OptimisticLock(excluded = true)
|
||||||
private byte[] myResource;
|
private byte[] myResource;
|
||||||
|
|
||||||
@Column(name = "RES_TEXT_VC", length = RES_TEXT_VC_MAX_LENGTH, nullable = true)
|
@Column(name = "RES_TEXT_VC", nullable = true, length = Length.LONG32)
|
||||||
@JdbcTypeCode(SqlTypes.LONG32VARCHAR)
|
|
||||||
@OptimisticLock(excluded = true)
|
@OptimisticLock(excluded = true)
|
||||||
private String myResourceTextVc;
|
private String myResourceTextVc;
|
||||||
|
|
||||||
|
@ -153,7 +155,8 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResourceTextVc(String theResourceTextVc) {
|
public void setResourceTextVc(String theResourceTextVc) {
|
||||||
myResourceTextVc = theResourceTextVc;
|
myResource = null;
|
||||||
|
myResourceTextVc = defaultString(theResourceTextVc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceHistoryProvenanceEntity getProvenance() {
|
public ResourceHistoryProvenanceEntity getProvenance() {
|
||||||
|
@ -209,10 +212,6 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
|
||||||
return myResource;
|
return myResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResource(byte[] theResource) {
|
|
||||||
myResource = theResource;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long getResourceId() {
|
public Long getResourceId() {
|
||||||
return myResourceId;
|
return myResourceId;
|
||||||
|
|
|
@ -41,14 +41,12 @@ public class ConsumeFilesStepR4Test extends BasePartitioningR4Test {
|
||||||
public void before() throws Exception {
|
public void before() throws Exception {
|
||||||
super.before();
|
super.before();
|
||||||
myPartitionSettings.setPartitioningEnabled(false);
|
myPartitionSettings.setPartitioningEnabled(false);
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(10000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@Override
|
@Override
|
||||||
public void after() {
|
public void after() {
|
||||||
super.after();
|
super.after();
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(new JpaStorageSettings().getInlineResourceTextBelowSize());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -91,7 +91,6 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
|
||||||
myStorageSettings.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
|
myStorageSettings.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
|
||||||
myStorageSettings.setIndexOnContainedResources(new JpaStorageSettings().isIndexOnContainedResources());
|
myStorageSettings.setIndexOnContainedResources(new JpaStorageSettings().isIndexOnContainedResources());
|
||||||
myStorageSettings.setIndexOnContainedResourcesRecursively(new JpaStorageSettings().isIndexOnContainedResourcesRecursively());
|
myStorageSettings.setIndexOnContainedResourcesRecursively(new JpaStorageSettings().isIndexOnContainedResourcesRecursively());
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(new JpaStorageSettings().getInlineResourceTextBelowSize());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -1,102 +1,61 @@
|
||||||
package ca.uhn.fhir.jpa.dao.r4;
|
package ca.uhn.fhir.jpa.dao.r4;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
import ca.uhn.fhir.jpa.dao.GZipUtil;
|
||||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
|
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import ca.uhn.fhir.rest.param.HistorySearchDateRangeParam;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
public class FhirResourceDaoR4InlineResourceModeTest extends BaseJpaR4Test {
|
public class FhirResourceDaoR4InlineResourceModeTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void beforeSetDao() {
|
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
public void afterResetDao() {
|
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(new JpaStorageSettings().getInlineResourceTextBelowSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateWithInlineResourceTextStorage() {
|
public void testRetrieveNonInlinedResource() {
|
||||||
Patient patient = new Patient();
|
IIdType id = createPatient(withActiveTrue());
|
||||||
patient.setActive(true);
|
Long pid = id.getIdPartAsLong();
|
||||||
Long resourceId = myPatientDao.create(patient).getId().getIdPartAsLong();
|
|
||||||
|
|
||||||
patient = new Patient();
|
relocateResourceTextToCompressedColumn(pid, 1L);
|
||||||
patient.setId("Patient/" + resourceId);
|
|
||||||
patient.setActive(false);
|
|
||||||
myPatientDao.update(patient);
|
|
||||||
|
|
||||||
runInTransaction(()->{
|
runInTransaction(()->{
|
||||||
// Version 1
|
ResourceHistoryTable historyEntity = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(pid, 1);
|
||||||
ResourceHistoryTable entity = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(resourceId, 1);
|
assertNotNull(historyEntity.getResource());
|
||||||
assertNull(entity.getResource());
|
assertNull(historyEntity.getResourceTextVc());
|
||||||
assertThat(entity.getResourceTextVc(), containsString("\"active\":true"));
|
assertEquals(ResourceEncodingEnum.JSONC, historyEntity.getEncoding());
|
||||||
// Version 2
|
|
||||||
entity = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(resourceId, 2);
|
|
||||||
assertNull(entity.getResource());
|
|
||||||
assertThat(entity.getResourceTextVc(), containsString("\"active\":false"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
patient = myPatientDao.read(new IdType("Patient/" + resourceId));
|
// Read
|
||||||
assertFalse(patient.getActive());
|
validatePatient(myPatientDao.read(id.withVersion(null), mySrd));
|
||||||
|
|
||||||
patient = (Patient) myPatientDao.search(SearchParameterMap.newSynchronous()).getAllResources().get(0);
|
// VRead
|
||||||
assertFalse(patient.getActive());
|
validatePatient(myPatientDao.read(id.withVersion("1"), mySrd));
|
||||||
|
|
||||||
|
// Search (Sync)
|
||||||
|
validatePatient(myPatientDao.search(SearchParameterMap.newSynchronous(), mySrd).getResources(0, 1).get(0));
|
||||||
|
|
||||||
|
// Search (Async)
|
||||||
|
validatePatient(myPatientDao.search(new SearchParameterMap(), mySrd).getResources(0, 1).get(0));
|
||||||
|
|
||||||
|
// History
|
||||||
|
validatePatient(myPatientDao.history(id, new HistorySearchDateRangeParam(new HashMap<>(), new DateRangeParam(), 0), mySrd).getResources(0, 1).get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
private void validatePatient(IBaseResource theRead) {
|
||||||
public void testDontUseInlineAboveThreshold() {
|
assertTrue(((Patient)theRead).getActive());
|
||||||
String veryLongFamilyName = StringUtils.leftPad("", 6000, 'a');
|
|
||||||
|
|
||||||
Patient patient = new Patient();
|
|
||||||
patient.setActive(true);
|
|
||||||
patient.addName().setFamily(veryLongFamilyName);
|
|
||||||
Long resourceId = myPatientDao.create(patient).getId().getIdPartAsLong();
|
|
||||||
|
|
||||||
runInTransaction(() -> {
|
|
||||||
// Version 1
|
|
||||||
ResourceHistoryTable entity = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(resourceId, 1);
|
|
||||||
assertNotNull(entity.getResource());
|
|
||||||
assertNull(entity.getResourceTextVc());
|
|
||||||
});
|
|
||||||
|
|
||||||
patient = myPatientDao.read(new IdType("Patient/" + resourceId));
|
|
||||||
assertEquals(veryLongFamilyName, patient.getNameFirstRep().getFamily());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testNopOnUnchangedUpdate() {
|
|
||||||
Patient patient = new Patient();
|
|
||||||
patient.setActive(true);
|
|
||||||
Long resourceId = myPatientDao.create(patient).getId().getIdPartAsLong();
|
|
||||||
|
|
||||||
patient = new Patient();
|
|
||||||
patient.setId("Patient/" + resourceId);
|
|
||||||
patient.setActive(true);
|
|
||||||
DaoMethodOutcome updateOutcome = myPatientDao.update(patient);
|
|
||||||
assertEquals("1", updateOutcome.getId().getVersionIdPart());
|
|
||||||
assertTrue(updateOutcome.isNop());
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1031,26 +1031,15 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({
|
||||||
// NoOp OptimisticLock OptimizeMode ExpectedSelect ExpectedUpdate
|
// OptimisticLock OptimizeMode ExpectedSelect ExpectedUpdate
|
||||||
" false, false, CURRENT_VERSION, 2, 1",
|
" false, CURRENT_VERSION, 2, 0",
|
||||||
" true, false, CURRENT_VERSION, 2, 0",
|
" true, CURRENT_VERSION, 12, 0",
|
||||||
" false, true, CURRENT_VERSION, 12, 1",
|
" false, ALL_VERSIONS, 12, 0",
|
||||||
" true, true, CURRENT_VERSION, 12, 0",
|
" true, ALL_VERSIONS, 22, 0",
|
||||||
" false, false, ALL_VERSIONS, 12, 10",
|
|
||||||
" true, false, ALL_VERSIONS, 12, 0",
|
|
||||||
" false, true, ALL_VERSIONS, 22, 10",
|
|
||||||
" true, true, ALL_VERSIONS, 22, 0",
|
|
||||||
})
|
})
|
||||||
public void testReindexJob_OptimizeStorage(boolean theNoOp, boolean theOptimisticLock, ReindexParameters.OptimizeStorageModeEnum theOptimizeStorageModeEnum, int theExpectedSelectCount, int theExpectedUpdateCount) {
|
public void testReindexJob_OptimizeStorage(boolean theOptimisticLock, ReindexParameters.OptimizeStorageModeEnum theOptimizeStorageModeEnum, int theExpectedSelectCount, int theExpectedUpdateCount) {
|
||||||
// Setup
|
// Setup
|
||||||
|
|
||||||
// In no-op mode, the inlining is already in the state it needs to be in
|
|
||||||
if (theNoOp) {
|
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(10000);
|
|
||||||
} else {
|
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
ResourceIdListWorkChunkJson data = new ResourceIdListWorkChunkJson();
|
ResourceIdListWorkChunkJson data = new ResourceIdListWorkChunkJson();
|
||||||
IIdType patientId = createPatient(withActiveTrue());
|
IIdType patientId = createPatient(withActiveTrue());
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
|
|
|
@ -274,7 +274,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
|
||||||
ResourceHistoryTable newHistory = table.toHistory(true);
|
ResourceHistoryTable newHistory = table.toHistory(true);
|
||||||
ResourceHistoryTable currentHistory = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(table.getId(), 1L);
|
ResourceHistoryTable currentHistory = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(table.getId(), 1L);
|
||||||
newHistory.setEncoding(currentHistory.getEncoding());
|
newHistory.setEncoding(currentHistory.getEncoding());
|
||||||
newHistory.setResource(currentHistory.getResource());
|
newHistory.setResourceTextVc(currentHistory.getResourceTextVc());
|
||||||
myResourceHistoryTableDao.save(newHistory);
|
myResourceHistoryTableDao.save(newHistory);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2928,7 +2928,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
|
||||||
ResourceHistoryTable table = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 1L);
|
ResourceHistoryTable table = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 1L);
|
||||||
String newContent = myFhirContext.newJsonParser().encodeResourceToString(p);
|
String newContent = myFhirContext.newJsonParser().encodeResourceToString(p);
|
||||||
newContent = newContent.replace("male", "foo");
|
newContent = newContent.replace("male", "foo");
|
||||||
table.setResource(newContent.getBytes(Charsets.UTF_8));
|
table.setResourceTextVc(newContent);
|
||||||
table.setEncoding(ResourceEncodingEnum.JSON);
|
table.setEncoding(ResourceEncodingEnum.JSON);
|
||||||
myResourceHistoryTableDao.save(table);
|
myResourceHistoryTableDao.save(table);
|
||||||
}
|
}
|
||||||
|
|
|
@ -620,11 +620,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
||||||
template.execute((TransactionCallback<ResourceTable>) t -> {
|
template.execute((TransactionCallback<ResourceTable>) t -> {
|
||||||
ResourceHistoryTable resourceHistoryTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), id.getVersionIdPartAsLong());
|
ResourceHistoryTable resourceHistoryTable = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), id.getVersionIdPartAsLong());
|
||||||
resourceHistoryTable.setEncoding(ResourceEncodingEnum.JSON);
|
resourceHistoryTable.setEncoding(ResourceEncodingEnum.JSON);
|
||||||
try {
|
resourceHistoryTable.setResourceTextVc("{\"resourceType\":\"FOO\"}");
|
||||||
resourceHistoryTable.setResource("{\"resourceType\":\"FOO\"}".getBytes("UTF-8"));
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new Error(e);
|
|
||||||
}
|
|
||||||
myResourceHistoryTableDao.save(resourceHistoryTable);
|
myResourceHistoryTableDao.save(resourceHistoryTable);
|
||||||
|
|
||||||
ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||||
|
@ -1917,11 +1913,11 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
||||||
|
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.addIdentifier().setSystem("urn:system").setValue(methodName);
|
p.addIdentifier().setSystem("urn:system").setValue(methodName);
|
||||||
myPatientDao.create(p, mySrd).getId();
|
myPatientDao.create(p, mySrd);
|
||||||
|
|
||||||
p = new Patient();
|
p = new Patient();
|
||||||
p.addIdentifier().setSystem("urn:system").setValue(methodName);
|
p.addIdentifier().setSystem("urn:system").setValue(methodName);
|
||||||
myPatientDao.create(p, mySrd).getId();
|
myPatientDao.create(p, mySrd);
|
||||||
|
|
||||||
Observation o = new Observation();
|
Observation o = new Observation();
|
||||||
o.getCode().setText("Some Observation");
|
o.getCode().setText("Some Observation");
|
||||||
|
|
|
@ -87,6 +87,11 @@ public class ReindexJobTest extends BaseJpaR4Test {
|
||||||
createPatient(withActiveTrue());
|
createPatient(withActiveTrue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move resource text to compressed storage, which we don't write to anymore but legacy
|
||||||
|
// data may exist that was previously stored there, so we're simulating that.
|
||||||
|
List<ResourceHistoryTable> allHistoryEntities = runInTransaction(() -> myResourceHistoryTableDao.findAll());
|
||||||
|
allHistoryEntities.forEach(t->relocateResourceTextToCompressedColumn(t.getResourceId(), t.getVersion()));
|
||||||
|
|
||||||
runInTransaction(()->{
|
runInTransaction(()->{
|
||||||
assertEquals(20, myResourceHistoryTableDao.count());
|
assertEquals(20, myResourceHistoryTableDao.count());
|
||||||
for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) {
|
for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) {
|
||||||
|
@ -141,6 +146,11 @@ public class ReindexJobTest extends BaseJpaR4Test {
|
||||||
createPatient(withActiveTrue());
|
createPatient(withActiveTrue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move resource text to compressed storage, which we don't write to anymore but legacy
|
||||||
|
// data may exist that was previously stored there, so we're simulating that.
|
||||||
|
List<ResourceHistoryTable> allHistoryEntities = runInTransaction(() -> myResourceHistoryTableDao.findAll());
|
||||||
|
allHistoryEntities.forEach(t->relocateResourceTextToCompressedColumn(t.getResourceId(), t.getVersion()));
|
||||||
|
|
||||||
runInTransaction(()->{
|
runInTransaction(()->{
|
||||||
assertEquals(20, myResourceHistoryTableDao.count());
|
assertEquals(20, myResourceHistoryTableDao.count());
|
||||||
for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) {
|
for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) {
|
||||||
|
@ -149,8 +159,6 @@ public class ReindexJobTest extends BaseJpaR4Test {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
myStorageSettings.setInlineResourceTextBelowSize(10000);
|
|
||||||
|
|
||||||
// execute
|
// execute
|
||||||
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
|
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
|
||||||
startRequest.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
|
startRequest.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
|
||||||
|
|
|
@ -38,11 +38,9 @@ public class ResourceProviderInvalidDataR4Test extends BaseResourceProviderR4Tes
|
||||||
// Manually set the value to be an invalid decimal number
|
// Manually set the value to be an invalid decimal number
|
||||||
runInTransaction(() -> {
|
runInTransaction(() -> {
|
||||||
ResourceHistoryTable resVer = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id, 1);
|
ResourceHistoryTable resVer = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id, 1);
|
||||||
byte[] bytesCompressed = resVer.getResource();
|
String resourceText = resVer.getResourceTextVc();
|
||||||
String resourceText = GZipUtil.decompress(bytesCompressed);
|
|
||||||
resourceText = resourceText.replace("100", "-.100");
|
resourceText = resourceText.replace("100", "-.100");
|
||||||
bytesCompressed = GZipUtil.compress(resourceText);
|
resVer.setResourceTextVc(resourceText);
|
||||||
resVer.setResource(bytesCompressed);
|
|
||||||
myResourceHistoryTableDao.save(resVer);
|
myResourceHistoryTableDao.save(resVer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
|
||||||
import ca.uhn.fhir.util.ClasspathUtil;
|
import ca.uhn.fhir.util.ClasspathUtil;
|
||||||
import ca.uhn.fhir.util.StopWatch;
|
import ca.uhn.fhir.util.StopWatch;
|
||||||
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
import ca.uhn.fhir.util.UrlUtil;
|
import ca.uhn.fhir.util.UrlUtil;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
@ -6723,7 +6724,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
||||||
|
|
||||||
// Update Patient after delay
|
// Update Patient after delay
|
||||||
int delayInMs = 1000;
|
int delayInMs = 1000;
|
||||||
TimeUnit.MILLISECONDS.sleep(delayInMs);
|
TestUtil.sleepAtLeast(delayInMs + 100);
|
||||||
patient.getNameFirstRep().addGiven("Bob");
|
patient.getNameFirstRep().addGiven("Bob");
|
||||||
myClient.update().resource(patient).execute();
|
myClient.update().resource(patient).execute();
|
||||||
|
|
||||||
|
|
|
@ -397,6 +397,11 @@ public class GiantTransactionPerfTest {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateNonInlinedContents(byte[] theText, long thePid) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public List<ResourceHistoryTable> findAll() {
|
public List<ResourceHistoryTable> findAll() {
|
||||||
|
|
|
@ -34,6 +34,20 @@
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Needed for Testcontainers -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.13.2</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.hamcrest</groupId>
|
||||||
|
<artifactId>hamcrest-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.r5.database;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
|
import ca.uhn.fhir.jpa.embedded.JpaEmbeddedDatabase;
|
||||||
|
import ca.uhn.fhir.jpa.migrate.HapiMigrationStorageSvc;
|
||||||
|
import ca.uhn.fhir.jpa.migrate.MigrationTaskList;
|
||||||
|
import ca.uhn.fhir.jpa.migrate.SchemaMigrator;
|
||||||
|
import ca.uhn.fhir.jpa.migrate.dao.HapiMigrationDao;
|
||||||
|
import ca.uhn.fhir.jpa.migrate.tasks.HapiFhirJpaMigrationTasks;
|
||||||
|
import ca.uhn.fhir.jpa.test.config.TestR5Config;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
|
import ca.uhn.fhir.util.VersionEnum;
|
||||||
|
import jakarta.persistence.EntityManagerFactory;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r5.model.Patient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
|
||||||
|
@ContextConfiguration(classes = {BaseDatabaseVerificationIT.TestConfig.class})
|
||||||
|
public abstract class BaseDatabaseVerificationIT {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(BaseDatabaseVerificationIT.class);
|
||||||
|
private static final String MIGRATION_TABLENAME = "MIGRATIONS";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
EntityManagerFactory myEntityManagerFactory;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
JpaEmbeddedDatabase myJpaEmbeddedDatabase;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IFhirResourceDao<Patient> myPatientDao;
|
||||||
|
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(ints = {10, 100000})
|
||||||
|
public void testCreateRead(int theSize) {
|
||||||
|
String name = StringUtils.leftPad("", theSize, "a");
|
||||||
|
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.setActive(true);
|
||||||
|
patient.addName().setFamily(name);
|
||||||
|
IIdType id = myPatientDao.create(patient, new SystemRequestDetails()).getId();
|
||||||
|
|
||||||
|
Patient actual = myPatientDao.read(id, new SystemRequestDetails());
|
||||||
|
assertEquals(name, actual.getName().get(0).getFamily());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDelete() {
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.setActive(true);
|
||||||
|
IIdType id = myPatientDao.create(patient, new SystemRequestDetails()).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
myPatientDao.delete(id, new SystemRequestDetails());
|
||||||
|
|
||||||
|
assertThrows(ResourceGoneException.class, () -> myPatientDao.read(id, new SystemRequestDetails()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public static class TestConfig extends TestR5Config {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JpaDatabaseContextConfigParamObject myJpaDatabaseContextConfigParamObject;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Bean
|
||||||
|
public DataSource dataSource() {
|
||||||
|
DataSource dataSource = myJpaDatabaseContextConfigParamObject.getJpaEmbeddedDatabase().getDataSource();
|
||||||
|
|
||||||
|
HapiMigrationDao hapiMigrationDao = new HapiMigrationDao(dataSource, myJpaDatabaseContextConfigParamObject.getJpaEmbeddedDatabase().getDriverType(), MIGRATION_TABLENAME);
|
||||||
|
HapiMigrationStorageSvc hapiMigrationStorageSvc = new HapiMigrationStorageSvc(hapiMigrationDao);
|
||||||
|
|
||||||
|
MigrationTaskList tasks = new HapiFhirJpaMigrationTasks(Set.of()).getAllTasks(VersionEnum.values());
|
||||||
|
|
||||||
|
SchemaMigrator schemaMigrator = new SchemaMigrator(
|
||||||
|
"HAPI FHIR", MIGRATION_TABLENAME, dataSource, new Properties(), tasks, hapiMigrationStorageSvc);
|
||||||
|
schemaMigrator.setDriverType(myJpaDatabaseContextConfigParamObject.getJpaEmbeddedDatabase().getDriverType());
|
||||||
|
|
||||||
|
ourLog.info("About to run migration...");
|
||||||
|
schemaMigrator.createMigrationTableIfRequired();
|
||||||
|
schemaMigrator.migrate();
|
||||||
|
ourLog.info("Migration complete");
|
||||||
|
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JpaEmbeddedDatabase jpaEmbeddedDatabase(JpaDatabaseContextConfigParamObject theJpaDatabaseContextConfigParamObject) {
|
||||||
|
return theJpaDatabaseContextConfigParamObject.getJpaEmbeddedDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Properties jpaProperties() {
|
||||||
|
Properties retVal = super.jpaProperties();
|
||||||
|
retVal.put("hibernate.hbm2ddl.auto", "none");
|
||||||
|
retVal.put("hibernate.dialect", myJpaDatabaseContextConfigParamObject.getDialect());
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JpaDatabaseContextConfigParamObject {
|
||||||
|
private JpaEmbeddedDatabase myJpaEmbeddedDatabase;
|
||||||
|
private String myDialect;
|
||||||
|
|
||||||
|
public JpaDatabaseContextConfigParamObject(JpaEmbeddedDatabase theJpaEmbeddedDatabase, String theDialect) {
|
||||||
|
myJpaEmbeddedDatabase = theJpaEmbeddedDatabase;
|
||||||
|
myDialect = theDialect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JpaEmbeddedDatabase getJpaEmbeddedDatabase() {
|
||||||
|
return myJpaEmbeddedDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDialect() {
|
||||||
|
return myDialect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.r5.database;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.embedded.MsSqlEmbeddedDatabase;
|
||||||
|
import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect;
|
||||||
|
import ca.uhn.fhir.jpa.model.dialect.HapiFhirSQLServerDialect;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
|
||||||
|
@ContextConfiguration(classes = {
|
||||||
|
DatabaseVerificationWithMsSqlIT.TestConfig.class
|
||||||
|
})
|
||||||
|
public class DatabaseVerificationWithMsSqlIT extends BaseDatabaseVerificationIT {
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public static class TestConfig {
|
||||||
|
@Bean
|
||||||
|
public JpaDatabaseContextConfigParamObject jpaDatabaseParamObject() {
|
||||||
|
return new JpaDatabaseContextConfigParamObject(
|
||||||
|
new MsSqlEmbeddedDatabase(),
|
||||||
|
HapiFhirSQLServerDialect.class.getName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.r5.database;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.embedded.OracleEmbeddedDatabase;
|
||||||
|
import ca.uhn.fhir.jpa.model.dialect.HapiFhirOracleDialect;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
|
||||||
|
@ContextConfiguration(classes = {
|
||||||
|
DatabaseVerificationWithOracleIT.TestConfig.class
|
||||||
|
})
|
||||||
|
public class DatabaseVerificationWithOracleIT extends BaseDatabaseVerificationIT {
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public static class TestConfig {
|
||||||
|
@Bean
|
||||||
|
public JpaDatabaseContextConfigParamObject jpaDatabaseParamObject(){
|
||||||
|
return new JpaDatabaseContextConfigParamObject(
|
||||||
|
new OracleEmbeddedDatabase(),
|
||||||
|
HapiFhirOracleDialect.class.getName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.r5.database;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.embedded.PostgresEmbeddedDatabase;
|
||||||
|
import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
|
||||||
|
@ContextConfiguration(classes = {
|
||||||
|
DatabaseVerificationWithPostgresIT.TestConfig.class
|
||||||
|
})
|
||||||
|
public class DatabaseVerificationWithPostgresIT extends BaseDatabaseVerificationIT {
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public static class TestConfig {
|
||||||
|
@Bean
|
||||||
|
public JpaDatabaseContextConfigParamObject jpaDatabaseParamObject() {
|
||||||
|
return new JpaDatabaseContextConfigParamObject(
|
||||||
|
new PostgresEmbeddedDatabase(),
|
||||||
|
HapiFhirPostgresDialect.class.getName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
|
@ -131,20 +131,14 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>1.17.6</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
<artifactId>mssqlserver</artifactId>
|
<artifactId>mssqlserver</artifactId>
|
||||||
<version>1.17.6</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
<artifactId>oracle-xe</artifactId>
|
<artifactId>oracle-xe</artifactId>
|
||||||
<version>1.17.6</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
package ca.uhn.fhir.jpa.embedded;
|
package ca.uhn.fhir.jpa.embedded;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
|
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -53,6 +54,7 @@ public abstract class JpaEmbeddedDatabase {
|
||||||
private JdbcTemplate myJdbcTemplate;
|
private JdbcTemplate myJdbcTemplate;
|
||||||
private Connection myConnection;
|
private Connection myConnection;
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
public abstract void stop();
|
public abstract void stop();
|
||||||
|
|
||||||
public abstract void disableConstraints();
|
public abstract void disableConstraints();
|
||||||
|
@ -116,7 +118,7 @@ public abstract class JpaEmbeddedDatabase {
|
||||||
for (String sql : theStatements) {
|
for (String sql : theStatements) {
|
||||||
if (!StringUtils.isBlank(sql)) {
|
if (!StringUtils.isBlank(sql)) {
|
||||||
statement.addBatch(sql);
|
statement.addBatch(sql);
|
||||||
ourLog.info("Added to batch: {}", sql);
|
ourLog.debug("Added to batch: {}", sql);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
statement.executeBatch();
|
statement.executeBatch();
|
||||||
|
|
|
@ -40,6 +40,7 @@ import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
|
||||||
import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor;
|
import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor;
|
||||||
import ca.uhn.fhir.jpa.binary.provider.BinaryAccessProvider;
|
import ca.uhn.fhir.jpa.binary.provider.BinaryAccessProvider;
|
||||||
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
|
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
|
||||||
|
import ca.uhn.fhir.jpa.dao.GZipUtil;
|
||||||
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
|
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IBatch2JobInstanceRepository;
|
import ca.uhn.fhir.jpa.dao.data.IBatch2JobInstanceRepository;
|
||||||
import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository;
|
import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository;
|
||||||
|
@ -82,6 +83,7 @@ import ca.uhn.fhir.jpa.entity.TermValueSet;
|
||||||
import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
|
import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
|
||||||
import ca.uhn.fhir.jpa.interceptor.PerformanceTracingLoggingInterceptor;
|
import ca.uhn.fhir.jpa.interceptor.PerformanceTracingLoggingInterceptor;
|
||||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
|
||||||
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
|
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
|
||||||
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
|
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
|
||||||
import ca.uhn.fhir.jpa.provider.JpaSystemProvider;
|
import ca.uhn.fhir.jpa.provider.JpaSystemProvider;
|
||||||
|
@ -663,6 +665,14 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
|
||||||
return myTxManager;
|
return myTxManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void relocateResourceTextToCompressedColumn(Long theResourcePid, Long theVersion) {
|
||||||
|
runInTransaction(()->{
|
||||||
|
ResourceHistoryTable historyEntity = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theResourcePid, theVersion);
|
||||||
|
byte[] contents = GZipUtil.compress(historyEntity.getResourceTextVc());
|
||||||
|
myResourceHistoryTableDao.updateNonInlinedContents(contents, historyEntity.getId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected ValidationResult validateWithResult(IBaseResource theResource) {
|
protected ValidationResult validateWithResult(IBaseResource theResource) {
|
||||||
FhirValidator validatorModule = myFhirContext.newValidator();
|
FhirValidator validatorModule = myFhirContext.newValidator();
|
||||||
FhirInstanceValidator instanceValidator = new FhirInstanceValidator(myValidationSupport);
|
FhirInstanceValidator instanceValidator = new FhirInstanceValidator(myValidationSupport);
|
||||||
|
|
|
@ -186,7 +186,7 @@ public class TestR5Config {
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Properties jpaProperties() {
|
protected Properties jpaProperties() {
|
||||||
Properties extraProperties = new Properties();
|
Properties extraProperties = new Properties();
|
||||||
extraProperties.put("hibernate.format_sql", "false");
|
extraProperties.put("hibernate.format_sql", "false");
|
||||||
extraProperties.put("hibernate.show_sql", "false");
|
extraProperties.put("hibernate.show_sql", "false");
|
||||||
|
|
|
@ -34,7 +34,7 @@ public enum ColumnTypeEnum {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unlimited length text, with a column definition containing the annotation:
|
* Unlimited length text, with a column definition containing the annotation:
|
||||||
* <code>@JdbcTypeCode(SqlTypes.LONG32VARCHAR)</code>
|
* <code>@Column(length=Integer.MAX_VALUE)</code>
|
||||||
*/
|
*/
|
||||||
TEXT,
|
TEXT,
|
||||||
BIG_DECIMAL;
|
BIG_DECIMAL;
|
||||||
|
|
|
@ -62,7 +62,7 @@ public final class ColumnTypeToDriverTypeToSqlType {
|
||||||
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.MYSQL_5_7, "double precision");
|
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.MYSQL_5_7, "double precision");
|
||||||
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.MSSQL_2012, "double precision");
|
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.MSSQL_2012, "double precision");
|
||||||
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.ORACLE_12C, "double precision");
|
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.ORACLE_12C, "double precision");
|
||||||
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.POSTGRES_9_4, "float8");
|
setColumnType(ColumnTypeEnum.DOUBLE, DriverTypeEnum.POSTGRES_9_4, "double precision");
|
||||||
|
|
||||||
setColumnType(ColumnTypeEnum.LONG, DriverTypeEnum.H2_EMBEDDED, "bigint");
|
setColumnType(ColumnTypeEnum.LONG, DriverTypeEnum.H2_EMBEDDED, "bigint");
|
||||||
setColumnType(ColumnTypeEnum.LONG, DriverTypeEnum.DERBY_EMBEDDED, "bigint");
|
setColumnType(ColumnTypeEnum.LONG, DriverTypeEnum.DERBY_EMBEDDED, "bigint");
|
||||||
|
@ -123,7 +123,7 @@ public final class ColumnTypeToDriverTypeToSqlType {
|
||||||
"oid"); // the PG driver will write oid into a `text` column
|
"oid"); // the PG driver will write oid into a `text` column
|
||||||
setColumnType(ColumnTypeEnum.CLOB, DriverTypeEnum.MSSQL_2012, "varchar(MAX)");
|
setColumnType(ColumnTypeEnum.CLOB, DriverTypeEnum.MSSQL_2012, "varchar(MAX)");
|
||||||
|
|
||||||
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.H2_EMBEDDED, "character large object");
|
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.H2_EMBEDDED, "clob");
|
||||||
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.DERBY_EMBEDDED, "clob");
|
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.DERBY_EMBEDDED, "clob");
|
||||||
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.MARIADB_10_1, "longtext");
|
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.MARIADB_10_1, "longtext");
|
||||||
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.MYSQL_5_7, "longtext");
|
setColumnType(ColumnTypeEnum.TEXT, DriverTypeEnum.MYSQL_5_7, "longtext");
|
||||||
|
|
|
@ -267,14 +267,6 @@ public class JpaStorageSettings extends StorageSettings {
|
||||||
* @since 5.6.0
|
* @since 5.6.0
|
||||||
*/
|
*/
|
||||||
private boolean myAdvancedHSearchIndexing = false;
|
private boolean myAdvancedHSearchIndexing = false;
|
||||||
/**
|
|
||||||
* If set to a positive number, any resources with a character length at or below the given number
|
|
||||||
* of characters will be stored inline in the <code>HFJ_RES_VER</code> table instead of using a
|
|
||||||
* separate LOB column.
|
|
||||||
*
|
|
||||||
* @since 5.7.0
|
|
||||||
*/
|
|
||||||
private int myInlineResourceTextBelowSize = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 5.7.0
|
* @since 5.7.0
|
||||||
|
@ -381,25 +373,21 @@ public class JpaStorageSettings extends StorageSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set to a positive number, any resources with a character length at or below the given number
|
|
||||||
* of characters will be stored inline in the <code>HFJ_RES_VER</code> table instead of using a
|
|
||||||
* separate LOB column.
|
|
||||||
*
|
|
||||||
* @since 5.7.0
|
* @since 5.7.0
|
||||||
|
* @deprecated This setting no longer does anything as of HAPI FHIR 7.0.0
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public int getInlineResourceTextBelowSize() {
|
public int getInlineResourceTextBelowSize() {
|
||||||
return myInlineResourceTextBelowSize;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set to a positive number, any resources with a character length at or below the given number
|
|
||||||
* of characters will be stored inline in the <code>HFJ_RES_VER</code> table instead of using a
|
|
||||||
* separate LOB column.
|
|
||||||
*
|
|
||||||
* @since 5.7.0
|
* @since 5.7.0
|
||||||
|
* @deprecated This setting no longer does anything as of HAPI FHIR 7.0.0
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public void setInlineResourceTextBelowSize(int theInlineResourceTextBelowSize) {
|
public void setInlineResourceTextBelowSize(int theInlineResourceTextBelowSize) {
|
||||||
myInlineResourceTextBelowSize = theInlineResourceTextBelowSize;
|
// ignored
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,7 +36,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
|
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.util.ICallable;
|
import ca.uhn.fhir.util.ICallable;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.SleepUtil;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import jakarta.annotation.Nonnull;
|
import jakarta.annotation.Nonnull;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
|
@ -48,6 +48,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.dao.PessimisticLockingFailureException;
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
import org.springframework.transaction.TransactionStatus;
|
import org.springframework.transaction.TransactionStatus;
|
||||||
|
@ -89,11 +90,18 @@ public class HapiTransactionService implements IHapiTransactionService {
|
||||||
|
|
||||||
private Propagation myTransactionPropagationWhenChangingPartitions = Propagation.REQUIRED;
|
private Propagation myTransactionPropagationWhenChangingPartitions = Propagation.REQUIRED;
|
||||||
|
|
||||||
|
private SleepUtil mySleepUtil = new SleepUtil();
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) {
|
public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) {
|
||||||
myInterceptorBroadcaster = theInterceptorBroadcaster;
|
myInterceptorBroadcaster = theInterceptorBroadcaster;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public void setSleepUtil(SleepUtil theSleepUtil) {
|
||||||
|
mySleepUtil = theSleepUtil;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IExecutionBuilder withRequest(@Nullable RequestDetails theRequestDetails) {
|
public IExecutionBuilder withRequest(@Nullable RequestDetails theRequestDetails) {
|
||||||
return new ExecutionBuilder(theRequestDetails);
|
return new ExecutionBuilder(theRequestDetails);
|
||||||
|
@ -281,6 +289,25 @@ public class HapiTransactionService implements IHapiTransactionService {
|
||||||
return doExecuteInTransaction(theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId);
|
return doExecuteInTransaction(theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isThrowableOrItsSubclassPresent(Throwable theThrowable, Class<? extends Throwable> theClass) {
|
||||||
|
return ExceptionUtils.indexOfType(theThrowable, theClass) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isThrowablePresent(Throwable theThrowable, Class<? extends Throwable> theClass) {
|
||||||
|
return ExceptionUtils.indexOfThrowable(theThrowable, theClass) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRetriable(Throwable theThrowable) {
|
||||||
|
return isThrowablePresent(theThrowable, ResourceVersionConflictException.class)
|
||||||
|
|| isThrowablePresent(theThrowable, DataIntegrityViolationException.class)
|
||||||
|
|| isThrowablePresent(theThrowable, ConstraintViolationException.class)
|
||||||
|
|| isThrowablePresent(theThrowable, ObjectOptimisticLockingFailureException.class)
|
||||||
|
// calling isThrowableOrItsSubclassPresent instead of isThrowablePresent for
|
||||||
|
// PessimisticLockingFailureException, because we want to retry on its subclasses as well, especially
|
||||||
|
// CannotAcquireLockException, which is thrown in some deadlock situations which we want to retry
|
||||||
|
|| isThrowableOrItsSubclassPresent(theThrowable, PessimisticLockingFailureException.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private <T> T doExecuteInTransaction(
|
private <T> T doExecuteInTransaction(
|
||||||
ExecutionBuilder theExecutionBuilder,
|
ExecutionBuilder theExecutionBuilder,
|
||||||
|
@ -294,11 +321,7 @@ public class HapiTransactionService implements IHapiTransactionService {
|
||||||
return doExecuteCallback(theExecutionBuilder, theCallback);
|
return doExecuteCallback(theExecutionBuilder, theCallback);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!(ExceptionUtils.indexOfThrowable(e, ResourceVersionConflictException.class) != -1
|
if (!isRetriable(e)) {
|
||||||
|| ExceptionUtils.indexOfThrowable(e, DataIntegrityViolationException.class) != -1
|
|
||||||
|| ExceptionUtils.indexOfThrowable(e, ConstraintViolationException.class) != -1
|
|
||||||
|| ExceptionUtils.indexOfThrowable(e, ObjectOptimisticLockingFailureException.class)
|
|
||||||
!= -1)) {
|
|
||||||
ourLog.debug("Unexpected transaction exception. Will not be retried.", e);
|
ourLog.debug("Unexpected transaction exception. Will not be retried.", e);
|
||||||
throw e;
|
throw e;
|
||||||
} else {
|
} else {
|
||||||
|
@ -354,7 +377,7 @@ public class HapiTransactionService implements IHapiTransactionService {
|
||||||
}
|
}
|
||||||
double sleepAmount = (250.0d * i) * Math.random();
|
double sleepAmount = (250.0d * i) * Math.random();
|
||||||
long sleepAmountLong = (long) sleepAmount;
|
long sleepAmountLong = (long) sleepAmount;
|
||||||
TestUtil.sleepAtLeast(sleepAmountLong, false);
|
mySleepUtil.sleepAtLeast(sleepAmountLong, false);
|
||||||
|
|
||||||
ourLog.info(
|
ourLog.info(
|
||||||
"About to start a transaction retry due to conflict or constraint error. Sleeping {}ms first.",
|
"About to start a transaction retry due to conflict or constraint error. Sleeping {}ms first.",
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.tx;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||||
|
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy;
|
||||||
|
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||||
|
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
||||||
|
import ca.uhn.fhir.util.SleepUtil;
|
||||||
|
import org.hibernate.exception.ConstraintViolationException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.dao.CannotAcquireLockException;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.TransactionStatus;
|
||||||
|
import org.springframework.transaction.support.TransactionCallback;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class HapiTransactionServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IInterceptorBroadcaster myInterceptorBroadcasterMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PlatformTransactionManager myTransactionManagerMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IRequestPartitionHelperSvc myRequestPartitionHelperSvcMock;
|
||||||
|
@Mock
|
||||||
|
private PartitionSettings myPartitionSettingsMock;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SleepUtil mySleepUtilMock;
|
||||||
|
|
||||||
|
private HapiTransactionService myHapiTransactionService;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void beforeEach() {
|
||||||
|
myHapiTransactionService = new HapiTransactionService();
|
||||||
|
myHapiTransactionService.setTransactionManager(myTransactionManagerMock);
|
||||||
|
myHapiTransactionService.setInterceptorBroadcaster(myInterceptorBroadcasterMock);
|
||||||
|
myHapiTransactionService.setPartitionSettingsForUnitTest(myPartitionSettingsMock);
|
||||||
|
myHapiTransactionService.setRequestPartitionSvcForUnitTest(myRequestPartitionHelperSvcMock);
|
||||||
|
myHapiTransactionService.setSleepUtil(mySleepUtilMock);
|
||||||
|
mockInterceptorBroadcaster();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockInterceptorBroadcaster() {
|
||||||
|
lenient().when(myInterceptorBroadcasterMock.callHooksAndReturnObject(eq(Pointcut.STORAGE_VERSION_CONFLICT),
|
||||||
|
isA(HookParams.class)))
|
||||||
|
.thenAnswer(invocationOnMock -> {
|
||||||
|
HookParams hookParams = (HookParams) invocationOnMock.getArguments()[1];
|
||||||
|
//answer with whatever retry settings passed in as HookParam
|
||||||
|
RequestDetails requestDetails = hookParams.get(RequestDetails.class);
|
||||||
|
ResourceVersionConflictResolutionStrategy answer = new ResourceVersionConflictResolutionStrategy();
|
||||||
|
answer.setRetry(requestDetails.isRetry());
|
||||||
|
answer.setMaxRetries(requestDetails.getMaxRetries());
|
||||||
|
return answer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper method to test retry logic on exceptions
|
||||||
|
* TransactionCallback interface allows only throwing RuntimeExceptions,
|
||||||
|
* that's why the parameter type is RunTimeException
|
||||||
|
*/
|
||||||
|
private Exception testRetriesOnException(RuntimeException theException,
|
||||||
|
boolean theRetryEnabled,
|
||||||
|
int theMaxRetries,
|
||||||
|
int theExpectedNumberOfCallsToTransactionCallback) {
|
||||||
|
RequestDetails requestDetails = new SystemRequestDetails();
|
||||||
|
requestDetails.setRetry(theRetryEnabled);
|
||||||
|
requestDetails.setMaxRetries(theMaxRetries);
|
||||||
|
|
||||||
|
HapiTransactionService.IExecutionBuilder executionBuilder = myHapiTransactionService
|
||||||
|
.withRequest(requestDetails)
|
||||||
|
.withTransactionDetails(new TransactionDetails());
|
||||||
|
|
||||||
|
AtomicInteger numberOfCalls = new AtomicInteger();
|
||||||
|
TransactionCallback<Void> transactionCallback = (TransactionStatus theStatus) -> {
|
||||||
|
numberOfCalls.incrementAndGet();
|
||||||
|
throw theException;
|
||||||
|
};
|
||||||
|
|
||||||
|
Exception theExceptionThrownByDoExecute = assertThrows(Exception.class, () -> {
|
||||||
|
myHapiTransactionService.doExecute((HapiTransactionService.ExecutionBuilder) executionBuilder, transactionCallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(theExpectedNumberOfCallsToTransactionCallback, numberOfCalls.get());
|
||||||
|
verify(mySleepUtilMock, times(theExpectedNumberOfCallsToTransactionCallback - 1))
|
||||||
|
.sleepAtLeast(anyLong(), anyBoolean());
|
||||||
|
return theExceptionThrownByDoExecute;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> provideRetriableExceptionParameters() {
|
||||||
|
String exceptionMessage = "failed!";
|
||||||
|
return Stream.of(
|
||||||
|
arguments(new ResourceVersionConflictException(exceptionMessage)),
|
||||||
|
arguments(new DataIntegrityViolationException(exceptionMessage)),
|
||||||
|
arguments(new ConstraintViolationException(exceptionMessage, new SQLException(""), null)),
|
||||||
|
arguments(new ObjectOptimisticLockingFailureException(exceptionMessage, new Exception())),
|
||||||
|
//CannotAcquireLockException is a subclass of
|
||||||
|
//PessimisticLockingFailureException which we treat as a retriable exception
|
||||||
|
arguments(new CannotAcquireLockException(exceptionMessage))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{index}: {0}")
|
||||||
|
@MethodSource(value = "provideRetriableExceptionParameters")
|
||||||
|
void testDoExecute_WhenRetryEnabled_RetriesOnRetriableExceptions(RuntimeException theException) {
|
||||||
|
testRetriesOnException(theException, true, 2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{index}: {0}")
|
||||||
|
@MethodSource(value = "provideRetriableExceptionParameters")
|
||||||
|
void testDoExecute_WhenRetryEnabled_RetriesOnRetriableInnerExceptions(RuntimeException theException) {
|
||||||
|
//in this test we wrap the retriable exception to test that nested exceptions are covered as well
|
||||||
|
RuntimeException theWrapperException = new RuntimeException("this is the wrapper", theException);
|
||||||
|
testRetriesOnException(theWrapperException, true, 2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{index}: {0}")
|
||||||
|
@MethodSource(value = "provideRetriableExceptionParameters")
|
||||||
|
void testDoExecute_WhenRetryIsDisabled_DoesNotRetryExceptions(RuntimeException theException) {
|
||||||
|
testRetriesOnException(theException, false, 10, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoExecute_WhenRetryEnabled_DoesNotRetryOnNonRetriableException() {
|
||||||
|
RuntimeException nonRetriableException = new RuntimeException("should not be retried");
|
||||||
|
Exception exceptionThrown = testRetriesOnException(nonRetriableException, true, 10, 1);
|
||||||
|
assertEquals(nonRetriableException, exceptionThrown);
|
||||||
|
verifyNoInteractions(myInterceptorBroadcasterMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoExecute_WhenRetyEnabled_StopsRetryingWhenARetryIsSuccessfull() {
|
||||||
|
RequestDetails requestDetails = new SystemRequestDetails();
|
||||||
|
requestDetails.setRetry(true);
|
||||||
|
requestDetails.setMaxRetries(10);
|
||||||
|
|
||||||
|
HapiTransactionService.IExecutionBuilder executionBuilder = myHapiTransactionService
|
||||||
|
.withRequest(requestDetails)
|
||||||
|
.withTransactionDetails(new TransactionDetails());
|
||||||
|
|
||||||
|
AtomicInteger numberOfCalls = new AtomicInteger();
|
||||||
|
TransactionCallback<Void> transactionCallback = (TransactionStatus theStatus) -> {
|
||||||
|
int currentCallNum = numberOfCalls.incrementAndGet();
|
||||||
|
//fail for the first two calls then succeed on the third
|
||||||
|
if (currentCallNum < 3) {
|
||||||
|
// using ResourceVersionConflictException, since it is a retriable exception
|
||||||
|
throw new ResourceVersionConflictException("failed");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
myHapiTransactionService.doExecute((HapiTransactionService.ExecutionBuilder) executionBuilder, transactionCallback);
|
||||||
|
|
||||||
|
assertEquals(3, numberOfCalls.get());
|
||||||
|
verify(mySleepUtilMock, times(2))
|
||||||
|
.sleepAtLeast(anyLong(), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
|
@ -361,9 +361,6 @@ public class JpaModelScannerAndVerifier {
|
||||||
if (!theIsView && column.length() == 255) {
|
if (!theIsView && column.length() == 255) {
|
||||||
throw new IllegalStateException(Msg.code(1626) + "Field does not have an explicit maximum length specified: " + field);
|
throw new IllegalStateException(Msg.code(1626) + "Field does not have an explicit maximum length specified: " + field);
|
||||||
}
|
}
|
||||||
if (column.length() > MAX_COL_LENGTH) {
|
|
||||||
throw new IllegalStateException(Msg.code(1627) + "Field is too long: " + field);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Size size = theAnnotatedElement.getAnnotation(Size.class);
|
Size size = theAnnotatedElement.getAnnotation(Size.class);
|
||||||
|
|
|
@ -184,14 +184,6 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.maven</groupId>
|
<groupId>org.apache.maven</groupId>
|
||||||
<artifactId>maven-plugin-api</artifactId>
|
<artifactId>maven-plugin-api</artifactId>
|
||||||
<!--
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.eclipse.sisu</groupId>
|
|
||||||
<artifactId>org.eclipse.sisu.plexus</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
-->
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.maven.plugin-tools</groupId>
|
<groupId>org.apache.maven.plugin-tools</groupId>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package ca.uhn.fhir.tinder.ddl;
|
package ca.uhn.fhir.tinder.ddl;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.util.ISequenceValueMassager;
|
import ca.uhn.fhir.jpa.util.ISequenceValueMassager;
|
||||||
|
import ca.uhn.fhir.util.IoUtil;
|
||||||
import jakarta.annotation.Nonnull;
|
import jakarta.annotation.Nonnull;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.MappedSuperclass;
|
import jakarta.persistence.MappedSuperclass;
|
||||||
|
@ -29,6 +30,7 @@ import org.springframework.core.io.ResourceLoader;
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -125,19 +127,8 @@ public class DdlGeneratorHibernate61 {
|
||||||
|
|
||||||
writeContentsToFile(nextDialect.getAppendFile(), classLoader, outputFile);
|
writeContentsToFile(nextDialect.getAppendFile(), classLoader, outputFile);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeContentsToFile(String prependFile, ClassLoader classLoader, File outputFile)
|
IoUtil.closeQuietly(connectionProvider);
|
||||||
throws MojoFailureException {
|
|
||||||
if (isNotBlank(prependFile)) {
|
|
||||||
ResourceLoader loader = new DefaultResourceLoader(classLoader);
|
|
||||||
Resource resource = loader.getResource(prependFile);
|
|
||||||
try (Writer w = new FileWriter(outputFile, true)) {
|
|
||||||
w.append(resource.getContentAsString(StandardCharsets.UTF_8));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new MojoFailureException("Failed to write to file " + outputFile + ": " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProject(MavenProject theProject) {
|
public void setProject(MavenProject theProject) {
|
||||||
|
@ -204,18 +195,64 @@ public class DdlGeneratorHibernate61 {
|
||||||
* here. The schema export doesn't actually touch this DB, so it doesn't
|
* here. The schema export doesn't actually touch this DB, so it doesn't
|
||||||
* matter that it doesn't correlate to the specified dialect.
|
* matter that it doesn't correlate to the specified dialect.
|
||||||
*/
|
*/
|
||||||
private static class FakeConnectionConnectionProvider extends UserSuppliedConnectionProviderImpl {
|
private static class FakeConnectionConnectionProvider extends UserSuppliedConnectionProviderImpl
|
||||||
|
implements Closeable {
|
||||||
private static final long serialVersionUID = 4147495169899817244L;
|
private static final long serialVersionUID = 4147495169899817244L;
|
||||||
|
private Connection connection;
|
||||||
|
|
||||||
|
public FakeConnectionConnectionProvider() {
|
||||||
|
try {
|
||||||
|
connection = DriverManager.getConnection("jdbc:h2:mem:tmp", "sa", "sa");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
connection = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The Oracle Dialect tries to query for any existing sequences, so we need to supply
|
||||||
|
* a fake empty table to answer that query.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
connection.setAutoCommit(true);
|
||||||
|
connection
|
||||||
|
.prepareStatement("create table all_sequences (PID bigint not null, primary key (PID))")
|
||||||
|
.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
ourLog.error("Failed to create sequences table", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Connection getConnection() throws SQLException {
|
public Connection getConnection() {
|
||||||
ourLog.trace("Using internal driver: {}", org.h2.Driver.class);
|
ourLog.trace("Using internal driver: {}", org.h2.Driver.class);
|
||||||
return DriverManager.getConnection("jdbc:h2:mem:tmp", "sa", "sa");
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void closeConnection(Connection conn) throws SQLException {
|
public void closeConnection(Connection conn) {
|
||||||
conn.close();
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
try {
|
||||||
|
connection.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeContentsToFile(String prependFile, ClassLoader classLoader, File outputFile)
|
||||||
|
throws MojoFailureException {
|
||||||
|
if (isNotBlank(prependFile)) {
|
||||||
|
ResourceLoader loader = new DefaultResourceLoader(classLoader);
|
||||||
|
Resource resource = loader.getResource(prependFile);
|
||||||
|
try (Writer w = new FileWriter(outputFile, true)) {
|
||||||
|
w.append(resource.getContentAsString(StandardCharsets.UTF_8));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new MojoFailureException("Failed to write to file " + outputFile + ": " + e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,13 +28,13 @@ public class GenerateDdlMojo extends AbstractMojo {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(GenerateDdlMojo.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(GenerateDdlMojo.class);
|
||||||
|
|
||||||
@Parameter
|
@Parameter
|
||||||
private List<String> packageNames;
|
List<String> packageNames;
|
||||||
|
|
||||||
@Parameter
|
@Parameter
|
||||||
private List<Dialect> dialects;
|
List<Dialect> dialects;
|
||||||
|
|
||||||
@Parameter
|
@Parameter
|
||||||
private String outputDirectory;
|
String outputDirectory;
|
||||||
|
|
||||||
@Parameter(defaultValue = "${project}", readonly = true)
|
@Parameter(defaultValue = "${project}", readonly = true)
|
||||||
private transient MavenProject project;
|
private transient MavenProject project;
|
||||||
|
@ -70,18 +70,20 @@ public class GenerateDdlMojo extends AbstractMojo {
|
||||||
|
|
||||||
public static void main(String[] args) throws MojoExecutionException, MojoFailureException {
|
public static void main(String[] args) throws MojoExecutionException, MojoFailureException {
|
||||||
/*
|
/*
|
||||||
* Note, to execute this, add the following snippet to this module's POM. The whole project won't work with
|
* Note, to execute this for real entities, add the following snippet to this module's POM. The whole project won't work with
|
||||||
* that added, but you can add it temporarily in order to debug this in IJ:
|
* that added, but you can add it temporarily in order to debug this in IJ:
|
||||||
* <dependency>
|
* <dependency>
|
||||||
* <groupId>ca.uhn.hapi.fhir</groupId>
|
* <groupId>ca.uhn.hapi.fhir</groupId>
|
||||||
* <artifactId>hapi-fhir-jpaserver-model</artifactId>
|
* <artifactId>hapi-fhir-jpaserver-model</artifactId>
|
||||||
* <version>${project.version}</version>
|
* <version>${project.version}</version>
|
||||||
* </dependency>
|
* </dependency>
|
||||||
|
*
|
||||||
|
* Alternately, there is a unit test with fake entities that also runs this class.
|
||||||
*/
|
*/
|
||||||
GenerateDdlMojo m = new GenerateDdlMojo();
|
GenerateDdlMojo m = new GenerateDdlMojo();
|
||||||
m.packageNames = List.of("ca.uhn.fhir.jpa.model.entity");
|
m.packageNames = List.of("ca.uhn.fhir.jpa.model.entity");
|
||||||
m.outputDirectory = "hapi-tinder-plugin/target";
|
m.outputDirectory = "hapi-tinder-plugin/target";
|
||||||
m.dialects = List.of(new Dialect("ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect", "h2.sql"));
|
m.dialects = List.of(new Dialect("ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect", "postgres.sql"));
|
||||||
m.execute();
|
m.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package ca.uhn.fhir.tinder.ddl;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.maven.plugin.MojoExecutionException;
|
||||||
|
import org.apache.maven.plugin.MojoFailureException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
class GenerateDdlMojoTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenerateSequences() throws MojoExecutionException, MojoFailureException, IOException {
|
||||||
|
|
||||||
|
GenerateDdlMojo m = new GenerateDdlMojo();
|
||||||
|
m.packageNames = List.of("ca.uhn.fhir.tinder.ddl.test");
|
||||||
|
m.outputDirectory = "target/generate-ddl-plugin-test/";
|
||||||
|
m.dialects = List.of(
|
||||||
|
new GenerateDdlMojo.Dialect("ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect", "h2.sql"),
|
||||||
|
new GenerateDdlMojo.Dialect("ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect", "postgres.sql"),
|
||||||
|
new GenerateDdlMojo.Dialect("ca.uhn.fhir.jpa.model.dialect.HapiFhirOracleDialect", "oracle.sql"),
|
||||||
|
new GenerateDdlMojo.Dialect("ca.uhn.fhir.jpa.model.dialect.HapiFhirSQLServerDialect", "sqlserver.sql")
|
||||||
|
);
|
||||||
|
m.execute();
|
||||||
|
|
||||||
|
verifySequence("sqlserver.sql");
|
||||||
|
verifySequence("oracle.sql");
|
||||||
|
verifySequence("postgres.sql");
|
||||||
|
verifySequence("h2.sql");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifySequence(String fileName) throws IOException {
|
||||||
|
String contents = FileUtils.readFileToString(new File("target/generate-ddl-plugin-test/" + fileName), StandardCharsets.UTF_8).toUpperCase(Locale.ROOT);
|
||||||
|
assertThat(fileName, contents, containsString("CREATE SEQUENCE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package ca.uhn.fhir.tinder.ddl.test;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.SequenceGenerator;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@Entity()
|
||||||
|
public class ExampleEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@SequenceGenerator(name = "SEQ_RESOURCE_HISTORY_ID", sequenceName = "SEQ_RESOURCE_HISTORY_ID")
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESOURCE_HISTORY_ID")
|
||||||
|
@Column(name = "PID")
|
||||||
|
private Long myId;
|
||||||
|
|
||||||
|
@Column(name = "RES_ID", nullable = false, updatable = false, insertable = false)
|
||||||
|
private Long myResourceId;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
18
pom.xml
18
pom.xml
|
@ -2188,7 +2188,21 @@
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
<artifactId>testcontainers</artifactId>
|
<artifactId>testcontainers</artifactId>
|
||||||
<version>${testcontainers_version}</version>
|
<version>${testcontainers_version}</version>
|
||||||
<scope>test</scope>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>${testcontainers_version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>mssqlserver</artifactId>
|
||||||
|
<version>${testcontainers_version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testcontainers</groupId>
|
||||||
|
<artifactId>oracle-xe</artifactId>
|
||||||
|
<version>${testcontainers_version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testcontainers</groupId>
|
<groupId>org.testcontainers</groupId>
|
||||||
|
@ -2343,7 +2357,7 @@
|
||||||
<maxmem>2000m</maxmem>
|
<maxmem>2000m</maxmem>
|
||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
<arg>-XDcompilePolicy=simple</arg>
|
<arg>-XDcompilePolicy=simple</arg>
|
||||||
<arg>-Xplugin:ErrorProne -Xep:MissingSummary:OFF</arg>
|
<arg>-Xplugin:ErrorProne -Xep:MissingSummary:OFF -XepExcludedPaths:.*/src/test/java/.*</arg>
|
||||||
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
|
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
|
||||||
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
|
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
|
||||||
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
|
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
|
||||||
|
|
Loading…
Reference in New Issue