Partition patient interceptor (#2766)

* Start work on interceptor

* Implemented

* Add interceptor and docs

* Add docs

* Compile fix

* Test fixes

* Test cleanup

* Test fixes

* Test fixes

* Improve error message

* Add broken tests

* Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2766-add-patient-id-compartment-interceptor.yaml

Co-authored-by: Tadgh <tadgh@cs.toronto.edu>

* Update hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptor.java

Co-authored-by: Tadgh <tadgh@cs.toronto.edu>

* Address review comments

Co-authored-by: Tadgh <garygrantgraham@gmail.com>
Co-authored-by: Tadgh <tadgh@cs.toronto.edu>
This commit is contained in:
James Agnew 2021-07-05 09:15:20 -04:00 committed by GitHub
parent 6bb5cbf764
commit d762a07817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1412 additions and 237 deletions

View File

@ -1848,6 +1848,7 @@ public enum Pointcut implements IPointcut {
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
* </li>
* <li>ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails - Contains details about what is being read</li>
* </ul>
* <p>
* Hooks must return an instance of <code>ca.uhn.fhir.interceptor.model.RequestPartitionId</code>.
@ -1858,7 +1859,8 @@ public enum Pointcut implements IPointcut {
"ca.uhn.fhir.interceptor.model.RequestPartitionId",
// Params
"ca.uhn.fhir.rest.api.server.RequestDetails",
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
"ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails"
),
/**

View File

@ -0,0 +1,76 @@
package ca.uhn.fhir.interceptor.model;
/*-
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import org.hl7.fhir.instance.model.api.IIdType;
public class ReadPartitionIdRequestDetails {
private final String myResourceType;
private final RestOperationTypeEnum myRestOperationType;
private final IIdType myReadResourceId;
private final Object mySearchParams;
public ReadPartitionIdRequestDetails(String theResourceType, RestOperationTypeEnum theRestOperationType, IIdType theReadResourceId, Object theSearchParams) {
myResourceType = theResourceType;
myRestOperationType = theRestOperationType;
myReadResourceId = theReadResourceId;
mySearchParams = theSearchParams;
}
public String getResourceType() {
return myResourceType;
}
public RestOperationTypeEnum getRestOperationType() {
return myRestOperationType;
}
public IIdType getReadResourceId() {
return myReadResourceId;
}
public Object getSearchParams() {
return mySearchParams;
}
public static ReadPartitionIdRequestDetails forRead(String theResourceType, IIdType theId, boolean theIsVread) {
RestOperationTypeEnum op = theIsVread ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
return new ReadPartitionIdRequestDetails(theResourceType, op, theId.withResourceType(theResourceType), null);
}
public static ReadPartitionIdRequestDetails forSearchType(String theResourceType, Object theParams) {
return new ReadPartitionIdRequestDetails(theResourceType, RestOperationTypeEnum.SEARCH_TYPE, null, theParams);
}
public static ReadPartitionIdRequestDetails forHistory(String theResourceType, IIdType theIdType) {
RestOperationTypeEnum restOperationTypeEnum;
if (theIdType != null) {
restOperationTypeEnum = RestOperationTypeEnum.HISTORY_INSTANCE;
} else if (theResourceType != null) {
restOperationTypeEnum = RestOperationTypeEnum.HISTORY_TYPE;
} else {
restOperationTypeEnum = RestOperationTypeEnum.HISTORY_SYSTEM;
}
return new ReadPartitionIdRequestDetails(theResourceType, restOperationTypeEnum, theIdType, null);
}
}

View File

@ -64,4 +64,7 @@ public abstract class BaseOrListParam<MT extends BaseOrListParam<?, ?>, PT exten
return myList.toString();
}
public int size() {
return myList.size();
}
}

View File

@ -0,0 +1,6 @@
---
type: add
issue: 2766
title: "A new JPA partitioning interceptor `PatientIdPartitionInterceptor` has been added. This interceptor uses the
ID of the patient associated with any resources in the patient compartment to generate a consistent
partition ID."

View File

@ -0,0 +1,6 @@
---
type: change
issue: 2766
title: "When operating in partitioned mode, the interceptor pointcut `STORAGE_PARTITION_IDENTIFY_CREATE` will now be invoked
to determine the partition to use for create with client-assigned IDs. Previously the `STORAGE_PARTITION_IDENTIFY_READ`
pointcut was invoked, which was confusing and potentially unexpected."

View File

@ -178,8 +178,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private String myResourceName;
private Class<T> myResourceType;
@Autowired
private IRequestPartitionHelperSvc myPartitionHelperSvc;
@Autowired
private MemoryCacheService myMemoryCacheService;
private TransactionTemplate myTxTemplate;
@ -259,7 +257,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
ResourceTable entity = new ResourceTable();
entity.setResourceType(toResourceName(theResource));
entity.setPartitionId(theRequestPartitionId);
entity.setPartitionId(myRequestPartitionHelperService.toStoragePartition(theRequestPartitionId));
entity.setCreatedByMatchUrl(theIfNoneExist);
entity.setVersion(1);
@ -1211,26 +1209,39 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) {
validateResourceTypeAndThrowInvalidRequestException(theId);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName());
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong());
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequest, getResourceName(), theId);
// Verify that the resource is for the correct partition
if (entity != null && !requestPartitionId.isAllPartitions()) {
if (entity.getPartitionId() != null && entity.getPartitionId().getPartitionId() != null) {
if (!requestPartitionId.hasPartitionId(entity.getPartitionId().getPartitionId())) {
ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId());
entity = null;
BaseHasResource entity;
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, getResourceName(), theId.getIdPart());
Set<Integer> readPartitions = null;
if (requestPartitionId.isAllPartitions()) {
entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong());
} else {
readPartitions = myRequestPartitionHelperService.toReadPartitions(requestPartitionId);
if (readPartitions.size() == 1) {
if (readPartitions.contains(null)) {
entity = myResourceTableDao.readByPartitionIdNull(pid.getIdAsLong()).orElse(null);
} else {
entity = myResourceTableDao.readByPartitionId(readPartitions.iterator().next(), pid.getIdAsLong()).orElse(null);
}
} else {
// Entity Partition ID is null
if (!requestPartitionId.hasPartitionId(null)) {
ourLog.debug("Performing a read for PartitionId=null but entity has partition: {}", entity.getPartitionId());
entity = null;
if (readPartitions.contains(null)) {
List<Integer> readPartitionsWithoutNull = readPartitions.stream().filter(t -> t != null).collect(Collectors.toList());
entity = myResourceTableDao.readByPartitionIdsOrNull(readPartitionsWithoutNull, pid.getIdAsLong()).orElse(null);
} else {
entity = myResourceTableDao.readByPartitionIds(readPartitions, pid.getIdAsLong()).orElse(null);
}
}
}
// Verify that the resource is for the correct partition
if (entity != null && readPartitions != null && entity.getPartitionId() != null) {
if (!readPartitions.contains(entity.getPartitionId().getPartitionId())) {
ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId());
entity = null;
}
}
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
@ -1269,12 +1280,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Nonnull
protected ResourceTable readEntityLatestVersion(IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, getResourceName());
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequestDetails, getResourceName(), theId);
return readEntityLatestVersion(theId, requestPartitionId, theTransactionDetails);
}
@Nonnull
private ResourceTable readEntityLatestVersion(IIdType theId, @Nullable RequestPartitionId theRequestPartitionId, TransactionDetails theTransactionDetails) {
private ResourceTable readEntityLatestVersion(IIdType theId, @Nonnull RequestPartitionId theRequestPartitionId, TransactionDetails theTransactionDetails) {
validateResourceTypeAndThrowInvalidRequestException(theId);
if (theTransactionDetails.isResolvedResourceIdEmpty(theId.toUnqualifiedVersionless())) {
@ -1390,7 +1401,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL));
}
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequest, getResourceName());
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams);
IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective, theRequest, requestPartitionId);
if (retVal instanceof PersistedJpaBundleProvider) {
@ -1483,7 +1494,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
String uuid = UUID.randomUUID().toString();
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName());
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams);
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) {
while (iter.hasNext()) {
retVal.add(iter.next());
@ -1619,7 +1630,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
assert resourceId != null;
assert resourceId.hasIdPart();
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName());
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName());
boolean create = false;
@ -1639,7 +1650,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
if (create) {
requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName());
return doCreateForPostOrPut(resource, null, thePerformIndexing, theTransactionDetails, theRequest, requestPartitionId);
}
}

View File

@ -91,7 +91,7 @@ public abstract class BaseHapiFhirResourceDaoObservation<T extends IBaseResource
TreeMap<Long, IQueryParameterType> orderedSubjectReferenceMap = new TreeMap<>();
if(theSearchParameterMap.containsKey(getSubjectParamName())) {
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, getResourceName());
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
List<List<IQueryParameterType>> patientParams = new ArrayList<>();
if (theSearchParameterMap.get(getPatientParamName()) != null) {

View File

@ -79,7 +79,7 @@ public class FhirResourceDaoPatientDstu2 extends BaseHapiFhirResourceDao<Patient
paramMap.setLoadSynchronous(true);
}
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequest, getResourceName());
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
}

View File

@ -127,7 +127,7 @@ public class TransactionProcessor extends BaseTransactionProcessor {
for (IBase nextEntry : theEntries) {
IBaseResource resource = versionAdapter.getResource(nextEntry);
if (resource != null) {
RequestPartitionId requestPartition = myRequestPartitionSvc.determineReadPartitionForRequest(theRequest, myFhirContext.getResourceType(resource));
RequestPartitionId requestPartition = myRequestPartitionSvc.determineCreatePartitionForRequest(theRequest, resource, myFhirContext.getResourceType(resource));
requestPartitionIdsForAllEntries.add(requestPartition);
}
}

View File

@ -12,6 +12,7 @@ import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/*
* #%L
@ -94,4 +95,16 @@ public interface IResourceTableDao extends JpaRepository<ResourceTable, Long> {
@Query("SELECT t.myVersion FROM ResourceTable t WHERE t.myId = :pid")
Long findCurrentVersionByPid(@Param("pid") Long thePid);
@Query("SELECT t FROM ResourceTable t LEFT JOIN FETCH t.myForcedId WHERE t.myPartitionId.myPartitionId IS NULL AND t.myId = :pid")
Optional<ResourceTable> readByPartitionIdNull(@Param("pid") Long theResourceId);
@Query("SELECT t FROM ResourceTable t LEFT JOIN FETCH t.myForcedId WHERE t.myPartitionId.myPartitionId = :partitionId AND t.myId = :pid")
Optional<ResourceTable> readByPartitionId(@Param("partitionId") int thePartitionId, @Param("pid") Long theResourceId);
@Query("SELECT t FROM ResourceTable t LEFT JOIN FETCH t.myForcedId WHERE (t.myPartitionId.myPartitionId IS NULL OR t.myPartitionId.myPartitionId IN (:partitionIds)) AND t.myId = :pid")
Optional<ResourceTable> readByPartitionIdsOrNull(@Param("partitionIds") Collection<Integer> thrValues, @Param("pid") Long theResourceId);
@Query("SELECT t FROM ResourceTable t LEFT JOIN FETCH t.myForcedId WHERE t.myPartitionId.myPartitionId IN (:partitionIds) AND t.myId = :pid")
Optional<ResourceTable> readByPartitionIds(@Param("partitionIds") Collection<Integer> thrValues, @Param("pid") Long theResourceId);
}

View File

@ -48,7 +48,7 @@ public class FhirResourceDaoObservationDstu3 extends BaseHapiFhirResourceDaoObse
updateSearchParamsForLastn(theSearchParameterMap, theRequestDetails);
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, getResourceName());
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
return mySearchCoordinatorSvc.registerSearch(this, theSearchParameterMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequestDetails, requestPartitionId);
}

View File

@ -43,7 +43,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
public class FhirResourceDaoPatientDstu3 extends BaseHapiFhirResourceDao<Patient>implements IFhirResourceDaoPatient<Patient> {
public class FhirResourceDaoPatientDstu3 extends BaseHapiFhirResourceDao<Patient> implements IFhirResourceDaoPatient<Patient> {
@Autowired
private IRequestPartitionHelperSvc myPartitionHelperSvc;
@ -69,12 +69,12 @@ public class FhirResourceDaoPatientDstu3 extends BaseHapiFhirResourceDao<Patient
if (theId != null) {
paramMap.add("_id", new StringParam(theId.getIdPart()));
}
if (!isPagingProviderDatabaseBacked(theRequest)) {
paramMap.setLoadSynchronous(true);
}
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequest, getResourceName());
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
}

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.cross.ResourceLookup;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
@ -71,6 +72,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder.replaceDefaultPartitionIdIfNonNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
@ -179,6 +181,9 @@ public class IdHelperService {
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
private EntityManager myEntityManager;
@Autowired
private PartitionSettings myPartitionSettings;
/**
* Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs.
* <p>
@ -232,12 +237,20 @@ public class IdHelperService {
Predicate idCriteria = cb.equal(from.get("myForcedId").as(String.class), next.getIdPart());
andPredicates.add(idCriteria);
if (theRequestPartitionId.isDefaultPartition()) {
if (theRequestPartitionId.isDefaultPartition() && myPartitionSettings.getDefaultPartitionId() == null) {
Predicate partitionIdCriteria = cb.isNull(from.get("myPartitionIdValue").as(Integer.class));
andPredicates.add(partitionIdCriteria);
} else if (!theRequestPartitionId.isAllPartitions()) {
Predicate partitionIdCriteria = from.get("myPartitionIdValue").as(Integer.class).in(theRequestPartitionId.getPartitionIds());
andPredicates.add(partitionIdCriteria);
List<Integer> partitionIds = theRequestPartitionId.getPartitionIds();
partitionIds = replaceDefaultPartitionIdIfNonNull(myPartitionSettings, partitionIds);
if (partitionIds.size() > 1) {
Predicate partitionIdCriteria = from.get("myPartitionIdValue").as(Integer.class).in(partitionIds);
andPredicates.add(partitionIdCriteria);
} else {
Predicate partitionIdCriteria = cb.equal(from.get("myPartitionIdValue").as(Integer.class), partitionIds.get(0));
andPredicates.add(partitionIdCriteria);
}
}
predicates.add(cb.and(andPredicates.toArray(EMPTY_PREDICATE_ARRAY)));
@ -310,6 +323,7 @@ public class IdHelperService {
}
List<IResourceLookup> retVal = new ArrayList<>();
RequestPartitionId requestPartitionId = replaceDefault(theRequestPartitionId);
if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) {
List<Long> pids = theId
@ -318,7 +332,7 @@ public class IdHelperService {
.map(t -> t.getIdPartAsLong())
.collect(Collectors.toList());
if (!pids.isEmpty()) {
resolvePids(theRequestPartitionId, pids, retVal);
resolvePids(requestPartitionId, pids, retVal);
}
}
@ -343,15 +357,15 @@ public class IdHelperService {
Collection<Object[]> views;
assert isNotBlank(nextResourceType);
if (theRequestPartitionId.isAllPartitions()) {
if (requestPartitionId.isAllPartitions()) {
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds);
} else {
if (theRequestPartitionId.isDefaultPartition()) {
if (requestPartitionId.isDefaultPartition()) {
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds);
} else if (theRequestPartitionId.hasDefaultPartitionId()) {
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(nextResourceType, nextIds, theRequestPartitionId.getPartitionIdsWithoutDefault());
} else if (requestPartitionId.hasDefaultPartitionId()) {
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(nextResourceType, nextIds, requestPartitionId.getPartitionIdsWithoutDefault());
} else {
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionIds());
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, requestPartitionId.getPartitionIds());
}
}
@ -375,6 +389,20 @@ public class IdHelperService {
return retVal;
}
private RequestPartitionId replaceDefault(RequestPartitionId theRequestPartitionId) {
if (myPartitionSettings.getDefaultPartitionId() != null) {
if (theRequestPartitionId.hasDefaultPartitionId()) {
List<Integer> partitionIds = theRequestPartitionId
.getPartitionIds()
.stream()
.map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t)
.collect(Collectors.toList());
return RequestPartitionId.fromPartitionIds(partitionIds);
}
}
return theRequestPartitionId;
}
private void resolvePids(@Nonnull RequestPartitionId theRequestPartitionId, List<Long> thePidsToResolve, List<IResourceLookup> theTarget) {
if (!myDaoConfig.isDeleteEnabled()) {
@ -470,6 +498,11 @@ public class IdHelperService {
return retVal;
}
/**
* @deprecated This method doesn't take a partition ID as input, so it is unsafe. It
* should be reworked to include the partition ID before any new use is incorporated
*/
@Deprecated
@Nullable
public Long getPidOrNull(IBaseResource theResource) {
IAnyResource anyResource = (IAnyResource) theResource;
@ -485,6 +518,11 @@ public class IdHelperService {
return retVal;
}
/**
* @deprecated This method doesn't take a partition ID as input, so it is unsafe. It
* should be reworked to include the partition ID before any new use is incorporated
*/
@Deprecated
@Nonnull
public Long getPidOrThrowException(IIdType theId) {
List<IIdType> ids = Collections.singletonList(theId);
@ -492,6 +530,11 @@ public class IdHelperService {
return resourcePersistentIds.get(0).getIdAsLong();
}
/**
* @deprecated This method doesn't take a partition ID as input, so it is unsafe. It
* should be reworked to include the partition ID before any new use is incorporated
*/
@Deprecated
@Nonnull
public List<Long> getPidsOrThrowException(List<IIdType> theIds) {
List<ResourcePersistentId> resourcePersistentIds = this.resolveResourcePersistentIdsWithCache(RequestPartitionId.allPartitions(), theIds);

View File

@ -53,7 +53,7 @@ public class FhirResourceDaoObservationR4 extends BaseHapiFhirResourceDaoObserva
public IBundleProvider observationsLastN(SearchParameterMap theSearchParameterMap, RequestDetails theRequestDetails, HttpServletResponse theServletResponse) {
updateSearchParamsForLastn(theSearchParameterMap, theRequestDetails);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, getResourceName());
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
return mySearchCoordinatorSvc.registerSearch(this, theSearchParameterMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequestDetails, requestPartitionId);
}

View File

@ -74,7 +74,7 @@ public class FhirResourceDaoPatientR4 extends BaseHapiFhirResourceDao<Patient>im
paramMap.setLoadSynchronous(true);
}
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequest, getResourceName());
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
}

View File

@ -48,7 +48,7 @@ public class FhirResourceDaoObservationR5 extends BaseHapiFhirResourceDaoObserva
updateSearchParamsForLastn(theSearchParameterMap, theRequestDetails);
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, getResourceName());
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, getResourceName(), theSearchParameterMap);
return mySearchCoordinatorSvc.registerSearch(this, theSearchParameterMap, getResourceName(), new CacheControlDirective().parse(theRequestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequestDetails, requestPartitionId);
}

View File

@ -74,7 +74,7 @@ public class FhirResourceDaoPatientR5 extends BaseHapiFhirResourceDao<Patient> i
paramMap.setLoadSynchronous(true);
}
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequest(theRequest, getResourceName());
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), paramMap);
return mySearchCoordinatorSvc.registerSearch(this, paramMap, getResourceName(), new CacheControlDirective().parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)), theRequest, requestPartitionId);
}

View File

@ -92,7 +92,7 @@ public class DeleteExpungeJobSubmitterImpl implements IDeleteExpungeJobSubmitter
List<RequestPartitionId> retval = new ArrayList<>();
for (String url : theUrlsToDeleteExpunge) {
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(url);
RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequest, resourceSearch.getResourceName());
RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequest, resourceSearch.getResourceName(), null);
retval.add(requestPartitionId);
}
return retval;

View File

@ -0,0 +1,256 @@
package ca.uhn.fhir.jpa.interceptor;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This interceptor allows JPA servers to be partitioned by Patient ID. It selects the compartment for read/create operations
* based on the patient ID associated with the resource (and uses a default partition ID for any resources
* not in the patient compartment).
*/
@Interceptor
public class PatientIdPartitionInterceptor {
@Autowired
private FhirContext myFhirContext;
/**
* Constructor
*/
public PatientIdPartitionInterceptor() {
super();
}
/**
* Constructor
*/
public PatientIdPartitionInterceptor(FhirContext theFhirContext) {
this();
myFhirContext = theFhirContext;
}
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
public RequestPartitionId identifyForCreate(IBaseResource theResource, RequestDetails theRequestDetails) {
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResource);
List<RuntimeSearchParam> compartmentSps = getCompartmentSearchParams(resourceDef);
if (compartmentSps.isEmpty()) {
return provideNonCompartmentMemberTypeResponse(theResource);
}
String compartmentIdentity;
if (resourceDef.getName().equals("Patient")) {
compartmentIdentity = theResource.getIdElement().getIdPart();
if (isBlank(compartmentIdentity)) {
throw new MethodNotAllowedException("Patient resource IDs must be client-assigned in patient compartment mode");
}
} else {
IFhirPath fhirPath = myFhirContext.newFhirPath();
compartmentIdentity = compartmentSps
.stream()
.flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath())))
.filter(StringUtils::isNotBlank)
.map(path -> fhirPath.evaluateFirst(theResource, path, IBaseReference.class))
.filter(Optional::isPresent)
.map(Optional::get)
.map(t -> t.getReferenceElement().getValue())
.map(t -> new IdType(t).getIdPart())
.filter(StringUtils::isNotBlank)
.findFirst()
.orElse(null);
if (isBlank(compartmentIdentity)) {
return provideNonCompartmentMemberInstanceResponse(theResource);
}
}
return provideCompartmentMemberInstanceResponse(theRequestDetails, compartmentIdentity);
}
@Nonnull
private List<RuntimeSearchParam> getCompartmentSearchParams(RuntimeResourceDefinition resourceDef) {
return resourceDef
.getSearchParams()
.stream()
.filter(param -> param.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
.filter(param -> param.getProvidesMembershipInCompartments() != null && param.getProvidesMembershipInCompartments().contains("Patient"))
.collect(Collectors.toList());
}
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
public RequestPartitionId identifyForRead(ReadPartitionIdRequestDetails theReadDetails, RequestDetails theRequestDetails) {
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theReadDetails.getResourceType());
List<RuntimeSearchParam> compartmentSps = getCompartmentSearchParams(resourceDef);
if (compartmentSps.isEmpty()) {
return provideNonCompartmentMemberTypeResponse(null);
}
//noinspection EnumSwitchStatementWhichMissesCases
switch (theReadDetails.getRestOperationType()) {
case READ:
case VREAD:
if ("Patient".equals(theReadDetails.getResourceType())) {
return provideCompartmentMemberInstanceResponse(theRequestDetails, theReadDetails.getReadResourceId().getIdPart());
}
break;
case SEARCH_TYPE:
SearchParameterMap params = (SearchParameterMap) theReadDetails.getSearchParams();
String idPart = null;
if ("Patient".equals(theReadDetails.getResourceType())) {
idPart = getSingleResourceIdValueOrNull(params, "_id", "Patient");
} else {
for (RuntimeSearchParam nextCompartmentSp : compartmentSps) {
idPart = getSingleResourceIdValueOrNull(params, nextCompartmentSp.getName(), "Patient");
if (idPart != null) {
break;
}
}
}
if (isNotBlank(idPart)) {
return provideCompartmentMemberInstanceResponse(theRequestDetails, idPart);
}
break;
default:
// nothing
}
return provideUnsupportedQueryResponse(theReadDetails);
}
private String getSingleResourceIdValueOrNull(SearchParameterMap theParams, String theParamName, String theResourceType) {
String idPart = null;
List<List<IQueryParameterType>> idParamAndList = theParams.get(theParamName);
if (idParamAndList != null && idParamAndList.size() == 1) {
List<IQueryParameterType> idParamOrList = idParamAndList.get(0);
if (idParamOrList.size() == 1) {
IQueryParameterType idParam = idParamOrList.get(0);
if (isNotBlank(idParam.getQueryParameterQualifier())) {
throw new MethodNotAllowedException("The parameter " + theParamName + idParam.getQueryParameterQualifier() + " is not supported in patient compartment mode");
}
if (idParam instanceof ReferenceParam) {
String chain = ((ReferenceParam) idParam).getChain();
if (chain != null) {
throw new MethodNotAllowedException("The parameter " + theParamName + "." + chain + " is not supported in patient compartment mode");
}
}
IdType id = new IdType(idParam.getValueAsQueryToken(myFhirContext));
if (!id.hasResourceType() || id.getResourceType().equals(theResourceType)) {
idPart = id.getIdPart();
}
} else if (idParamOrList.size() > 1) {
throw new MethodNotAllowedException("Multiple values for parameter " + theParamName + " is not supported in patient compartment mode");
}
} else if (idParamAndList != null && idParamAndList.size() > 1) {
throw new MethodNotAllowedException("Multiple values for parameter " + theParamName + " is not supported in patient compartment mode");
}
return idPart;
}
/**
* Return a partition or throw an error for FHIR operations that can not be used with this interceptor
*/
protected RequestPartitionId provideUnsupportedQueryResponse(ReadPartitionIdRequestDetails theRequestDetails) {
throw new MethodNotAllowedException("This server is not able to handle this request of type " + theRequestDetails.getRestOperationType());
}
/**
* Generate the partition for a given patient resource ID. This method may be overridden in subclasses, but it
* may be easier to override {@link #providePartitionIdForPatientId(RequestDetails, String)} instead.
*/
@Nonnull
protected RequestPartitionId provideCompartmentMemberInstanceResponse(RequestDetails theRequestDetails, String theResourceIdPart) {
int partitionId = providePartitionIdForPatientId(theRequestDetails, theResourceIdPart);
return RequestPartitionId.fromPartitionId(partitionId);
}
/**
* Translates an ID (e.g. "ABC") into a compartment ID number.
* <p>
* The default implementation of this method returns:
* <code>Math.abs(theResourceIdPart.hashCode()) % 15000</code>.
* <p>
* This logic can be replaced with other logic of your choosing.
*/
@SuppressWarnings("unused")
protected int providePartitionIdForPatientId(RequestDetails theRequestDetails, String theResourceIdPart) {
return Math.abs(theResourceIdPart.hashCode()) % 15000;
}
/**
* Return a compartment ID (or throw an exception) when an attempt is made to search for a resource that is
* in the patient compartment, but without any search parameter identifying which compartment to search.
* <p>
* E.g. this method will be called for the search <code>Observation?code=foo</code> since the patient
* is not identified in the URL.
*/
@Nonnull
protected RequestPartitionId provideNonCompartmentMemberInstanceResponse(IBaseResource theResource) {
throw new MethodNotAllowedException("Resource of type " + myFhirContext.getResourceType(theResource) + " has no values placing it in the Patient compartment");
}
/**
* Return a compartment ID (or throw an exception) when storing/reading resource types that
* are not in the patient compartment (e.g. ValueSet).
*/
@SuppressWarnings("unused")
@Nonnull
protected RequestPartitionId provideNonCompartmentMemberTypeResponse(IBaseResource theResource) {
return RequestPartitionId.defaultPartition();
}
}

View File

@ -20,17 +20,47 @@ package ca.uhn.fhir.jpa.partition;
* #L%
*/
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Set;
public interface IRequestPartitionHelperSvc {
@Nonnull
RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType);
RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType, @Nonnull ReadPartitionIdRequestDetails theDetails);
@Nonnull
default RequestPartitionId determineReadPartitionForRequestForRead(RequestDetails theRequest, String theResourceType, IIdType theId) {
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forRead(theResourceType, theId, theId.hasVersionIdPart());
return determineReadPartitionForRequest(theRequest, theResourceType, details);
}
@Nonnull
default RequestPartitionId determineReadPartitionForRequestForSearchType(RequestDetails theRequest, String theResourceType, SearchParameterMap theParams) {
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forSearchType(theResourceType, theParams);
return determineReadPartitionForRequest(theRequest, theResourceType, details);
}
@Nonnull
default RequestPartitionId determineReadPartitionForRequestForHistory(RequestDetails theRequest, String theResourceType, IIdType theIdType) {
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(theResourceType, theIdType);
return determineReadPartitionForRequest(theRequest, theResourceType, details);
}
@Nonnull
RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource, @Nonnull String theResourceType);
@Nonnull
PartitionablePartitionId toStoragePartition(@Nonnull RequestPartitionId theRequestPartitionId);
@Nonnull
Set<Integer> toReadPartitions(@Nonnull RequestPartitionId theRequestPartitionId);
}

View File

@ -23,8 +23,10 @@ package ca.uhn.fhir.jpa.partition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.data.IPartitionDao;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
@ -51,6 +53,8 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
private static final Pattern PARTITION_NAME_VALID_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+");
private static final Logger ourLog = LoggerFactory.getLogger(PartitionLookupSvcImpl.class);
@Autowired
private PartitionSettings myPartitionSettings;
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired
@ -62,6 +66,13 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
@Autowired
private FhirContext myFhirCtx;
/**
* Constructor
*/
public PartitionLookupSvcImpl() {
super();
}
@Override
@PostConstruct
public void start() {
@ -79,6 +90,7 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
@Override
public PartitionEntity getPartitionByName(String theName) {
Validate.notBlank(theName, "The name must not be null or blank");
validateNotInUnnamedPartitionMode();
if (JpaConstants.DEFAULT_PARTITION_NAME.equals(theName)) {
return null;
}
@ -88,6 +100,12 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
@Override
public PartitionEntity getPartitionById(Integer thePartitionId) {
validatePartitionIdSupplied(myFhirCtx, thePartitionId);
if (myPartitionSettings.isUnnamedPartitionMode()) {
return new PartitionEntity().setId(thePartitionId);
}
if (myPartitionSettings.getDefaultPartitionId() != null && myPartitionSettings.getDefaultPartitionId().equals(thePartitionId)) {
return new PartitionEntity().setId(thePartitionId).setName(JpaConstants.DEFAULT_PARTITION_NAME);
}
return myIdToPartitionCache.get(thePartitionId);
}
@ -100,6 +118,7 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
@Override
@Transactional
public PartitionEntity createPartition(PartitionEntity thePartition) {
validateNotInUnnamedPartitionMode();
validateHaveValidPartitionIdAndName(thePartition);
validatePartitionNameDoesntAlreadyExist(thePartition.getName());
@ -112,6 +131,7 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
@Override
@Transactional
public PartitionEntity updatePartition(PartitionEntity thePartition) {
validateNotInUnnamedPartitionMode();
validateHaveValidPartitionIdAndName(thePartition);
Optional<PartitionEntity> existingPartitionOpt = myPartitionDao.findById(thePartition.getId());
@ -136,6 +156,7 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
@Transactional
public void deletePartition(Integer thePartitionId) {
validatePartitionIdSupplied(myFhirCtx, thePartitionId);
validateNotInUnnamedPartitionMode();
Optional<PartitionEntity> partition = myPartitionDao.findById(thePartitionId);
if (!partition.isPresent()) {
@ -173,6 +194,12 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc {
}
private void validateNotInUnnamedPartitionMode() {
if (myPartitionSettings.isUnnamedPartitionMode()) {
throw new MethodNotAllowedException("Can not invoke this operation in unnamed partition mode");
}
}
private class NameToPartitionCacheLoader implements @NonNull CacheLoader<String, PartitionEntity> {
@Nullable
@Override

View File

@ -25,9 +25,11 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
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.interceptor.model.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -45,6 +47,8 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME;
import static ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster.doCallHooks;
@ -98,7 +102,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
*/
@Nonnull
@Override
public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType) {
public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType, ReadPartitionIdRequestDetails theDetails) {
RequestPartitionId requestPartitionId;
boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType);
@ -115,7 +119,8 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
} else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, theRequest)) {
HookParams params = new HookParams()
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest);
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(ReadPartitionIdRequestDetails.class, theDetails);
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params);
} else {
requestPartitionId = null;
@ -210,6 +215,26 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
return RequestPartitionId.allPartitions();
}
@Nonnull
@Override
public PartitionablePartitionId toStoragePartition(@Nonnull RequestPartitionId theRequestPartitionId) {
Integer partitionId = theRequestPartitionId.getFirstPartitionIdOrNull();
if (partitionId == null) {
partitionId = myPartitionSettings.getDefaultPartitionId();
}
return new PartitionablePartitionId(partitionId, theRequestPartitionId.getPartitionDate());
}
@Nonnull
@Override
public Set<Integer> toReadPartitions(@Nonnull RequestPartitionId theRequestPartitionId) {
return theRequestPartitionId
.getPartitionIds()
.stream()
.map(t->t == null ? myPartitionSettings.getDefaultPartitionId() : t)
.collect(Collectors.toSet());
}
/**
* If the partition only has a name but not an ID, this method resolves the ID.
* <p>

View File

@ -54,6 +54,7 @@ import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -151,7 +152,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(mySearchEntity.getResourceType(), mySearchEntity.getResourceId(), mySearchEntity.getLastUpdatedLow(), mySearchEntity.getLastUpdatedHigh());
RequestPartitionId partitionId = getRequestPartitionId();
RequestPartitionId partitionId = getRequestPartitionIdForHistory();
List<ResourceHistoryTable> results = historyBuilder.fetchEntities(partitionId, theOffset, theFromIndex, theToIndex);
List<IBaseResource> retVal = new ArrayList<>();
@ -196,13 +197,13 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
}
@Nonnull
private RequestPartitionId getRequestPartitionId() {
private RequestPartitionId getRequestPartitionIdForHistory() {
if (myRequestPartitionId == null) {
if (mySearchEntity.getResourceId() != null) {
// If we have an ID, we've already checked the partition and made sure it's appropriate
myRequestPartitionId = RequestPartitionId.allPartitions();
} else {
myRequestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(myRequest, mySearchEntity.getResourceType());
myRequestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(myRequest, mySearchEntity.getResourceType(), null);
}
}
return myRequestPartitionId;
@ -271,7 +272,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
Function<MemoryCacheService.HistoryCountKey, Integer> supplier = k -> new TransactionTemplate(myTxManager).execute(t -> {
HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(mySearchEntity.getResourceType(), mySearchEntity.getResourceId(), mySearchEntity.getLastUpdatedLow(), mySearchEntity.getLastUpdatedHigh());
Long count = historyBuilder.fetchCount(getRequestPartitionId());
Long count = historyBuilder.fetchCount(getRequestPartitionIdForHistory());
return count.intValue();
});

View File

@ -45,7 +45,6 @@ import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.InterceptorUtil;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.CacheControlDirective;
@ -64,6 +63,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.method.PageMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
import ca.uhn.fhir.util.AsyncUtil;
import ca.uhn.fhir.util.StopWatch;
@ -103,7 +103,6 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
@ -124,13 +123,13 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public static final Integer INTEGER_0 = 0;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchCoordinatorSvcImpl.class);
private final ConcurrentHashMap<String, SearchTask> myIdToSearchTask = new ConcurrentHashMap<>();
private final ExecutorService myExecutor;
@Autowired
private FhirContext myContext;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private EntityManager myEntityManager;
private final ExecutorService myExecutor;
private Integer myLoadingThrottleForUnitTests = null;
private long myMaxMillisToWaitForRemoteResults = DateUtils.MILLIS_PER_MINUTE;
private boolean myNeverUseLocalSearchForUnitTests;
@ -272,7 +271,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
String resourceType = search.getResourceType();
SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search"));
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(resourceType);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, resourceType);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequestDetails, resourceType, params);
SearchContinuationTask task = new SearchContinuationTask(search, resourceDao, params, resourceType, theRequestDetails, requestPartitionId);
myIdToSearchTask.put(search.getUuid(), task);
myExecutor.submit(task);

View File

@ -21,10 +21,9 @@ package ca.uhn.fhir.jpa.search.builder.predicate;
*/
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.Condition;
import com.healthmarketscience.sqlbuilder.InCondition;
import com.healthmarketscience.sqlbuilder.NotCondition;
import com.healthmarketscience.sqlbuilder.UnaryCondition;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
@ -33,6 +32,7 @@ import org.apache.commons.lang3.Validate;
import javax.annotation.Nullable;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
@ -70,18 +70,25 @@ public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder {
@Nullable
public Condition createPartitionIdPredicate(RequestPartitionId theRequestPartitionId) {
if (theRequestPartitionId != null && !theRequestPartitionId.isAllPartitions()) {
Condition condition;
if (theRequestPartitionId.isDefaultPartition()) {
boolean defaultPartitionIsNull = getPartitionSettings().getDefaultPartitionId() == null;
if (theRequestPartitionId.isDefaultPartition() && defaultPartitionIsNull) {
condition = UnaryCondition.isNull(getPartitionIdColumn());
} else if (theRequestPartitionId.hasDefaultPartitionId()) {
} else if (theRequestPartitionId.hasDefaultPartitionId() && defaultPartitionIsNull) {
List<String> placeholders = generatePlaceholders(theRequestPartitionId.getPartitionIdsWithoutDefault());
UnaryCondition partitionNullPredicate = UnaryCondition.isNull(getPartitionIdColumn());
InCondition partitionIdsPredicate = new InCondition(getPartitionIdColumn(), placeholders);
Condition partitionIdsPredicate = toEqualToOrInPredicate(getPartitionIdColumn(), placeholders);
condition = toOrPredicate(partitionNullPredicate, partitionIdsPredicate);
} else {
List<String> placeholders = generatePlaceholders(theRequestPartitionId.getPartitionIds());
condition = new InCondition(getPartitionIdColumn(), placeholders);
List<Integer> partitionIds = theRequestPartitionId.getPartitionIds();
partitionIds = replaceDefaultPartitionIdIfNonNull(getPartitionSettings(), partitionIds);
List<String> placeholders = generatePlaceholders(partitionIds);
condition = toEqualToOrInPredicate(getPartitionIdColumn(), placeholders);
}
return condition;
} else {
@ -101,5 +108,16 @@ public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder {
}
public static List<Integer> replaceDefaultPartitionIdIfNonNull(PartitionSettings thePartitionSettings, List<Integer> thePartitionIds) {
List<Integer> partitionIds = thePartitionIds;
if (thePartitionSettings.getDefaultPartitionId() != null) {
partitionIds = partitionIds
.stream()
.map(t -> t == null ? thePartitionSettings.getDefaultPartitionId() : t)
.collect(Collectors.toList());
}
return partitionIds;
}
}

View File

@ -56,6 +56,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toOrPredicate;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -300,11 +301,7 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder {
if (!haveMultipleColumns && conditions.length > 1) {
List<Long> values = Arrays.asList(hashes);
InCondition predicate = new InCondition(columns[0], generatePlaceholders(values));
if (!theWantEquals) {
predicate.setNegate(true);
}
return predicate;
return toEqualToOrInPredicate(columns[0], generatePlaceholders(values), !theWantEquals);
}
for (int i = 0; i < conditions.length; i++) {

View File

@ -11,16 +11,19 @@ import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao;
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
@ -31,6 +34,7 @@ import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.stresstest.GiantTransactionPerfTest;
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionLoader;
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
@ -168,7 +172,11 @@ public abstract class BaseJpaTest extends BaseTest {
@Autowired
private IResourceTableDao myResourceTableDao;
@Autowired
private IResourceTagDao myResourceTagDao;
@Autowired
private IResourceHistoryTableDao myResourceHistoryTableDao;
@Autowired
private IForcedIdDao myForcedIdDao;
@AfterEach
public void afterPerformCleanup() {
@ -284,6 +292,14 @@ public abstract class BaseJpaTest extends BaseTest {
});
}
protected int logAllForcedIds() {
return runInTransaction(() -> {
List<ForcedId> forcedIds = myForcedIdDao.findAll();
ourLog.info("Resources:\n * {}", forcedIds.stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
return forcedIds.size();
});
}
protected void logAllDateIndexes() {
runInTransaction(() -> {
ourLog.info("Date indexes:\n * {}", myResourceIndexedSearchParamDateDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
@ -296,6 +312,12 @@ public abstract class BaseJpaTest extends BaseTest {
});
}
protected void logAllResourceTags() {
runInTransaction(() -> {
ourLog.info("Token tags:\n * {}", myResourceTagDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
});
}
public TransactionTemplate newTxTemplate() {
TransactionTemplate retVal = new TransactionTemplate(getTxManager());
retVal.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

View File

@ -31,14 +31,15 @@ import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PartitioningSqlR4Test.class);
static final String PARTITION_1 = "PART-1";
static final String PARTITION_2 = "PART-2";
static final String PARTITION_3 = "PART-3";
static final String PARTITION_4 = "PART-4";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PartitioningSqlR4Test.class);
protected MyReadWriteInterceptor myPartitionInterceptor;
protected LocalDate myPartitionDate;
protected LocalDate myPartitionDate2;
@ -55,6 +56,7 @@ public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest {
myPartitionSettings.setIncludePartitionInSearchHashes(new PartitionSettings().isIncludePartitionInSearchHashes());
myPartitionSettings.setPartitioningEnabled(new PartitionSettings().isPartitioningEnabled());
myPartitionSettings.setAllowReferencesAcrossPartitions(new PartitionSettings().getAllowReferencesAcrossPartitions());
myPartitionSettings.setDefaultPartitionId(new PartitionSettings().getDefaultPartitionId());
mySrdInterceptorService.unregisterInterceptorsIf(t -> t instanceof MyReadWriteInterceptor);
myInterceptor = null;
@ -135,6 +137,10 @@ public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest {
myHaveDroppedForcedIdUniqueConstraint = true;
}
protected void addCreatePartition(Integer thePartitionId) {
addCreatePartition(thePartitionId, null);
}
protected void addCreatePartition(Integer thePartitionId, LocalDate thePartitionDate) {
Validate.notNull(thePartitionId);
RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionId(thePartitionId, thePartitionDate);
@ -183,18 +189,6 @@ public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest {
};
}
protected Consumer<IBaseResource> withPutPartition(Integer thePartitionId) {
return t -> {
if (thePartitionId != null) {
addReadPartition(thePartitionId);
addCreatePartition(thePartitionId, null);
} else {
addReadDefaultPartition();
addCreateDefaultPartition();
}
};
}
@Interceptor
public static class MyReadWriteInterceptor extends MyWriteInterceptor {
@ -215,7 +209,7 @@ public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest {
@Override
public void assertNoRemainingIds() {
super.assertNoRemainingIds();
assertEquals(0, myReadRequestPartitionIds.size());
assertEquals(0, myReadRequestPartitionIds.size(), () -> "Found " + myReadRequestPartitionIds.size() + " READ partitions remaining in interceptor");
}
}
@ -233,13 +227,14 @@ public abstract class BasePartitioningR4Test extends BaseJpaR4SystemTest {
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
public RequestPartitionId PartitionIdentifyCreate(IBaseResource theResource, ServletRequestDetails theRequestDetails) {
assertNotNull(theResource);
assertTrue(!myCreateRequestPartitionIds.isEmpty(), "No create partitions left in interceptor");
RequestPartitionId retVal = myCreateRequestPartitionIds.remove(0);
ourLog.info("Returning partition for create: {}", retVal);
return retVal;
}
public void assertNoRemainingIds() {
assertEquals(0, myCreateRequestPartitionIds.size());
assertEquals(0, myCreateRequestPartitionIds.size(), () -> "Still have " + myCreateRequestPartitionIds.size() + " CREATE partitions remaining in interceptor");
}
}

View File

@ -260,6 +260,28 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
}
@Test
public void testCreateInvalidUnquotedExtensionUrl() {
String invalidExpression = "Patient.extension.where(url=http://foo).value";
SearchParameter fooSp = new SearchParameter();
fooSp.setCode("foo");
fooSp.setType(Enumerations.SearchParamType.STRING);
fooSp.setTitle("FOO SP");
fooSp.setExpression(invalidExpression);
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
fooSp.addBase("Patient");
try {
mySearchParameterDao.create(fooSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), containsString("The token : is not expected here"));
}
}
@Test
public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter();
@ -278,7 +300,6 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
}
@Test
@Disabled
public void testCreateInvalidParamInvalidResourceName() {

View File

@ -0,0 +1,194 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hamcrest.Matchers;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@SuppressWarnings({"unchecked", "ConstantConditions"})
public class PartitioningNonNullDefaultPartitionR4Test extends BasePartitioningR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(PartitioningNonNullDefaultPartitionR4Test.class);
@BeforeEach
@Override
public void before() throws ServletException {
super.before();
myPartitionSettings.setDefaultPartitionId(1);
}
@AfterEach
@Override
public void after() {
super.after();
myPartitionSettings.setDefaultPartitionId(new PartitionSettings().getDefaultPartitionId());
}
@Test
public void testCreateAndSearch_NonPartitionable() {
addCreateDefaultPartition();
SearchParameter sp = new SearchParameter();
sp.addBase("Patient");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setType(Enumerations.SearchParamType.REFERENCE);
sp.setCode("extpatorg");
sp.setName("extpatorg");
sp.setExpression("Patient.extension('http://patext').value.as(Reference)");
Long id = mySearchParameterDao.create(sp, mySrd).getId().getIdPartAsLong();
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(id).orElseThrow(IllegalArgumentException::new);
assertEquals(1, resourceTable.getPartitionId().getPartitionId().intValue());
});
// Search on Token
addReadDefaultPartition();
List<String> outcome = toUnqualifiedVersionlessIdValues(mySearchParameterDao.search(SearchParameterMap.newSynchronous().add("code", new TokenParam("extpatorg")), mySrd));
assertThat(outcome, Matchers.contains("SearchParameter/" + id));
// Search on All Resources
addReadDefaultPartition();
outcome = toUnqualifiedVersionlessIdValues(mySearchParameterDao.search(SearchParameterMap.newSynchronous(), mySrd));
assertThat(outcome, Matchers.contains("SearchParameter/" + id));
}
@Test
public void testCreateAndSearch_NonPartitionable_ForcedId() {
addCreateDefaultPartition();
SearchParameter sp = new SearchParameter();
sp.setId("SearchParameter/A");
sp.addBase("Patient");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setType(Enumerations.SearchParamType.REFERENCE);
sp.setCode("extpatorg");
sp.setName("extpatorg");
sp.setExpression("Patient.extension('http://patext').value.as(Reference)");
mySearchParameterDao.update(sp, mySrd);
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findAll().get(0);
assertEquals(1, resourceTable.getPartitionId().getPartitionId().intValue());
});
// Search on Token
addReadDefaultPartition();
List<String> outcome = toUnqualifiedVersionlessIdValues(mySearchParameterDao.search(SearchParameterMap.newSynchronous().add("code", new TokenParam("extpatorg")), mySrd));
assertThat(outcome, Matchers.contains("SearchParameter/A"));
// Search on All Resources
addReadDefaultPartition();
outcome = toUnqualifiedVersionlessIdValues(mySearchParameterDao.search(SearchParameterMap.newSynchronous(), mySrd));
assertThat(outcome, Matchers.contains("SearchParameter/A"));
}
@Test
public void testCreateAndSearch_Partitionable_ForcedId() {
addCreateDefaultPartition();
Patient patient = new Patient();
patient.setId("A");
patient.addIdentifier().setSystem("http://foo").setValue("123");
patient.setActive(true);
myPatientDao.update(patient, mySrd);
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findAll().get(0);
assertEquals(1, resourceTable.getPartitionId().getPartitionId().intValue());
});
// Search on Token
addReadDefaultPartition();
List<String> outcome = toUnqualifiedVersionlessIdValues(myPatientDao.search(SearchParameterMap.newSynchronous().add("identifier", new TokenParam("http://foo", "123")), mySrd));
assertThat(outcome, Matchers.contains("Patient/A"));
// Search on All Resources
addReadDefaultPartition();
outcome = toUnqualifiedVersionlessIdValues(myPatientDao.search(SearchParameterMap.newSynchronous(), mySrd));
assertThat(outcome, Matchers.contains("Patient/A"));
}
@Test
public void testCreateAndSearch_Partitionable() {
addCreateDefaultPartition();
Patient patient = new Patient();
patient.getMeta().addTag().setSystem("http://foo").setCode("TAG");
patient.addIdentifier().setSystem("http://foo").setValue("123");
patient.setActive(true);
Long id = myPatientDao.create(patient, mySrd).getId().getIdPartAsLong();
logAllResourceTags();
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(id).orElseThrow(IllegalArgumentException::new);
assertEquals(1, resourceTable.getPartitionId().getPartitionId().intValue());
});
// Search on Token
addReadDefaultPartition();
List<String> outcome = toUnqualifiedVersionlessIdValues(myPatientDao.search(SearchParameterMap.newSynchronous().add("identifier", new TokenParam("http://foo", "123")), mySrd));
assertThat(outcome, Matchers.contains("Patient/" + id));
// Search on Tag
addReadDefaultPartition();
outcome = toUnqualifiedVersionlessIdValues(myPatientDao.search(SearchParameterMap.newSynchronous().add("_tag", new TokenParam("http://foo", "TAG")), mySrd));
assertThat(outcome, Matchers.contains("Patient/" + id));
// Search on All Resources
addReadDefaultPartition();
outcome = toUnqualifiedVersionlessIdValues(myPatientDao.search(SearchParameterMap.newSynchronous(), mySrd));
assertThat(outcome, Matchers.contains("Patient/" + id));
}
@Test
public void testRead_Partitionable() {
addCreateDefaultPartition();
Patient patient = new Patient();
patient.getMeta().addTag().setSystem("http://foo").setCode("TAG");
patient.addIdentifier().setSystem("http://foo").setValue("123");
patient.setActive(true);
Long id = myPatientDao.create(patient, mySrd).getId().getIdPartAsLong();
addReadDefaultPartition();
patient = myPatientDao.read(new IdType("Patient/" + id), mySrd);
assertTrue(patient.getActive());
// Wrong partition
addReadPartition(2);
try {
myPatientDao.read(new IdType("Patient/" + id), mySrd);
fail();
} catch (ResourceNotFoundException e) {
// good
}
}
}

View File

@ -175,7 +175,6 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
myPartitionSettings.setAllowReferencesAcrossPartitions(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED);
// Create patient in partition 1
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId, myPartitionDate);
Patient patient = new Patient();
patient.setId("ONE");
@ -201,7 +200,6 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
public void testCreate_CrossPartitionReference_ByForcedId_NotAllowed() {
// Create patient in partition 1
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId, myPartitionDate);
Patient patient = new Patient();
patient.setId("ONE");
@ -248,7 +246,6 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testCreate_SamePartitionReference_DefaultPartition_ByForcedId() {
// Create patient in partition NULL
addReadDefaultPartition();
addCreateDefaultPartition(myPartitionDate);
Patient patient = new Patient();
patient.setId("ONE");
@ -319,7 +316,6 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
addCreatePartition(1, null);
addCreatePartition(1, null);
addReadPartition(1);
IIdType patientId1 = createPatient(withOrganization(new IdType("Organization/FOO")));
addReadPartition(1);
@ -534,14 +530,12 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testCreate_ForcedId_WithPartition() {
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId, myPartitionDate);
Organization org = new Organization();
org.setId("org");
org.setName("org");
IIdType orgId = myOrganizationDao.update(org, mySrd).getId().toUnqualifiedVersionless();
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId, myPartitionDate);
Patient p = new Patient();
p.setId("pat");
@ -562,14 +556,12 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testCreate_ForcedId_NoPartition() {
addReadDefaultPartition();
addCreateDefaultPartition();
Organization org = new Organization();
org.setId("org");
org.setName("org");
IIdType orgId = myOrganizationDao.update(org, mySrd).getId().toUnqualifiedVersionless();
addReadDefaultPartition();
addCreateDefaultPartition();
Patient p = new Patient();
p.setId("pat");
@ -588,14 +580,12 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testCreate_ForcedId_DefaultPartition() {
addReadDefaultPartition();
addCreateDefaultPartition(myPartitionDate);
Organization org = new Organization();
org.setId("org");
org.setName("org");
IIdType orgId = myOrganizationDao.update(org, mySrd).getId().toUnqualifiedVersionless();
addReadDefaultPartition();
addCreateDefaultPartition(myPartitionDate);
Patient p = new Patient();
p.setId("pat");
@ -620,8 +610,8 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
createUniqueCompositeSp();
createRequestId();
addReadPartition(myPartitionId);
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId, myPartitionDate);
addCreatePartition(myPartitionId, myPartitionDate);
addCreatePartition(myPartitionId, myPartitionDate);
addCreatePartition(myPartitionId, myPartitionDate);
@ -676,7 +666,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
});
// Update that resource
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId);
patient = new Patient();
patient.setId("Patient/" + patientId);
patient.setActive(false);
@ -822,8 +812,8 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", searchSql);
// Only the read columns should be used, no criteria use partition
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID='1'"), searchSql);
}
// Read in null Partition
@ -859,6 +849,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Two partitions - Found
{
myCaptureQueriesListener.clear();
myPartitionInterceptor.assertNoRemainingIds();
myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionNames(PARTITION_1, PARTITION_2));
IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId1, gotId1);
@ -866,7 +857,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Only the read columns should be used, but no selectors on partition ID
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID in ("), searchSql);
}
// Two partitions including default - Found
@ -879,7 +870,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Only the read columns should be used, but no selectors on partition ID
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID is null"), searchSql);
}
// Two partitions - Not Found
@ -920,12 +911,13 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Only the read columns should be used, but no selectors on partition ID
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID in ("), searchSql);
}
// Two partitions including default - Found
{
myCaptureQueriesListener.clear();
myPartitionInterceptor.assertNoRemainingIds();
myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionIds(1, null));
IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientIdNull, gotId1);
@ -933,7 +925,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Only the read columns should be used, but no selectors on partition ID
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "), searchSql);
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID is null"), searchSql);
}
// Two partitions - Not Found
@ -975,7 +967,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Only the read columns should be used, no criteria use partition
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID is null"));
}
// Read in wrong Partition
@ -1044,9 +1036,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testRead_ForcedId_SpecificPartition() {
IIdType patientIdNull = createPatient(withPutPartition(null), withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(withPutPartition(1), withActiveTrue(), withId("ONE"));
IIdType patientId2 = createPatient(withPutPartition(2), withActiveTrue(), withId("TWO"));
IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("ONE"));
IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withId("TWO"));
// Read in correct Partition
addReadPartition(1);
@ -1089,9 +1081,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testRead_ForcedId_DefaultPartition() {
IIdType patientIdNull = createPatient(withPutPartition(null), withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(withPutPartition(1), withActiveTrue(), withId("ONE"));
IIdType patientId2 = createPatient(withPutPartition(2), withActiveTrue(), withId("TWO"));
IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("ONE"));
IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withId("TWO"));
// Read in correct Partition
addReadDefaultPartition();
@ -1119,9 +1111,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testRead_ForcedId_AllPartition() {
IIdType patientIdNull = createPatient(withPutPartition(null), withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(withPutPartition(1), withActiveTrue(), withId("ONE"));
createPatient(withPutPartition(2), withActiveTrue(), withId("TWO"));
IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("ONE"));
createPatient(withPartition(2), withActiveTrue(), withId("TWO"));
{
addReadAllPartitions();
IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
@ -1137,9 +1129,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testRead_ForcedId_AllPartition_WithDuplicate() {
dropForcedIdUniqueConstraint();
IIdType patientIdNull = createPatient(withPutPartition(null), withActiveTrue(), withId("FOO"));
IIdType patientId1 = createPatient(withPutPartition(1), withActiveTrue(), withId("FOO"));
IIdType patientId2 = createPatient(withPutPartition(2), withActiveTrue(), withId("FOO"));
IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("FOO"));
IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("FOO"));
IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withId("FOO"));
assertEquals(patientIdNull, patientId1);
assertEquals(patientIdNull, patientId2);
@ -1179,7 +1171,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", searchSql);
// Only the read columns should be used, no criteria use partition
assertThat(searchSql, searchSql, containsString("PARTITION_ID IN ('1')"));
assertThat(searchSql, searchSql, containsString("PARTITION_ID = '1'"));
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
}
@ -1231,7 +1223,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", searchSql);
// Only the read columns should be used, no criteria use partition
assertThat(searchSql, searchSql, containsString("PARTITION_ID IN ('1')"));
assertThat(searchSql, searchSql, containsString("PARTITION_ID = '1'"));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); // If this switches to 1 that would be fine
}
@ -1262,11 +1254,8 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testSearch_IdParamOnly_ForcedId_SpecificPartition() {
addReadPartition(new Integer[]{null});
IIdType patientIdNull = createPatient(withPartition(null), withId("PT-NULL"), withActiveTrue());
addReadPartition(1);
IIdType patientId1 = createPatient(withPartition(1), withId("PT-1"), withActiveTrue());
addReadPartition(2);
IIdType patientId2 = createPatient(withPartition(2), withId("PT-2"), withActiveTrue());
/* *******************************
@ -1280,6 +1269,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
SearchParameterMap map = SearchParameterMap.newSynchronous(IAnyResource.SP_RES_ID, new TokenParam(patientId1.toUnqualifiedVersionless().getValue()));
IBundleProvider searchOutcome = myPatientDao.search(map, mySrd);
myCaptureQueriesListener.logSelectQueries();
assertEquals(1, searchOutcome.size());
IIdType gotId1 = searchOutcome.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId1, gotId1);
@ -1315,11 +1305,8 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
@Test
public void testSearch_IdParamSecond_ForcedId_SpecificPartition() {
addReadPartition(new Integer[]{null});
IIdType patientIdNull = createPatient(withPartition(null), withId("PT-NULL"), withActiveTrue());
addReadPartition(1);
IIdType patientId1 = createPatient(withPartition(1), withId("PT-1"), withActiveTrue());
addReadPartition(2);
IIdType patientId2 = createPatient(withPartition(2), withId("PT-2"), withActiveTrue());
/* *******************************
@ -1348,7 +1335,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Second SQL performs the search
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false).toUpperCase();
ourLog.info("Search SQL:\n{}", searchSql);
assertThat(searchSql, searchSql, containsString("PARTITION_ID IN ('1')"));
assertThat(searchSql, searchSql, containsString("PARTITION_ID = '1'"));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql); // If this switches to 1 that would be fine
}
@ -1440,7 +1427,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "SP_MISSING = 'true'"), searchSql);
}
@ -1457,7 +1444,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"));
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_MISSING = 'false'"));
}
}
@ -1552,7 +1539,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "HFJ_RES_PARAM_PRESENT"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "HASH_PRESENCE = '-3438137196820602023'"), searchSql);
}
@ -1580,7 +1567,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "HFJ_RES_PARAM_PRESENT"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "HASH_PRESENCE = '1919227773735728687'"), searchSql);
}
@ -1692,7 +1679,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertThat(sql, sql, containsString("PARTITION_ID IN ('2')"));
assertThat(sql, sql, containsString("PARTITION_ID = '2'"));
assertThat(sql, sql, containsString("PARTITION_ID IS NULL"));
}
@ -1938,23 +1925,50 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
}
@Test
public void testSearch_DateParam_SearchDefaultPartitions_NonNullDefaultPartition() {
myPartitionSettings.setIncludePartitionInSearchHashes(false);
myPartitionSettings.setDefaultPartitionId(-1);
IIdType patientIdNull = createPatient(withPartition(null), withBirthdate("2020-04-20"));
createPatient(withPartition(1), withBirthdate("2020-04-20"));
createPatient(withPartition(2), withBirthdate("2020-04-20"));
createPatient(withPartition(null), withBirthdate("2021-04-20"));
createPatient(withPartition(1), withBirthdate("2021-04-20"));
createPatient(withPartition(2), withBirthdate("2021-04-20"));
// Date param
addReadDefaultPartition();
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20"));
map.setLoadSynchronous(true);
IBundleProvider results = myPatientDao.search(map, mySrd);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
assertThat(ids, contains(patientIdNull));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID = '-1'"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
}
@Test
public void testSearch_HasParam_SearchOnePartition() {
addReadPartition(1);
addCreatePartition(1, null);
Organization org = new Organization();
org.setId("ORG");
org.setName("ORG");
myOrganizationDao.update(org, mySrd);
addReadPartition(1);
addCreatePartition(1, null);
Practitioner practitioner = new Practitioner();
practitioner.setId("PRACT");
practitioner.addName().setFamily("PRACT");
myPractitionerDao.update(practitioner, mySrd);
addReadPartition(1);
addCreatePartition(1, null);
PractitionerRole role = new PractitionerRole();
role.setId("ROLE");
@ -2094,7 +2108,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertThat(searchSql, containsString("PARTITION_ID IS NULL"));
assertThat(searchSql, containsString("PARTITION_ID IN ('1')"));
assertThat(searchSql, containsString("PARTITION_ID = '1'"));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"));
}
}
@ -2487,7 +2501,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID IN ('1')"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.PARTITION_ID = '1'"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.SRC_PATH = 'Observation.subject'"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "t0.TARGET_RESOURCE_ID = '" + patientId.getIdPartAsLong() + "'"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
@ -2546,7 +2560,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
public void testSearch_RefParam_TargetForcedId_SearchOnePartition() {
createUniqueCompositeSp();
IIdType patientId = createPatient(withPutPartition(myPartitionId), withId("ONE"), withBirthdate("2020-01-01"));
IIdType patientId = createPatient(withPartition(myPartitionId), withId("ONE"), withBirthdate("2020-01-01"));
IIdType observationId = createObservation(withPartition(myPartitionId), withSubject(patientId));
addReadPartition(myPartitionId);
@ -2561,7 +2575,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("Search SQL:\n{}", myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
assertEquals(1, StringUtils.countMatches(searchSql.toUpperCase(Locale.US), "PARTITION_ID IN ('1')"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql.toUpperCase(Locale.US), "PARTITION_ID = '1'"), searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"), searchSql);
// Same query, different partition
@ -2595,9 +2609,9 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
vs.getCompose().addInclude().setSystem("http://cs");
myValueSetDao.create(vs, new SystemRequestDetails());
createObservation(withPutPartition(1), withId("OBS1"), withObservationCode("http://cs", "A"));
createObservation(withPutPartition(1), withId("OBS2"), withObservationCode("http://cs", "B"));
createObservation(withPutPartition(1), withId("OBS3"), withObservationCode("http://cs", "C"));
createObservation(withPartition(1), withId("OBS1"), withObservationCode("http://cs", "A"));
createObservation(withPartition(1), withId("OBS2"), withObservationCode("http://cs", "B"));
createObservation(withPartition(1), withId("OBS3"), withObservationCode("http://cs", "C"));
logAllTokenIndexes();
@ -2616,7 +2630,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
public void testSearch_RefParam_TargetForcedId_SearchDefaultPartition() {
createUniqueCompositeSp();
IIdType patientId = createPatient(withPutPartition(null), withId("ONE"), withBirthdate("2020-01-01"));
IIdType patientId = createPatient(withPartition(null), withId("ONE"), withBirthdate("2020-01-01"));
IIdType observationId = createObservation(withPartition(null), withSubject(patientId));
addReadDefaultPartition();
@ -2697,10 +2711,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
ourLog.info("About to start transaction");
for (int i = 0; i < 20; i++) {
addReadPartition(1);
}
for (int i = 0; i < 8; i++) {
for (int i = 0; i < 28; i++) {
addCreatePartition(1, null);
}
@ -2768,11 +2779,15 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
}
/**
* JA: I disabled this test - I am not clear on what it was actually trying to test
*/
@Test
@Disabled
public void testUpdate_ResourcePreExistsInWrongPartition() {
IIdType patientId = createPatient(withPutPartition(null), withId("ONE"), withBirthdate("2020-01-01"));
IIdType patientId = createPatient(withPartition(null), withId("ONE"), withBirthdate("2020-01-01"));
addReadAllPartitions();
addCreatePartition(1);
Patient patient = new Patient();
patient.setId(patientId.toUnqualifiedVersionless());
@ -2785,7 +2800,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01"));
// Update the patient
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId);
Patient patient = new Patient();
patient.setActive(false);
patient.setId(id);
@ -2803,7 +2818,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
// Resolve resource
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("SQL:{}", searchSql);
assertEquals(0, countMatches(searchSql, "PARTITION_ID="), searchSql);
assertEquals(1, countMatches(searchSql, "PARTITION_ID="), searchSql);
// Fetch history resource
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true);
@ -2822,7 +2837,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01"));
// Update the patient
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId);
Patient p = new Patient();
p.setActive(false);
p.setId(id);
@ -2842,7 +2857,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
IIdType id = createPatient(withPartition(null), withBirthdate("2020-01-01"));
// Update the patient
addReadDefaultPartition();
addCreateDefaultPartition();
Patient patient = new Patient();
patient.setActive(false);
patient.setId(id);
@ -2878,7 +2893,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test {
IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01"));
// Update the patient
addReadPartition(myPartitionId);
addCreatePartition(myPartitionId);
Patient patient = new Patient();
patient.setActive(false);
patient.setId(id);

View File

@ -0,0 +1,326 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4SystemTest;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
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 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.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest {
public static final int ALTERNATE_DEFAULT_ID = -1;
private PatientIdPartitionInterceptor mySvc;
private ForceOffsetSearchModeInterceptor myForceOffsetSearchModeInterceptor;
@BeforeEach
public void before() {
mySvc = new PatientIdPartitionInterceptor(myFhirCtx);
myForceOffsetSearchModeInterceptor = new ForceOffsetSearchModeInterceptor();
myInterceptorRegistry.registerInterceptor(mySvc);
myInterceptorRegistry.registerInterceptor(myForceOffsetSearchModeInterceptor);
myPartitionSettings.setPartitioningEnabled(true);
myPartitionSettings.setUnnamedPartitionMode(true);
myPartitionSettings.setDefaultPartitionId(ALTERNATE_DEFAULT_ID);
}
@AfterEach
public void after() {
myInterceptorRegistry.unregisterInterceptor(mySvc);
myInterceptorRegistry.unregisterInterceptor(myForceOffsetSearchModeInterceptor);
myPartitionSettings.setPartitioningEnabled(false);
myPartitionSettings.setUnnamedPartitionMode(new PartitionSettings().isUnnamedPartitionMode());
myPartitionSettings.setDefaultPartitionId(new PartitionSettings().getDefaultPartitionId());
}
@Test
public void testCreatePatient_ClientAssignedId() {
createPatientA();
runInTransaction(() -> {
ResourceTable pt = myResourceTableDao.findAll().iterator().next();
assertEquals("A", pt.getIdDt().getIdPart());
assertEquals(65, pt.getPartitionId().getPartitionId());
});
}
@Test
public void testCreatePatient_NonClientAssignedId() {
Patient patient = new Patient();
patient.setActive(true);
try {
myPatientDao.create(patient);
fail();
} catch (MethodNotAllowedException e) {
assertEquals("Patient resource IDs must be client-assigned in patient compartment mode", e.getMessage());
}
}
@Test
public void testCreateObservation_ValidMembershipInCompartment() {
createPatientA();
Observation obs = new Observation();
obs.getSubject().setReference("Patient/A");
Long id = myObservationDao.create(obs).getId().getIdPartAsLong();
runInTransaction(() -> {
ResourceTable observation = myResourceTableDao.findById(id).orElseThrow(() -> new IllegalArgumentException());
assertEquals("Observation", observation.getResourceType());
assertEquals(65, observation.getPartitionId().getPartitionId());
});
}
/**
* Type is not in the patient compartment
*/
@Test
public void testCreateOrganization_ValidMembershipInCompartment() {
Organization org = new Organization();
org.setName("Foo");
Long id = myOrganizationDao.create(org).getId().getIdPartAsLong();
runInTransaction(() -> {
ResourceTable observation = myResourceTableDao.findById(id).orElseThrow(() -> new IllegalArgumentException());
assertEquals("Organization", observation.getResourceType());
assertEquals(ALTERNATE_DEFAULT_ID, observation.getPartitionId().getPartitionId().intValue());
});
}
@Test
public void testReadPatient_Good() {
createPatientA();
myCaptureQueriesListener.clear();
Patient patient = myPatientDao.read(new IdType("Patient/A"), mySrd);
assertTrue(patient.getActive());
myCaptureQueriesListener.logSelectQueries();
assertEquals(3, myCaptureQueriesListener.getSelectQueries().size());
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false), containsString("forcedid0_.PARTITION_ID in (?)"));
assertThat(myCaptureQueriesListener.getSelectQueries().get(1).getSql(false, false), containsString("where resourceta0_.PARTITION_ID=? and resourceta0_.RES_ID=?"));
}
@Test
public void testReadObservation_Good() {
createPatientA();
Observation obs = new Observation();
obs.getSubject().setReference("Patient/A");
Long id = myObservationDao.create(obs).getId().getIdPartAsLong();
try {
myObservationDao.read(new IdType("Observation/" + id), mySrd);
} catch (MethodNotAllowedException e) {
assertEquals("This server is not able to handle this request of type READ", e.getMessage());
}
}
@Test
public void testReadPatientHistory_Good() {
Patient patientA = createPatientA();
patientA.setGender(Enumerations.AdministrativeGender.MALE);
myPatientDao.update(patientA);
IdType patientVersionOne = new IdType("Patient", "A", "1");
myCaptureQueriesListener.clear();
Patient patient = myPatientDao.read(patientVersionOne);
assertEquals("1", patient.getIdElement().getVersionIdPart());
myCaptureQueriesListener.logSelectQueries();
assertEquals(4, myCaptureQueriesListener.getSelectQueries().size());
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false), containsString("PARTITION_ID in (?)"));
assertThat(myCaptureQueriesListener.getSelectQueries().get(1).getSql(false, false), containsString("PARTITION_ID="));
}
@Test
public void testSearchPatient_Good() {
createPatientA();
myCaptureQueriesListener.clear();
IBundleProvider outcome = myPatientDao.search(SearchParameterMap.newSynchronous("_id", new TokenParam("A")), mySrd);
assertEquals(1, outcome.size());
myCaptureQueriesListener.logSelectQueries();
assertEquals(3, myCaptureQueriesListener.getSelectQueries().size());
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false), containsString("forcedid0_.PARTITION_ID in (?)"));
assertThat(myCaptureQueriesListener.getSelectQueries().get(1).getSql(false, false), containsString("t0.PARTITION_ID = ?"));
}
@Test
public void testSearchObservation_Good() {
createPatientA();
createObservationB();
myCaptureQueriesListener.clear();
IBundleProvider outcome = myObservationDao.search(SearchParameterMap.newSynchronous("subject", new ReferenceParam("Patient/A")), mySrd);
assertEquals(1, outcome.size());
myCaptureQueriesListener.logSelectQueries();
assertEquals(2, myCaptureQueriesListener.getSelectQueries().size());
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false), containsString("SELECT t0.SRC_RESOURCE_ID FROM HFJ_RES_LINK t0 WHERE ((t0.PARTITION_ID = ?)"));
// Typed
myCaptureQueriesListener.clear();
ReferenceParam referenceParam = new ReferenceParam();
referenceParam.setValueAsQueryToken(myFhirCtx, "subject", ":Patient", "A");
outcome = myObservationDao.search(SearchParameterMap.newSynchronous("subject", referenceParam), mySrd);
assertEquals(1, outcome.size());
myCaptureQueriesListener.logSelectQueries();
assertEquals(2, myCaptureQueriesListener.getSelectQueries().size());
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false), containsString("SELECT t0.SRC_RESOURCE_ID FROM HFJ_RES_LINK t0 WHERE ((t0.PARTITION_ID = ?)"));
}
@Test
public void testSearchObservation_NoCompartmentMembership() {
createPatientA();
createObservationB();
myCaptureQueriesListener.clear();
try {
myObservationDao.search(SearchParameterMap.newSynchronous(), mySrd);
} catch (MethodNotAllowedException e) {
assertEquals("This server is not able to handle this request of type SEARCH_TYPE", e.getMessage());
}
}
@Test
public void testSearchObservation_MultipleCompartmentMembership() {
createPatientA();
createObservationB();
// Multiple ANDs
try {
myObservationDao.search(SearchParameterMap.newSynchronous()
.add("subject", new TokenParam("http://foo", "1"))
.add("subject", new TokenParam("http://foo", "2"))
, mySrd);
} catch (MethodNotAllowedException e) {
assertEquals("Multiple values for parameter subject is not supported in patient compartment mode", e.getMessage());
}
// Multiple ORs
try {
myObservationDao.search(SearchParameterMap.newSynchronous()
.add(
"subject", new TokenOrListParam("http://foo", "1", "2")
), mySrd);
} catch (MethodNotAllowedException e) {
assertEquals("Multiple values for parameter subject is not supported in patient compartment mode", e.getMessage());
}
}
@Test
public void testSearchObservation_ChainedValue() {
createPatientA();
createObservationB();
// Chain
try {
myObservationDao.search(SearchParameterMap.newSynchronous().add("subject", new ReferenceParam("identifier", "http://foo|123")), mySrd);
} catch (MethodNotAllowedException e) {
assertEquals("The parameter subject.identifier is not supported in patient compartment mode", e.getMessage());
}
// Missing
try {
myObservationDao.search(SearchParameterMap.newSynchronous().add("subject", new ReferenceParam("Patient/ABC").setMdmExpand(true)), mySrd);
} catch (MethodNotAllowedException e) {
assertEquals("The parameter subject:mdm is not supported in patient compartment mode", e.getMessage());
}
}
/**
* Type is not in the patient compartment
*/
@Test
public void testSearchOrganization_Good() {
createOrganizationC();
myCaptureQueriesListener.clear();
IBundleProvider outcome = myOrganizationDao.search(SearchParameterMap.newSynchronous(), mySrd);
assertEquals(1, outcome.size());
myCaptureQueriesListener.logSelectQueries();
assertEquals(2, myCaptureQueriesListener.getSelectQueries().size());
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false), containsString("t0.PARTITION_ID = ?"));
}
@Test
public void testHistory_Instance() {
Organization org = createOrganizationC();
org.setName("name 2");
logAllResources();
logAllForcedIds();
myOrganizationDao.update(org);
myCaptureQueriesListener.clear();
IBundleProvider outcome = myOrganizationDao.history(new IdType("Organization/C"), null, null, null, mySrd);
assertEquals(2, outcome.size());
myCaptureQueriesListener.logSelectQueries();
assertEquals(3, myCaptureQueriesListener.getSelectQueries().size());
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(false, false), containsString("PARTITION_ID in "));
assertThat(myCaptureQueriesListener.getSelectQueries().get(1).getSql(false, false), containsString("PARTITION_ID="));
}
@Test
public void testHistory_Type() {
myOrganizationDao.history(null, null, null, mySrd);
}
@Test
public void testHistory_System() {
mySystemDao.history(null, null, null, mySrd);
}
private Organization createOrganizationC() {
Organization org = new Organization();
org.setId("C");
org.setName("Foo");
myOrganizationDao.update(org);
return org;
}
private void createObservationB() {
Observation obs = new Observation();
obs.setId("B");
obs.getSubject().setReference("Patient/A");
myObservationDao.update(obs);
}
private Patient createPatientA() {
Patient patient = new Patient();
patient.setId("Patient/A");
patient.setActive(true);
DaoMethodOutcome update = myPatientDao.update(patient);
return (Patient)update.getResource();
}
}

View File

@ -4,12 +4,17 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.dao.data.INpmPackageDao;
import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.interceptor.PatientIdPartitionInterceptor;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
import ca.uhn.fhir.util.ClasspathUtil;
import org.hl7.fhir.utilities.npm.NpmPackage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
@ -24,9 +29,9 @@ import static org.junit.jupiter.api.Assertions.fail;
public class JpaPackageCacheTest extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(JpaPackageCacheTest.class);
@Autowired
private IHapiPackageCacheManager myPackageCacheManager;
@Autowired
private INpmPackageDao myPackageDao;
@Autowired
@ -39,13 +44,13 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
@AfterEach
public void disablePartitioning() {
myPartitionSettings.setPartitioningEnabled(false);
myPartitionSettings.setDefaultPartitionId(new PartitionSettings().getDefaultPartitionId());
myInterceptorService.unregisterInterceptor(myRequestTenantPartitionInterceptor);
}
@Test
public void testSavePackage() throws IOException {
try (InputStream stream = IgInstallerDstu3Test.class.getResourceAsStream("/packages/basisprofil.de.tar.gz")) {
try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/basisprofil.de.tar.gz")) {
myPackageCacheManager.addPackageToCache("basisprofil.de", "0.2.40", stream, "basisprofil.de");
}
@ -65,13 +70,14 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
}
}
@Test
public void testSaveAndDeletePackagePartitionsEnabled() throws IOException {
myPartitionSettings.setPartitioningEnabled(true);
myPartitionSettings.setDefaultPartitionId(1);
myInterceptorService.registerInterceptor(new PatientIdPartitionInterceptor());
myInterceptorService.registerInterceptor(myRequestTenantPartitionInterceptor);
try (InputStream stream = IgInstallerDstu3Test.class.getResourceAsStream("/packages/basisprofil.de.tar.gz")) {
try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/basisprofil.de.tar.gz")) {
myPackageCacheManager.addPackageToCache("basisprofil.de", "0.2.40", stream, "basisprofil.de");
}
@ -90,15 +96,16 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
assertEquals("Unable to locate package basisprofil.de#99", e.getMessage());
}
logAllResources();
PackageDeleteOutcomeJson deleteOutcomeJson = myPackageCacheManager.uninstallPackage("basisprofil.de", "0.2.40");
List<String> deleteOutcomeMsgs = deleteOutcomeJson.getMessage();
assertEquals("Deleting package basisprofil.de#0.2.40", deleteOutcomeMsgs.get(0));
}
@Test
public void testSavePackageWithLongDescription() throws IOException {
try (InputStream stream = IgInstallerDstu3Test.class.getResourceAsStream("/packages/package-davinci-cdex-0.2.0.tgz")) {
try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/package-davinci-cdex-0.2.0.tgz")) {
myPackageCacheManager.addPackageToCache("hl7.fhir.us.davinci-cdex", "0.2.0", stream, "hl7.fhir.us.davinci-cdex");
}
@ -112,14 +119,8 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
}
@Test
public void testSavePackageCorrectFhirVersion() {
}
@Test
public void testPackageIdHandlingIsNotCaseSensitive() throws IOException {
public void testPackageIdHandlingIsNotCaseSensitive() {
String packageNameAllLowercase = "hl7.fhir.us.davinci-cdex";
String packageNameUppercase = packageNameAllLowercase.toUpperCase(Locale.ROOT);
InputStream stream = IgInstallerDstu3Test.class.getResourceAsStream("/packages/package-davinci-cdex-0.2.0.tgz");
@ -131,9 +132,9 @@ public class JpaPackageCacheTest extends BaseJpaR4Test {
@Test
public void testNonMatchingPackageIdsCauseError() throws IOException {
String incorrectPackageName = "hl7.fhir.us.davinci-nonsense";
InputStream stream = IgInstallerDstu3Test.class.getResourceAsStream("/packages/package-davinci-cdex-0.2.0.tgz");
assertThrows(InvalidRequestException.class, () -> myPackageCacheManager.addPackageToCache(incorrectPackageName, "0.2.0", stream, "hl7.fhir.us.davinci-cdex"));
try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/package-davinci-cdex-0.2.0.tgz")) {
assertThrows(InvalidRequestException.class, () -> myPackageCacheManager.addPackageToCache(incorrectPackageName, "0.2.0", stream, "hl7.fhir.us.davinci-cdex"));
}
}
}

View File

@ -3,7 +3,9 @@ package ca.uhn.fhir.jpa.partition;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -11,6 +13,11 @@ import static org.junit.jupiter.api.Assertions.fail;
public class PartitionSettingsSvcImplTest extends BaseJpaR4Test {
@AfterEach
public void after() {
myPartitionSettings.setUnnamedPartitionMode(false);
}
@Test
public void testCreateAndFetchPartition() {
@ -27,6 +34,21 @@ public class PartitionSettingsSvcImplTest extends BaseJpaR4Test {
assertEquals("NAME123", partition.getName());
}
@Test
public void testCreatePartition_BlockedInUnnamedPartitionMode() {
myPartitionSettings.setUnnamedPartitionMode(true);
PartitionEntity partition = new PartitionEntity();
partition.setId(123);
partition.setName("NAME123");
partition.setDescription("A description");
try {
myPartitionConfigSvc.createPartition(partition);
} catch (MethodNotAllowedException e) {
assertEquals("Can not invoke this operation in unnamed partition mode", e.getMessage());
}
}
@Test
public void testDeletePartition() {

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -43,7 +44,7 @@ class RequestPartitionHelperSvcTest {
when(myPartitionLookupSvc.getPartitionById(PARTITION_ID)).thenReturn(ourPartitionEntity);
// execute
RequestPartitionId result = mySvc.determineReadPartitionForRequest(srd, "Patient");
RequestPartitionId result = mySvc.determineReadPartitionForRequestForRead(srd, "Patient", new IdType("Patient/123"));
// verify
assertEquals(PARTITION_ID, result.getFirstPartitionIdOrNull());

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -24,6 +25,7 @@ import ca.uhn.fhir.jpa.dao.r4.FhirSystemDaoR4;
import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
@ -86,6 +88,7 @@ import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.CriteriaUpdate;
import javax.persistence.metamodel.Metamodel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -828,7 +831,7 @@ public class GiantTransactionPerfTest {
private static class MockRequestPartitionHelperSvc implements ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc {
@Nonnull
@Override
public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType) {
public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType, @Nonnull ReadPartitionIdRequestDetails theDetails) {
return RequestPartitionId.defaultPartition();
}
@ -837,6 +840,21 @@ public class GiantTransactionPerfTest {
public RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource, @Nonnull String theResourceType) {
return RequestPartitionId.defaultPartition();
}
@Override
@Nonnull
public PartitionablePartitionId toStoragePartition(@Nonnull RequestPartitionId theRequestPartitionId) {
return new PartitionablePartitionId(theRequestPartitionId.getFirstPartitionIdOrNull(), theRequestPartitionId.getPartitionDate());
}
@Nonnull
@Override
public Set<Integer> toReadPartitions(@Nonnull RequestPartitionId theRequestPartitionId) {
assert theRequestPartitionId.getPartitionIds().size() == 1;
return Collections.singleton(theRequestPartitionId.getFirstPartitionIdOrNull());
}
}
private static class MockTransactionManager implements PlatformTransactionManager {

View File

@ -28,6 +28,8 @@ public class PartitionSettings {
private boolean myPartitioningEnabled = false;
private CrossPartitionReferenceMode myAllowReferencesAcrossPartitions = CrossPartitionReferenceMode.NOT_ALLOWED;
private boolean myIncludePartitionInSearchHashes = false;
private boolean myUnnamedPartitionMode;
private Integer myDefaultPartitionId;
/**
* If set to <code>true</code> (default is <code>false</code>) the <code>PARTITION_ID</code> value will be factored into the
@ -92,6 +94,48 @@ public class PartitionSettings {
myAllowReferencesAcrossPartitions = theAllowReferencesAcrossPartitions;
}
/**
* If set to <code>true</code> (default is <code>false</code>), partitions will be unnamed and all IDs from {@link Integer#MIN_VALUE} through
* {@link Integer#MAX_VALUE} will be allowed without needing to be created ahead of time.
*
* @since 5.5.0
*/
public boolean isUnnamedPartitionMode() {
return myUnnamedPartitionMode;
}
/**
* If set to <code>true</code> (default is <code>false</code>), partitions will be unnamed and all IDs from {@link Integer#MIN_VALUE} through
* {@link Integer#MAX_VALUE} will be allowed without needing to be created ahead of time.
*
* @since 5.5.0
*/
public void setUnnamedPartitionMode(boolean theUnnamedPartitionMode) {
myUnnamedPartitionMode = theUnnamedPartitionMode;
}
/**
* If set, the given ID will be used for the default partition. The default is
* <code>null</code> which will result in the use of a null value for default
* partition items.
*
* @since 5.5.0
*/
public Integer getDefaultPartitionId() {
return myDefaultPartitionId;
}
/**
* If set, the given ID will be used for the default partition. The default is
* <code>null</code> which will result in the use of a null value for default
* partition items.
*
* @since 5.5.0
*/
public void setDefaultPartitionId(Integer theDefaultPartitionId) {
myDefaultPartitionId = theDefaultPartitionId;
}
public enum CrossPartitionReferenceMode {

View File

@ -50,13 +50,4 @@ public class BasePartitionable implements Serializable {
myPartitionId = thePartitionId;
}
public void setPartitionId(@Nullable RequestPartitionId theRequestPartitionId) {
if (theRequestPartitionId != null) {
myPartitionId = new PartitionablePartitionId(theRequestPartitionId.getFirstPartitionIdOrNull(), theRequestPartitionId.getPartitionDate());
} else {
myPartitionId = null;
}
}
}

View File

@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.model.entity;
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.model.api.IQueryParameterType;
@ -35,7 +34,6 @@ import com.google.common.hash.Hashing;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
@ -153,11 +151,6 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex {
public abstract IQueryParameterType toQueryParameterType();
@Override
public void setPartitionId(@Nullable RequestPartitionId theRequestPartitionId) {
super.setPartitionId(theRequestPartitionId);
}
public boolean matches(IQueryParameterType theParam) {
throw new UnsupportedOperationException("No parameter matcher for " + theParam);
}
@ -171,15 +164,15 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex {
return this;
}
public ModelConfig getModelConfig() {
return myModelConfig;
}
public BaseResourceIndexedSearchParam setModelConfig(ModelConfig theModelConfig) {
myModelConfig = theModelConfig;
return this;
}
public ModelConfig getModelConfig() {
return myModelConfig;
}
public static long calculateHashIdentity(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName) {
RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
return calculateHashIdentity(thePartitionSettings, requestPartitionId, theResourceType, theParamName);

View File

@ -41,6 +41,13 @@ import javax.persistence.UniqueConstraint;
@Entity()
@Table(name = ForcedId.HFJ_FORCED_ID, uniqueConstraints = {
@UniqueConstraint(name = "IDX_FORCEDID_RESID", columnNames = {"RESOURCE_PID"}),
/*
* This index is called IDX_FORCEDID_TYPE_FID and guarantees
* uniqueness of RESOURCE_TYPE,FORCED_ID. This doesn't make sense
* for partitioned servers, so we replace it on those servers
* with IDX_FORCEDID_TYPE_PFID covering
* PARTITION_ID,RESOURCE_TYPE,FORCED_ID
*/
@UniqueConstraint(name = ForcedId.IDX_FORCEDID_TYPE_FID, columnNames = {"RESOURCE_TYPE", "FORCED_ID"})
}, indexes = {
/*
@ -116,11 +123,14 @@ public class ForcedId extends BasePartitionable {
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("pid", myId)
.append("resourceType", myResourceType)
.append("forcedId", myForcedId)
.append("resourcePid", myResourcePid)
.toString();
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
b.append("pid", myId);
if (getPartitionId() != null) {
b.append("partitionId", getPartitionId().getPartitionId());
}
b.append("resourceType", myResourceType);
b.append("forcedId", myForcedId);
b.append("resourcePid", myResourcePid);
return b.toString();
}
}

View File

@ -637,6 +637,9 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
b.append("pid", myId);
b.append("resourceType", myResourceType);
if (getPartitionId() != null) {
b.append("partitionId", getPartitionId().getPartitionId());
}
b.append("lastUpdated", getUpdated().getValueAsString());
if (getDeleted() != null) {
b.append("deleted");

View File

@ -133,6 +133,9 @@ public class ResourceTag extends BaseTag {
@Override
public String toString() {
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
if (getPartitionId() != null) {
b.append("partition", getPartitionId().getPartitionId());
}
b.append("resId", getResourceId());
b.append("tag", getTag().getId());
return b.build();

View File

@ -115,9 +115,9 @@ public class SearchParameterMap implements Serializable {
return this;
}
public void add(String theName, IQueryParameterAnd<?> theAnd) {
public SearchParameterMap add(String theName, IQueryParameterAnd<?> theAnd) {
if (theAnd == null) {
return;
return this;
}
if (!containsKey(theName)) {
put(theName, new ArrayList<>());
@ -129,17 +129,19 @@ public class SearchParameterMap implements Serializable {
}
get(theName).add((List<IQueryParameterType>) next.getValuesAsQueryTokens());
}
return this;
}
public void add(String theName, IQueryParameterOr<?> theOr) {
public SearchParameterMap add(String theName, IQueryParameterOr<?> theOr) {
if (theOr == null) {
return;
return this;
}
if (!containsKey(theName)) {
put(theName, new ArrayList<>());
}
get(theName).add((List<IQueryParameterType>) theOr.getValuesAsQueryTokens());
return this;
}
public Collection<List<List<IQueryParameterType>>> values() {

View File

@ -41,10 +41,10 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.StringUtil;
@ -65,6 +65,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import javax.measure.quantity.Quantity;
import javax.measure.unit.NonSI;
@ -535,19 +536,24 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
protected abstract IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath);
protected FhirContext getContext() {
return myContext;
}
@VisibleForTesting
public void setContext(FhirContext theContext) {
myContext = theContext;
}
protected FhirContext getContext() {
return myContext;
}
protected ModelConfig getModelConfig() {
return myModelConfig;
}
@VisibleForTesting
public void setModelConfig(ModelConfig theModelConfig) {
myModelConfig = theModelConfig;
}
@VisibleForTesting
public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
mySearchParamRegistry = theSearchParamRegistry;
@ -574,7 +580,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
}
private void addQuantity_QuantityNormalized(String theResourceType, Set<ResourceIndexedSearchParamQuantityNormalized> theParams, RuntimeSearchParam theSearchParam, IBase theValue) {
Optional<IPrimitiveType<BigDecimal>> valueField = myQuantityValueValueChild.getAccessor().getFirstValueOrNull(theValue);
if (valueField.isPresent() && valueField.get().getValue() != null) {
@ -608,7 +613,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
}
private void addQuantity_MoneyNormalized(String theResourceType, Set<ResourceIndexedSearchParamQuantityNormalized> theParams, RuntimeSearchParam theSearchParam, IBase theValue) {
Optional<IPrimitiveType<BigDecimal>> valueField = myMoneyValueChild.getAccessor().getFirstValueOrNull(theValue);
if (valueField.isPresent() && valueField.get().getValue() != null) {
@ -623,7 +627,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
}
private void addQuantity_Range(String theResourceType, Set<ResourceIndexedSearchParamQuantity> theParams, RuntimeSearchParam theSearchParam, IBase theValue) {
Optional<IBase> low = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue);
low.ifPresent(theIBase -> addQuantity_Quantity(theResourceType, theParams, theSearchParam, theIBase));
@ -632,7 +635,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
high.ifPresent(theIBase -> addQuantity_Quantity(theResourceType, theParams, theSearchParam, theIBase));
}
private void addQuantity_RangeNormalized(String theResourceType, Set<ResourceIndexedSearchParamQuantityNormalized> theParams, RuntimeSearchParam theSearchParam, IBase theValue) {
Optional<IBase> low = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue);
low.ifPresent(theIBase -> addQuantity_QuantityNormalized(theResourceType, theParams, theSearchParam, theIBase));
@ -657,11 +659,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
}
@VisibleForTesting
public void setModelConfig(ModelConfig theModelConfig) {
myModelConfig = theModelConfig;
}
protected boolean shouldIndexTextComponentOfToken(RuntimeSearchParam theSearchParam) {
return tokenTextIndexingEnabledForSearchParam(myModelConfig, theSearchParam);
}
@ -974,7 +971,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
* (e.g. "#3") or by resource (e.g. "new Reference(patientInstance)"). The FHIRPath evaluator only understands the
* first way, so if there is any chance of the FHIRPath evaluator needing to descend across references, we
* have to assign values to those references before indexing.
*
* <p>
* Doing this cleanup isn't hugely expensive, but it's not completely free either so we only do it
* if we think there's actually a chance
*/
@ -1095,7 +1092,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
if (!thePaths.contains("|")) {
return new String[]{thePaths};
}
return SPLIT_R4.split(thePaths);
return splitPathsR4(thePaths);
} else {
if (!thePaths.contains("|") && !thePaths.contains(" or ")) {
return new String[]{thePaths};
@ -1236,23 +1233,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
private static class CompositeExtractor<T> implements IExtractor<T> {
private final IExtractor<T> myExtractor0;
private final IExtractor<T> myExtractor1;
private CompositeExtractor(IExtractor<T> theExtractor0, IExtractor<T> theExtractor1) {
myExtractor0 = theExtractor0;
myExtractor1 = theExtractor1;
}
@Override
public void extract(SearchParamSet<T> theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) {
myExtractor0.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences);
myExtractor1.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences);
}
}
private class ResourceLinkExtractor implements IExtractor<PathAndRef> {
private PathAndRef myPathAndRef = null;
@ -1308,9 +1288,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
if (nextId == null ||
nextId.isEmpty() ||
nextId.getValue().startsWith("urn:")) {
return;
nextId.isEmpty() ||
nextId.getValue().startsWith("urn:")) {
return;
}
if (!theWantLocalReferences) {
if (nextId.getValue().startsWith("#"))
@ -1422,7 +1402,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
dates.add(start);
}
if (end != null) {
dates.add(end);
dates.add(end);
}
}
}
@ -1544,6 +1524,28 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
}
}
private static class CompositeExtractor<T> implements IExtractor<T> {
private final IExtractor<T> myExtractor0;
private final IExtractor<T> myExtractor1;
private CompositeExtractor(IExtractor<T> theExtractor0, IExtractor<T> theExtractor1) {
myExtractor0 = theExtractor0;
myExtractor1 = theExtractor1;
}
@Override
public void extract(SearchParamSet<T> theParams, RuntimeSearchParam theSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) {
myExtractor0.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences);
myExtractor1.extract(theParams, theSearchParam, theValue, thePath, theWantLocalReferences);
}
}
@Nonnull
public static String[] splitPathsR4(@Nonnull String thePaths) {
return SPLIT_R4.split(thePaths);
}
public static boolean tokenTextIndexingEnabledForSearchParam(ModelConfig theModelConfig, RuntimeSearchParam theSearchParam) {
Optional<Boolean> noSuppressForSearchParam = theSearchParam.getExtensions(HapiExtensions.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING).stream()
.map(IBaseExtension::getValue)