Skip to content

Commit 90e45e9

Browse files
authored
fix delete conflict on versioned patient ref on Provenance.target (#7158)
* fix delete conflict on allowed versioned patient ref on Provenance.target * change to use regex instead of startsWith * addressed code review comments
1 parent 55af8cb commit 90e45e9

File tree

4 files changed

+86
-9
lines changed

4 files changed

+86
-9
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: fix
3+
issue: 7160
4+
title: "Previously, a Patient resource could not be deleted if a `Provenance.target` has a versioned reference to the
5+
Patient. The delete was prevented by the referential integrity check on delete, even though the versioned references
6+
are not supposed to prevent deletion. This has now been fixed."

hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -613,12 +613,7 @@ private void extractResourceLinks(
613613
nextId = nextReference.getResource().getIdElement();
614614
}
615615

616-
if (myContext.getParserOptions().isStripVersionsFromReferences()
617-
&& !myContext
618-
.getParserOptions()
619-
.getDontStripVersionsFromReferencesAtPaths()
620-
.contains(thePathAndRef.getPath())
621-
&& nextId.hasVersionIdPart()) {
616+
if (nextId.hasVersionIdPart() && shouldStripVersionFromReferenceAtPath(thePathAndRef.getPath())) {
622617
nextId = nextId.toVersionless();
623618
}
624619

@@ -1090,6 +1085,30 @@ public void extractSearchParamComboNonUnique(ResourceTable theEntity, ResourceIn
10901085
populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity);
10911086
}
10921087

1088+
private boolean shouldStripVersionFromReferenceAtPath(String theSearchParamPath) {
1089+
1090+
if (!myContext.getParserOptions().isStripVersionsFromReferences()) {
1091+
// all references allowed to have versions globally, so don't strip
1092+
return false;
1093+
}
1094+
1095+
// global setting is to strip versions, see if there's any exceptions configured for specific paths
1096+
Set<String> pathsAllowedToHaveVersionedRefs =
1097+
myContext.getParserOptions().getDontStripVersionsFromReferencesAtPaths();
1098+
1099+
if (pathsAllowedToHaveVersionedRefs.contains(theSearchParamPath)) {
1100+
// path exactly matches
1101+
return false;
1102+
}
1103+
1104+
// there are some search parameters using a where clause to index the element for a specific resource type, such
1105+
// as "Provenance.target.where(resolve() is Patient)". We insert these in the ResourceLink table as well.
1106+
// Such entries in the ResourceLink table should remain versioned if the element is allowed to be versioned.
1107+
return pathsAllowedToHaveVersionedRefs.stream()
1108+
.noneMatch(pathToKeepVersioned -> theSearchParamPath.matches(
1109+
pathToKeepVersioned + "\\.where\\(resolve\\(\\) is [A-Z][a-zA-Z]*\\)"));
1110+
}
1111+
10931112
/**
10941113
* This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)}
10951114
* in order to use that method for extracting chained search parameter indexes both

hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/delete/DeleteConflictServiceR4Test.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ca.uhn.fhir.jpa.delete;
22

3+
import ca.uhn.fhir.context.ParserOptions;
34
import ca.uhn.fhir.i18n.Msg;
45
import ca.uhn.fhir.interceptor.api.Hook;
56
import ca.uhn.fhir.interceptor.api.Pointcut;
@@ -19,6 +20,7 @@
1920
import org.hl7.fhir.r4.model.OperationOutcome;
2021
import org.hl7.fhir.r4.model.Organization;
2122
import org.hl7.fhir.r4.model.Patient;
23+
import org.hl7.fhir.r4.model.Provenance;
2224
import org.hl7.fhir.r4.model.Reference;
2325
import org.junit.jupiter.api.AfterEach;
2426
import org.junit.jupiter.api.BeforeEach;
@@ -34,6 +36,7 @@
3436
import java.util.function.Function;
3537

3638
import static org.assertj.core.api.Assertions.assertThat;
39+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3740
import static org.junit.jupiter.api.Assertions.assertEquals;
3841
import static org.junit.jupiter.api.Assertions.assertFalse;
3942
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -375,6 +378,49 @@ public void testNoDuplicateConstraintReferences() {
375378
assertThat(conflicts).hasSize(1);
376379
}
377380

381+
382+
@Test
383+
void testDeletePatientReferencedInProvenanceTarget_AllowVersionsInProvenanceTarget_ShouldDeleteWithoutConflict() {
384+
//we don't expect this to be called, delete should occur without any conflict
385+
myDeleteInterceptor.deleteConflictFunction = x -> null;
386+
387+
try {
388+
myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths("Provenance.target");
389+
Patient pat = new Patient();
390+
IIdType patId = myPatientDao.create(pat, mySrd).getId();
391+
392+
Provenance prov = new Provenance();
393+
prov.addTarget(new Reference(patId.toUnqualified()));
394+
myProvenanceDao.create(prov, mySrd);
395+
396+
myPatientDao.delete(patId.toUnqualifiedVersionless(), mySrd);
397+
398+
assertThat(myDeleteInterceptor.myCallCount).isZero();
399+
} finally {
400+
//revert the parser option config to its default
401+
myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths(new ParserOptions().getDontStripVersionsFromReferencesAtPaths());
402+
}
403+
}
404+
405+
@Test
406+
void testDeletePatientReferencedInProvenanceTarget_DontAllowVersionsInProvenanceTarget_ShouldFailWithConflict() {
407+
myDeleteInterceptor.deleteConflictFunction = x -> null;
408+
409+
Patient pat = new Patient();
410+
IIdType patId = myPatientDao.create(pat, mySrd).getId();
411+
412+
Provenance prov = new Provenance();
413+
prov.addTarget(new Reference(patId.toUnqualified()));
414+
myProvenanceDao.create(prov, mySrd);
415+
416+
IIdType versionlessPatId = patId.toUnqualifiedVersionless();
417+
418+
assertThatThrownBy(() -> myPatientDao.delete(versionlessPatId, mySrd))
419+
.isInstanceOf(ResourceVersionConflictException.class);
420+
421+
assertThat(myDeleteInterceptor.myCallCount).isOne();
422+
}
423+
378424
private DeleteConflictOutcome deleteConflicts(DeleteConflictList theList) {
379425
for (DeleteConflict next : theList) {
380426
IdDt source = next.getSourceId();

hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
import java.util.ArrayList;
4747
import java.util.Collections;
4848
import java.util.List;
49+
import java.util.Set;
4950

51+
import static ca.uhn.fhir.jpa.config.r4.FhirContextR4Config.DEFAULT_PRESERVE_VERSION_REFS_R4_AND_LATER;
5052
import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM;
5153
import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesLargeTestData.RESOURCE_TYPES_EXPECTED_TO_BE_PATCHED;
5254
import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesLargeTestData.TOTAL_EXPECTED_PATCHES;
@@ -82,6 +84,9 @@ public void after() throws Exception {
8284

8385
myStorageSettings.setDefaultTransactionEntriesForWrite(new JpaStorageSettings().getDefaultTransactionEntriesForWrite());
8486
myStorageSettings.setReuseCachedSearchResultsForMillis(new JpaStorageSettings().getReuseCachedSearchResultsForMillis());
87+
// For some reason, if this value is not restored to its default at the end, it interferes with other suites,
88+
// so restore the default value
89+
myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths(DEFAULT_PRESERVE_VERSION_REFS_R4_AND_LATER);
8590
}
8691

8792
@Override
@@ -91,13 +96,14 @@ public void before() throws Exception {
9196
myStorageSettings.setReuseCachedSearchResultsForMillis(null);
9297
myStorageSettings.setAllowMultipleDelete(true);
9398
myFhirContext.setParserErrorHandler(new StrictErrorHandler());
94-
// we need to keep the version on Provenance.target fields to
95-
// verify that Provenance resources were saved with versioned target references
96-
myFhirContext.getParserOptions().setStripVersionsFromReferences(false);
99+
// we need to keep the version on Provenance.target fields to verify that Provenance resources were saved
100+
// with versioned target references, and delete-source on merge works correctly.
101+
myFhirContext.getParserOptions().setDontStripVersionsFromReferencesAtPaths("Provenance.target");
97102
myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry);
98103
myLargeTestData = new ReplaceReferencesLargeTestData(myDaoRegistry);
99104
}
100105

106+
101107
private void waitForAsyncTaskCompletion(Parameters theOutParams) {
102108
assertThat(getLastHttpStatusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED);
103109
Task task = (Task) theOutParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource();

0 commit comments

Comments
 (0)