Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce container ordinal column in the search parameter index tables and populate it #5832

Open
wants to merge 29 commits into
base: mm-20240318-support-for-contained-true-search-parameter
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7742aa7
Retain X-Request-ID header key casing in responses (#5787)
nathandoef Mar 20, 2024
4a726dd
Resolve 5790 Prefix a unit test log for hibernate exception in HapiFh…
TynerGjs Mar 20, 2024
4d4d8b2
CVE 2024 03 26 (#5804)
tadgh Mar 26, 2024
2f9693a
MDM expansion doesn't work when using the :mdm qualifier in query str…
epeartree Mar 27, 2024
6175807
Reduce use of LOB columns (#5799)
jamesagnew Mar 27, 2024
9ddd8bf
Update CR Operation Providers to latest version of CR release (#5751)
barhodes Mar 28, 2024
9e5db19
Fix BaseOutcomeReturningMethodBinding NullPointer (#5808)
nathandoef Mar 28, 2024
6ce3b17
Enforce maximum chunk size in bulk export (#5800)
jamesagnew Apr 1, 2024
70843cd
fixed resource fullUrl in _history bundles (#5815)
mrdnctrk Apr 1, 2024
e39ee7f
Add _language param to providers (#5801)
lilamikalson-smilecdr Apr 2, 2024
1736dad
Jr 20240325 concept map unmatched codes (#5809)
JasonRoberts-smile Apr 2, 2024
fcf0a85
Stop writing to hfj_forced_id (#5817)
michaelabuckley Apr 3, 2024
bdea4b6
Merge history table prefetch with resource table. (#5825)
michaelabuckley Apr 4, 2024
3a5ff47
Add CapabilityStatementFilterFactory to register filters that can mod…
fil512 Apr 5, 2024
620b46d
unify bulk export patient type and bulk export patient instance (#5822)
TipzCM Apr 5, 2024
5d55594
Batch2 test refactor only (#5823)
TipzCM Apr 6, 2024
eafa2ab
move mdm interceptor registration (#5830)
fil512 Apr 8, 2024
107de2c
reduction step failing fails jobs (#5831)
TipzCM Apr 8, 2024
65a4fcc
Introduce container ordinal in the search paramter index tables. Popu…
codeforgreen Apr 8, 2024
4d8427a
Ensure CanonicalizedSubscription reflects StorageSettings.isCrossPart…
lukedegruchy Apr 9, 2024
8b0441a
Fix build errors. Update containerOrd field to short instead of integer.
codeforgreen Apr 9, 2024
f7b5871
Merge remote-tracking branch 'origin/master' into mm-20240408-introdu…
codeforgreen Apr 9, 2024
71361a5
Resolve conflicts after merge.
codeforgreen Apr 9, 2024
079f497
Small changes to fix remaining failing tests
codeforgreen Apr 9, 2024
88289dc
Fix static code analysis problem with class name
codeforgreen Apr 9, 2024
5eb3b9b
Fix static code analysis warning
codeforgreen Apr 9, 2024
238a4b3
Fix a new more hash values in tests
codeforgreen Apr 9, 2024
04e3305
Include new bean in subscription test configs.
codeforgreen Apr 10, 2024
05f4870
Fix static code analysis warning
codeforgreen Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
221 changes: 85 additions & 136 deletions hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
import ca.uhn.fhir.model.base.composite.BaseContainedDt;
import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.DataFormatException;
import com.google.common.collect.Lists;
Expand Down Expand Up @@ -77,7 +76,6 @@
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.substring;

public class FhirTerser {

Expand Down Expand Up @@ -1330,6 +1328,19 @@ private void visit(
theStack.remove(theElement);
}

/**
* Returns the resources embedded in the provided resource.
* @param theResource the resource to scan
* @param theRecurse true if recursion should be applied when discovering embedded resources
* @return the embedded resources object
*/
public EmbeddedResources getEmbeddedResources(IBaseResource theResource, boolean theRecurse) {
Collection<IBaseResource> resourceList = getAllEmbeddedResources(theResource, theRecurse);
EmbeddedResources embeddedResources = new EmbeddedResources();
resourceList.forEach(embeddedResources::addResource);
return embeddedResources;
}

/**
* Returns all embedded resources that are found embedded within <code>theResource</code>.
* An embedded resource is a resource that can be found as a direct child within a resource,
Expand All @@ -1348,31 +1359,15 @@ public Collection<IBaseResource> getAllEmbeddedResources(IBaseResource theResour
Validate.notNull(theResource, "theResource must not be null");
ArrayList<IBaseResource> retVal = new ArrayList<>();

visit(theResource, new IModelVisitor2() {
@Override
public boolean acceptElement(
IBase theElement,
List<IBase> theContainingElementPath,
List<BaseRuntimeChildDefinition> theChildDefinitionPath,
List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
if (theElement == theResource) {
return true;
}
if (theElement instanceof IBaseResource) {
retVal.add((IBaseResource) theElement);
return theRecurse;
}
visit(theResource, (theElement, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath) -> {
if (theElement == theResource) {
return true;
}

@Override
public boolean acceptUndeclaredExtension(
IBaseExtension<?, ?> theNextExt,
List<IBase> theContainingElementPath,
List<BaseRuntimeChildDefinition> theChildDefinitionPath,
List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
return true;
if (theElement instanceof IBaseResource) {
retVal.add((IBaseResource) theElement);
return theRecurse;
}
return true;
});

return retVal;
Expand Down Expand Up @@ -1408,25 +1403,9 @@ public boolean acceptUndeclaredExtension(
});
}

private void containResourcesForEncoding(
private void containReferenceTargetsWithNoId(
ContainedResources theContained, IBaseResource theResource, boolean theModifyResource) {
List<IBaseReference> allReferences = getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
for (IBaseReference next : allReferences) {
IBaseResource resource = next.getResource();
if (resource == null && next.getReferenceElement().isLocal()) {
if (theContained.hasExistingIdToContainedResource()) {
IBaseResource potentialTarget = theContained
.getExistingIdToContainedResource()
.remove(next.getReferenceElement().getValue());
if (potentialTarget != null) {
theContained.addContained(next.getReferenceElement(), potentialTarget);
containResourcesForEncoding(theContained, potentialTarget, theModifyResource);
}
}
}
}

for (IBaseReference next : allReferences) {
for (IBaseReference next : getAllPopulatedChildElementsOfType(theResource, IBaseReference.class)) {
IBaseResource resource = next.getResource();
if (resource != null) {
if (resource.getIdElement().isEmpty() || resource.getIdElement().isLocal()) {
Expand All @@ -1439,11 +1418,6 @@ private void containResourcesForEncoding(
getContainedResourceList(theResource).add(resource);
next.setReference(id.getValue());
}
if (resource.getIdElement().isLocal() && theContained.hasExistingIdToContainedResource()) {
theContained
.getExistingIdToContainedResource()
.remove(resource.getIdElement().getValue());
}
}
}
}
Expand Down Expand Up @@ -1492,7 +1466,7 @@ public ContainedResources containResources(IBaseResource theResource, OptionsEnu
}

if (myContext.getParserOptions().isAutoContainReferenceTargetsWithNoId()) {
containResourcesForEncoding(contained, theResource, modifyResource);
containReferenceTargetsWithNoId(contained, theResource, modifyResource);
}

if (storeAndReuse) {
Expand Down Expand Up @@ -1760,129 +1734,104 @@ private interface ICompartmentOwnerVisitor {
boolean consume(IIdType theCompartmentOwner);
}

public static class ContainedResources {
private long myNextContainedId = 1;

public static class EmbeddedResources {
private List<IBaseResource> myResourceList;
private IdentityHashMap<IBaseResource, IIdType> myResourceToIdMap;
private Map<String, IBaseResource> myExistingIdToContainedResourceMap;
private Map<IBaseResource, IIdType> myResourceToIdMap;
private Map<IIdType, IBaseResource> myIdToResourceMap;

public Map<String, IBaseResource> getExistingIdToContainedResource() {
if (myExistingIdToContainedResourceMap == null) {
myExistingIdToContainedResourceMap = new HashMap<>();
}
return myExistingIdToContainedResourceMap;
void addResource(IBaseResource theResource) {
addResource(theResource.getIdElement(), theResource);
}

public IIdType addContained(IBaseResource theResource) {
IIdType existing = getResourceToIdMap().get(theResource);
if (existing != null) {
return existing;
}

IIdType newId = theResource.getIdElement();
if (isBlank(newId.getValue())) {
newId.setValue("#" + myNextContainedId++);
} else {
// Avoid auto-assigned contained IDs colliding with pre-existing ones
String idPart = newId.getValue();
if (substring(idPart, 0, 1).equals("#")) {
idPart = idPart.substring(1);
if (StringUtils.isNumeric(idPart)) {
myNextContainedId = Long.parseLong(idPart) + 1;
}
}
}

getResourceToIdMap().put(theResource, newId);
void addResource(IIdType theId, IBaseResource theResource) {
getOrCreateResourceList().add(theResource);
return newId;
getOrCreateResourceToIdMap().put(theResource, theId);
getOrCreateIdToResourceMap().put(theId, theResource);
}

public void addContained(IIdType theId, IBaseResource theResource) {
if (!getResourceToIdMap().containsKey(theResource)) {
getResourceToIdMap().put(theResource, theId);
getOrCreateResourceList().add(theResource);
}
}

public List<IBaseResource> getContainedResources() {
if (getResourceToIdMap() == null) {
return Collections.emptyList();
}
return getOrCreateResourceList();
public IIdType getResourceId(IBaseResource theNext) {
return getOrCreateResourceToIdMap().get(theNext);
}

public IIdType getResourceId(IBaseResource theNext) {
if (getResourceToIdMap() == null) {
return null;
}
return getResourceToIdMap().get(theNext);
public IBaseResource getResource(IIdType theId) {
return getOrCreateIdToResourceMap().get(theId);
}

private List<IBaseResource> getOrCreateResourceList() {
protected List<IBaseResource> getOrCreateResourceList() {
if (myResourceList == null) {
myResourceList = new ArrayList<>();
}
return myResourceList;
}

private IdentityHashMap<IBaseResource, IIdType> getResourceToIdMap() {
protected Map<IBaseResource, IIdType> getOrCreateResourceToIdMap() {
if (myResourceToIdMap == null) {
myResourceToIdMap = new IdentityHashMap<>();
}
return myResourceToIdMap;
}

public boolean isEmpty() {
if (myResourceToIdMap == null) {
return true;
protected Map<IIdType, IBaseResource> getOrCreateIdToResourceMap() {
if (myIdToResourceMap == null) {
myIdToResourceMap = new HashMap<>();
}
return myResourceToIdMap.isEmpty();
return myIdToResourceMap;
}

public boolean hasExistingIdToContainedResource() {
return myExistingIdToContainedResourceMap != null;
public boolean isEmpty() {
return myResourceList == null || myResourceList.isEmpty();
}
}

public void assignIdsToContainedResources() {

if (!getContainedResources().isEmpty()) {
public static class ContainedResources extends EmbeddedResources {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Do we need a new EmbeddedResources intermediate class? Is there a reason we can't apply ordinals to the other cases?
problem: I can't find a test for the ordinal assignment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Embedded resources can also mean resource entries in a bundle. Basically any resource that's embedded into another resource. I guess there may even be other cases. I kept is separate.
I added this test here for now: https://github.com/hapifhir/hapi-fhir/pull/5832/files#diff-d9ea0db9e7f4eb2987dbfb2fa7376d67cac04733f6fca71caeaa363bf207b12aR119. I plan to add some more after I implement the query/search part of the functionality.

private long myNextContainedId = 1;
private Map<IBaseResource, Short> myResourceToOrdMap;

/*
* The idea with the code block below:
*
* We want to preserve any IDs that were user-assigned, so that if it's really
* important to someone that their contained resource have the ID of #FOO
* or #1 we will keep that.
*
* For any contained resources where no ID was assigned by the user, we
* want to manually create an ID but make sure we don't reuse an existing ID.
*/
@Override
protected void addResource(IIdType theId, IBaseResource theResource) {
super.addResource(theId, theResource);
short ordinal = Integer.valueOf(getOrCreateResourceList().size()).shortValue();
getOrCreateResourceToOrdMap().put(theResource, ordinal);
}

Set<String> ids = new HashSet<>();
public IIdType addContained(IBaseResource theResource) {
IIdType id = assignIdIfNeeded(theResource);
addResource(id, theResource);
return id;
}

// Gather any user assigned IDs
for (IBaseResource nextResource : getContainedResources()) {
if (getResourceToIdMap().get(nextResource) != null) {
ids.add(getResourceToIdMap().get(nextResource).getValue());
}
}
private IIdType assignIdIfNeeded(IBaseResource theResource) {
/*
* We want to preserve any IDs that were user-assigned, so that if it's really
* important to someone that their contained resource have the ID of #FOO
* or #1 we will keep that.
* For any contained resources where no ID was assigned by the user, we
* want to manually create an ID but make sure we don't reuse an existing ID.
*/
IIdType newId = theResource.getIdElement();
while (newId.isEmpty() || getOrCreateIdToResourceMap().containsKey(newId)) {
newId.setValue("#" + myNextContainedId++);
}

// Automatically assign IDs to the rest
for (IBaseResource nextResource : getContainedResources()) {
return newId;
}

while (getResourceToIdMap().get(nextResource) == null) {
String nextCandidate = "#" + myNextContainedId;
myNextContainedId++;
if (!ids.add(nextCandidate)) {
continue;
}
public List<IBaseResource> getContainedResources() {
if (getOrCreateResourceToIdMap() == null) {
return Collections.emptyList();
}
return getOrCreateResourceList();
}

getResourceToIdMap().put(nextResource, new IdDt(nextCandidate));
}
}
private Map<IBaseResource, Short> getOrCreateResourceToOrdMap() {
if (myResourceToOrdMap == null) {
myResourceToOrdMap = new IdentityHashMap<>();
}
return myResourceToOrdMap;
}

public Short getContainedResourceOrd(IBaseResource theResource) {
return getOrCreateResourceToOrdMap().get(theResource);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public void testMigrateFrom340() throws IOException, SQLException {
assertEquals("identifier", values.get(0).get("SP_NAME"));
assertEquals("12345678", values.get(0).get("SP_VALUE"));
assertTrue(values.get(0).keySet().contains("HASH_IDENTITY"));
assertEquals(7001889285610424179L, values.get(0).get("HASH_IDENTITY"));
assertEquals(792023395537367244L, values.get(0).get("HASH_IDENTITY"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serious problem: we must never change the hashes of existing data. Why did this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the bit in the hash (boolean) to distinguish contained indexes. I guess I can always leave as is if the property to index them is false. I did think about it, the impact would be less it just requires more branching in the code.

return null;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor;
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.search.hash.ResourceIndexHasher;
import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager;
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
import ca.uhn.fhir.jpa.packages.JpaPackageCache;
Expand Down Expand Up @@ -876,4 +879,10 @@ public ResourceHistoryCalculator resourceHistoryCalculator(
FhirContext theFhirContext, HibernatePropertiesProvider theHibernatePropertiesProvider) {
return new ResourceHistoryCalculator(theFhirContext, theHibernatePropertiesProvider.isOracleDialect());
}

@Bean
public ResourceIndexHasher myResourceIndexHasher(
PartitionSettings thePartitionSettings, StorageSettings theStorageSettings) {
return new ResourceIndexHasher(thePartitionSettings, theStorageSettings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.search.hash.ResourceIndexHasher;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
Expand Down Expand Up @@ -115,6 +116,9 @@ public class TransactionProcessor extends BaseTransactionProcessor {
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionSvc;

@Autowired
private ResourceIndexHasher myResourceIndexHasher;

public void setEntityManagerForUnitTest(EntityManager theEntityManager) {
myEntityManager = theEntityManager;
}
Expand Down Expand Up @@ -502,17 +506,15 @@ private void buildHashPredicateFromTokenParam(
Set<Long> theSysAndValuePredicates,
Set<Long> theValuePredicates) {
if (isNotBlank(theTokenParam.getValue()) && isNotBlank(theTokenParam.getSystem())) {
theMatchUrl.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue(
myPartitionSettings,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

problem: Breaking change. The partition settings used to be used to control hash calculation.

theMatchUrl.myHashSystemAndValue = myResourceIndexHasher.hash(
theRequestPartitionId,
theMatchUrl.myResourceDefinition.getName(),
theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(),
theTokenParam.getSystem(),
theTokenParam.getValue());
theSysAndValuePredicates.add(theMatchUrl.myHashSystemAndValue);
} else if (isNotBlank(theTokenParam.getValue())) {
theMatchUrl.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same.

myPartitionSettings,
theMatchUrl.myHashValue = myResourceIndexHasher.hash(
theRequestPartitionId,
theMatchUrl.myResourceDefinition.getName(),
theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ public interface IResourceIndexedSearchParamDateDao
void deleteByResourceId(@Param("resid") Long theResourcePid);

@Query("SELECT t FROM ResourceIndexedSearchParamDate t WHERE t.myResourcePid = :resId")
List<ResourceIndexedSearchParamDate> findAllForResourceId(@Param("resId") Long thePatientId);
List<ResourceIndexedSearchParamDate> findAllForResourceId(@Param("resId") Long theResourcePid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ public interface IResourceIndexedSearchParamStringDao
void deleteByResourceId(@Param("resId") Long theResourcePid);

@Query("SELECT t FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resId")
List<ResourceIndexedSearchParamString> findAllForResourceId(@Param("resId") Long thePatientId);
List<ResourceIndexedSearchParamString> findAllForResourceId(@Param("resId") Long theResourcePid);
}