diff --git a/DeleteConflictService_fix.patch b/DeleteConflictService_fix.patch new file mode 100644 index 00000000000..c88dbd45c52 --- /dev/null +++ b/DeleteConflictService_fix.patch @@ -0,0 +1,15 @@ +diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java +index e575041cd9..93e364bc93 100644 +--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java ++++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java +@@ -49,8 +49,8 @@ import java.util.List; + public class DeleteConflictService { + private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class); + public static final int FIRST_QUERY_RESULT_COUNT = 1; +- public static final int RETRY_QUERY_RESULT_COUNT = 60; +- public static final int MAX_RETRY_ATTEMPTS = 10; ++ public static final int RETRY_QUERY_RESULT_COUNT = 100; ++ public static final int MAX_RETRY_ATTEMPTS = 100; + + @Autowired + DeleteConflictFinderService myDeleteConflictFinderService; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoObservation.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoObservation.java index 5f9239d208c..391e34db536 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoObservation.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoObservation.java @@ -37,6 +37,7 @@ public abstract class BaseHapiFhirResourceDaoObservation EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>()); private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class); @@ -182,6 +184,18 @@ public class SearchBuilder implements ISearchBuilder { myResourceType = theResourceType; } + public static int getMaximumPageSize() { + if (myIsTest) { + return MAXIMUM_PAGE_SIZE_FOR_TESTING; + } else { + return MAXIMUM_PAGE_SIZE; + } + } + + public static void setIsTest(boolean theIsTest) { + myIsTest = theIsTest; + } + @Override public void setMaxResultsToFetch(Integer theMaxResultsToFetch) { myMaxResultsToFetch = theMaxResultsToFetch; @@ -210,6 +224,10 @@ public class SearchBuilder implements ISearchBuilder { // Handle each parameter for (Map.Entry>> nextParamEntry : myParams.entrySet()) { String nextParamName = nextParamEntry.getKey(); + if (myParams.isLastN() && LastNParameterHelper.isLastNParameter(nextParamName, myContext)) { + // Skip parameters for Subject, Patient, Code and Category for LastN + continue; + } List> andOrParams = nextParamEntry.getValue(); searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams, theRequest); } @@ -231,8 +249,8 @@ public class SearchBuilder implements ISearchBuilder { init(theParams, theSearchUuid, theRequestPartitionId); - TypedQuery query = createQuery(null, null, true, theRequest); - return new CountQueryIterator(query); + List> queries = createQuery(null, null, true, theRequest); + return new CountQueryIterator(queries.get(0)); } /** @@ -265,7 +283,72 @@ public class SearchBuilder implements ISearchBuilder { } - private TypedQuery createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount, RequestDetails theRequest) { + private List> createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount, RequestDetails theRequest) { + List pids = new ArrayList<>(); + + /* + * Fulltext or lastn search + */ + if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT) || myParams.isLastN()) { + if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT)) { + if (myFulltextSearchSvc == null) { + if (myParams.containsKey(Constants.PARAM_TEXT)) { + throw new InvalidRequestException("Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_TEXT); + } else if (myParams.containsKey(Constants.PARAM_CONTENT)) { + throw new InvalidRequestException("Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_CONTENT); + } + } + + if (myParams.getEverythingMode() != null) { + pids = myFulltextSearchSvc.everything(myResourceName, myParams, theRequest); + } else { + pids = myFulltextSearchSvc.search(myResourceName, myParams); + } + } else if (myParams.isLastN()) { + if (myIElasticsearchSvc == null) { + if (myParams.isLastN()) { + throw new InvalidRequestException("LastN operation is not enabled on this service, can not process this request"); + } + } + Integer myMaxObservationsPerCode = null; + if(myParams.getLastNMax() != null) { + myMaxObservationsPerCode = myParams.getLastNMax(); + } else { + throw new InvalidRequestException("Max parameter is required for $lastn operation"); + } + List lastnResourceIds = myIElasticsearchSvc.executeLastN(myParams, myMaxObservationsPerCode); + for (String lastnResourceId : lastnResourceIds) { + pids.add(myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, lastnResourceId)); + } + } + if (pids.isEmpty()) { + // Will never match + pids = Collections.singletonList(new ResourcePersistentId(-1L)); + } + + } + + ArrayList> myQueries = new ArrayList<>(); + + if (!pids.isEmpty()) { + new QueryChunker().chunk(ResourcePersistentId.toLongList(pids), t->{ + doCreateChunkedQueries(t, sort, theMaximumResults, theCount, theRequest, myQueries); + }); + } else { + myQueries.add(createQuery(sort,theMaximumResults, theCount, theRequest, null)); + } + + return myQueries; + } + + private void doCreateChunkedQueries(List thePids, SortSpec sort, Integer theMaximumResults, boolean theCount, RequestDetails theRequest, ArrayList> theQueries) { + if(thePids.size() < MAXIMUM_PAGE_SIZE) { + thePids = normalizeIdListForLastNInClause(thePids); + } + theQueries.add(createQuery(sort, theMaximumResults, theCount, theRequest, thePids)); + } + + private TypedQuery createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount, RequestDetails theRequest, List thePidList) { CriteriaQuery outerQuery; /* * Sort @@ -329,7 +412,7 @@ public class SearchBuilder implements ISearchBuilder { /* * Fulltext or lastn search */ - if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT) || myParams.isLastN()) { +/* if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT) || myParams.isLastN()) { List pids = new ArrayList<>(); if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT)) { if (myFulltextSearchSvc == null) { @@ -352,19 +435,16 @@ public class SearchBuilder implements ISearchBuilder { } } Integer myMaxObservationsPerCode = null; -// String[] maxCountParams = theRequest.getParameters().get("max"); -// if (maxCountParams != null && maxCountParams.length > 0) { -// myMaxObservationsPerCode = Integer.valueOf(maxCountParams[0]); if(myParams.getLastNMax() != null) { myMaxObservationsPerCode = myParams.getLastNMax(); } else { throw new InvalidRequestException("Max parameter is required for $lastn operation"); } List lastnResourceIds = myIElasticsearchSvc.executeLastN(myParams, myMaxObservationsPerCode); -// for (String lastnResourceId : lastnResourceIds) { -// pids.add(myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, lastnResourceId)); -// } - pids = normalizeIdListForLastNInClause(lastnResourceIds); + for (String lastnResourceId : lastnResourceIds) { + pids.add(myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, lastnResourceId)); + } +// pids = normalizeIdListForLastNInClause(lastnResourceIds); } if (pids.isEmpty()) { // Will never match @@ -374,6 +454,11 @@ public class SearchBuilder implements ISearchBuilder { myQueryRoot.addPredicate(myQueryRoot.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(pids))); } +*/ + // Add PID list predicate for full text search and/or lastn operation + if (thePidList != null && thePidList.size() > 0) { + myQueryRoot.addPredicate(myQueryRoot.get("myId").as(Long.class).in(thePidList)); + } /* * Add a predicate to make sure we only include non-deleted resources, and only include @@ -415,10 +500,10 @@ public class SearchBuilder implements ISearchBuilder { return query; } - private List normalizeIdListForLastNInClause(List lastnResourceIds) { - List retVal = new ArrayList<>(); - for (String lastnResourceId : lastnResourceIds) { - retVal.add(new ResourcePersistentId(Long.parseLong(lastnResourceId))); + private List normalizeIdListForLastNInClause(List lastnResourceIds) { + List retVal = new ArrayList<>(); + for (Long lastnResourceId : lastnResourceIds) { + retVal.add(lastnResourceId); } /* @@ -430,32 +515,27 @@ public class SearchBuilder implements ISearchBuilder { arguments never exceeds the maximum specified below. */ int listSize = retVal.size(); + if(listSize > 1 && listSize < 10) { padIdListWithPlaceholders(retVal, 10); - } else if (listSize > 10 && listSize < 100) { + } else if (listSize > 10 && listSize < 50) { + padIdListWithPlaceholders(retVal, 50); + } else if (listSize > 50 && listSize < 100) { padIdListWithPlaceholders(retVal, 100); } else if (listSize > 100 && listSize < 200) { padIdListWithPlaceholders(retVal, 200); } else if (listSize > 200 && listSize < 500) { padIdListWithPlaceholders(retVal, 500); - } else if (listSize > 500 && listSize < 1000) { - padIdListWithPlaceholders(retVal, 1000); - } else if (listSize > 1000 && listSize < 500) { - padIdListWithPlaceholders(retVal, 5000); - } else if (listSize > 5000 && listSize < 10000) { - padIdListWithPlaceholders(retVal, 10000); - } else if (listSize > 10000 && listSize < 20000) { - padIdListWithPlaceholders(retVal, 20000); - } else if (listSize > 20000 && listSize < 30000) { - padIdListWithPlaceholders(retVal, 30000); + } else if (listSize > 500 && listSize < 800) { + padIdListWithPlaceholders(retVal, 800); } return retVal; } - private void padIdListWithPlaceholders(List theIdList, int preferredListSize) { + private void padIdListWithPlaceholders(List theIdList, int preferredListSize) { while(theIdList.size() < preferredListSize) { - theIdList.add(new ResourcePersistentId(-1L)); + theIdList.add(-1L); } } @@ -733,7 +813,7 @@ public class SearchBuilder implements ISearchBuilder { if (matchAll) { String sql; sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) "; - List> partitions = partition(nextRoundMatches, MAXIMUM_PAGE_SIZE); + List> partitions = partition(nextRoundMatches, getMaximumPageSize()); for (Collection nextPartition : partitions) { TypedQuery q = theEntityManager.createQuery(sql, Long.class); q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition)); @@ -786,7 +866,7 @@ public class SearchBuilder implements ISearchBuilder { sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)"; } - List> partitions = partition(nextRoundMatches, MAXIMUM_PAGE_SIZE); + List> partitions = partition(nextRoundMatches, getMaximumPageSize()); for (Collection nextPartition : partitions) { TypedQuery q = theEntityManager.createQuery(sql, Long.class); q.setParameter("src_path", nextPath); @@ -1076,6 +1156,8 @@ public class SearchBuilder implements ISearchBuilder { private int mySkipCount = 0; private int myNonSkipCount = 0; + private List> myQueryList; + private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) { mySearchRuntimeDetails = theSearchRuntimeDetails; mySort = myParams.getSort(); @@ -1126,7 +1208,12 @@ public class SearchBuilder implements ISearchBuilder { } if (myNext == null) { - while (myResultsIterator.hasNext()) { + while (myResultsIterator.hasNext() || !myQueryList.isEmpty()) { + // Update iterator with next chunk if necessary. + if (!myResultsIterator.hasNext() && !myQueryList.isEmpty()) { + retrieveNextIteratorQuery(); + } + Long nextLong = myResultsIterator.next(); if (myHavePerfTraceFoundIdHook) { HookParams params = new HookParams() @@ -1225,19 +1312,31 @@ public class SearchBuilder implements ISearchBuilder { } private void initializeIteratorQuery(Integer theMaxResultsToFetch) { - final TypedQuery query = createQuery(mySort, theMaxResultsToFetch, false, myRequest); + if (myQueryList == null || myQueryList.isEmpty()) { + myQueryList = createQuery(mySort, theMaxResultsToFetch, false, myRequest); + } mySearchRuntimeDetails.setQueryStopwatch(new StopWatch()); - Query hibernateQuery = (Query) query; - hibernateQuery.setFetchSize(myFetchSize); - ScrollableResults scroll = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); - myResultsIterator = new ScrollableResultsIterator<>(scroll); + retrieveNextIteratorQuery(); mySkipCount = 0; myNonSkipCount = 0; } + private void retrieveNextIteratorQuery() { + if (myQueryList != null && myQueryList.size() > 0) { + final TypedQuery query = myQueryList.remove(0); + Query hibernateQuery = (Query) (query); + hibernateQuery.setFetchSize(myFetchSize); + ScrollableResults scroll = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); + myResultsIterator = new ScrollableResultsIterator<>(scroll); + } else { + myResultsIterator = null; + } + + } + @Override public boolean hasNext() { if (myNext == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/lastn/entity/ObservationIndexedSearchParamLastNEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/lastn/entity/ObservationIndexedSearchParamLastNEntity.java index f3e26cdb847..5fddc842c30 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/lastn/entity/ObservationIndexedSearchParamLastNEntity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/lastn/entity/ObservationIndexedSearchParamLastNEntity.java @@ -13,79 +13,80 @@ import java.util.*; @Indexed(index = "observation_index") public class ObservationIndexedSearchParamLastNEntity { - @Id - @SequenceGenerator(name = "SEQ_LASTN", sequenceName = "SEQ_LASTN") - @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_LASTN") - @Column(name = "LASTN_ID") - private Long myId; + @Id + @SequenceGenerator(name = "SEQ_LASTN", sequenceName = "SEQ_LASTN") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_LASTN") + @Column(name = "LASTN_ID") + private Long myId; @Field(name = "subject", analyze = Analyze.NO) - @Column(name = "LASTN_SUBJECT_ID", nullable = true) - private String mySubject; + @Column(name = "LASTN_SUBJECT_ID", nullable = true) + private String mySubject; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "CODEABLE_CONCEPT_ID", nullable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_OBSERVATION_CODE_FK")) - @IndexedEmbedded(depth = 2, prefix = "codeconcept") - private ObservationIndexedCodeCodeableConceptEntity myObservationCode; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "CODEABLE_CONCEPT_ID", nullable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_OBSERVATION_CODE_FK")) + @IndexedEmbedded(depth = 2, prefix = "codeconcept") + private ObservationIndexedCodeCodeableConceptEntity myObservationCode; - @Field(name = "codeconceptid", analyze = Analyze.NO) - @Column(name = "CODEABLE_CONCEPT_ID", nullable = false, updatable = false, insertable = false) - private String myCodeNormalizedId; + @Field(name = "codeconceptid", analyze = Analyze.NO) + @Column(name = "CODEABLE_CONCEPT_ID", nullable = false, updatable = false, insertable = false) + private String myCodeNormalizedId; - @IndexedEmbedded(depth = 2, prefix = "categoryconcept") - @Transient - private Set myCategoryCodeableConcepts; + @IndexedEmbedded(depth = 2, prefix = "categoryconcept") + @Transient + private Set myCategoryCodeableConcepts; - @Field(name = "effectivedtm", analyze = Analyze.NO) - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "LASTN_EFFECTIVE_DATETIME", nullable = true) - private Date myEffectiveDtm; + @Field(name = "effectivedtm", analyze = Analyze.NO) + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "LASTN_EFFECTIVE_DATETIME", nullable = true) + private Date myEffectiveDtm; - @DocumentId(name = "identifier") - @Column(name = "RESOURCE_IDENTIFIER", nullable = false) - private String myIdentifier; + @DocumentId(name = "identifier") + @Column(name = "RESOURCE_IDENTIFIER", nullable = false) + private String myIdentifier; - public ObservationIndexedSearchParamLastNEntity() {} + public ObservationIndexedSearchParamLastNEntity() { + } - public String getSubject() { - return mySubject; - } + public String getSubject() { + return mySubject; + } - public void setSubject(String theSubject) { - mySubject = theSubject; - } + public void setSubject(String theSubject) { + mySubject = theSubject; + } - public String getIdentifier() { - return myIdentifier; - } + public String getIdentifier() { + return myIdentifier; + } - public void setIdentifier(String theIdentifier) { - myIdentifier = theIdentifier; - } + public void setIdentifier(String theIdentifier) { + myIdentifier = theIdentifier; + } - public void setEffectiveDtm(Date theEffectiveDtm) { - myEffectiveDtm = theEffectiveDtm; - } + public void setEffectiveDtm(Date theEffectiveDtm) { + myEffectiveDtm = theEffectiveDtm; + } - public Date getEffectiveDtm() { - return myEffectiveDtm; - } + public Date getEffectiveDtm() { + return myEffectiveDtm; + } - public void setCodeNormalizedId(String theCodeNormalizedId) { - myCodeNormalizedId = theCodeNormalizedId; - } + public void setCodeNormalizedId(String theCodeNormalizedId) { + myCodeNormalizedId = theCodeNormalizedId; + } - public String getCodeNormalizedId() { - return myCodeNormalizedId; - } + public String getCodeNormalizedId() { + return myCodeNormalizedId; + } - public void setObservationCode(ObservationIndexedCodeCodeableConceptEntity theObservationCode) { - myObservationCode = theObservationCode; - } + public void setObservationCode(ObservationIndexedCodeCodeableConceptEntity theObservationCode) { + myObservationCode = theObservationCode; + } - public void setCategoryCodeableConcepts(Set theCategoryCodeableConcepts) { - myCategoryCodeableConcepts = theCategoryCodeableConcepts; - } + public void setCategoryCodeableConcepts(Set theCategoryCodeableConcepts) { + myCategoryCodeableConcepts = theCategoryCodeableConcepts; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/ElasticsearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/ElasticsearchSvcImpl.java index 9a730b6bb69..47c42cd6b6e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/ElasticsearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/ElasticsearchSvcImpl.java @@ -200,6 +200,7 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc { } @Override + // TODO: Should eliminate dependency on SearchParameterMap in API. public List executeLastN(SearchParameterMap theSearchParameterMap, Integer theMaxObservationsPerCode) { String[] topHitsInclude = {OBSERVATION_IDENTIFIER_FIELD_NAME}; try { @@ -252,6 +253,7 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc { } @VisibleForTesting + // TODO: Should eliminate dependency on SearchParameterMap in API. List executeLastNWithAllFields(SearchParameterMap theSearchParameterMap, Integer theMaxObservationsPerCode) { try { List responses = buildAndExecuteSearch(theSearchParameterMap, theMaxObservationsPerCode, null); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/IndexConstants.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/IndexConstants.java index 9a48d3fc3b5..535d299fd9f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/IndexConstants.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/lastn/IndexConstants.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.search.lastn; public class IndexConstants { + // TODO: These should all be moved into ElasticSearchSvcImpl. public static final String OBSERVATION_INDEX = "observation_index"; public static final String CODE_INDEX = "code_index"; public static final String OBSERVATION_DOCUMENT_TYPE = "ca.uhn.fhir.jpa.dao.lastn.entity.ObservationIndexedSearchParamLastNEntity"; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryChunker.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryChunker.java index c549c9fd481..31b172f5d8a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryChunker.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryChunker.java @@ -34,8 +34,8 @@ import java.util.function.Consumer; public class QueryChunker { public void chunk(List theInput, Consumer> theBatchConsumer) { - for (int i = 0; i < theInput.size(); i += SearchBuilder.MAXIMUM_PAGE_SIZE) { - int to = i + SearchBuilder.MAXIMUM_PAGE_SIZE; + for (int i = 0; i < theInput.size(); i += SearchBuilder.getMaximumPageSize()) { + int to = i + SearchBuilder.getMaximumPageSize(); to = Math.min(to, theInput.size()); List batch = theInput.subList(i, to); theBatchConsumer.accept(batch); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchLastNIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchLastNIT.java index e5b40a53e56..20ab38370c9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchLastNIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchLastNIT.java @@ -5,13 +5,16 @@ import ca.uhn.fhir.jpa.api.dao.*; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.config.TestR4ConfigWithElasticsearchClient; import ca.uhn.fhir.jpa.dao.BaseJpaTest; +import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.rp.r4.ObservationResourceProvider; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; +import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; @@ -23,7 +26,9 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.PlatformTransactionManager; import java.util.*; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.matchesPattern; import static org.junit.Assert.*; import static org.mockito.Mockito.when; @@ -58,6 +63,9 @@ public class FhirResourceDaoR4SearchLastNIT extends BaseJpaTest { return myPlatformTransactionManager; } + @Autowired + protected CircularQueueCaptureQueriesListener myCaptureQueriesListener; + ObservationResourceProvider observationRp = new ObservationResourceProvider(); private final String observationCd0 = "code0"; @@ -108,6 +116,11 @@ public class FhirResourceDaoR4SearchLastNIT extends BaseJpaTest { } + @After + public void resetMaximumPageSize() { + SearchBuilder.setIsTest(false); + } + private void createObservationsForPatient(IIdType thePatientId) { createFiveObservationsForPatientCodeCategory(thePatientId,observationCd0, categoryCd0, 15); createFiveObservationsForPatientCodeCategory(thePatientId,observationCd0, categoryCd1, 10); @@ -192,9 +205,6 @@ public class FhirResourceDaoR4SearchLastNIT extends BaseJpaTest { params.setLastN(true); Map requestParameters = new HashMap<>(); -// String[] maxParam = new String[1]; -// maxParam[0] = "100"; -// requestParameters.put("max", maxParam); params.setLastNMax(100); when(mySrd.getParameters()).thenReturn(requestParameters); @@ -520,6 +530,82 @@ public class FhirResourceDaoR4SearchLastNIT extends BaseJpaTest { return new TokenAndListParam().addAnd(myTokenOrListParam); } + @Test + public void testLastNWithChunkedQuery() { + SearchBuilder.setIsTest(true); + Integer numberOfObservations = SearchBuilder.getMaximumPageSize()+1; + Calendar observationDate = new GregorianCalendar(); + + List myObservationIds = new ArrayList<>(); + List myPatientIds = new ArrayList<>(); + List myPatientReferences = new ArrayList<>(); + for (int idx=0; idx actual; + params.setLastN(true); + + Map requestParameters = new HashMap<>(); + params.setLastNMax(1); + + params.setCount(numberOfObservations); + + when(mySrd.getParameters()).thenReturn(requestParameters); + + myCaptureQueriesListener.clear(); + actual = toUnqualifiedVersionlessIdValues(myObservationDao.observationsLastN(params, mockSrd(),null)); + + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + List queries = myCaptureQueriesListener + .getSelectQueriesForCurrentThread() + .stream() + .map(t -> t.getSql(true, false)) + .collect(Collectors.toList()); + + // First chunked query + String resultingQueryNotFormatted = queries.get(0); + assertThat(resultingQueryNotFormatted, matchesPattern(".*RES_ID in \\('[0-9]+' , '[0-9]+' , '[0-9]+' , '[0-9]+'\\).*")); + + // Second chunked query chunk + resultingQueryNotFormatted = queries.get(1); + assertThat(resultingQueryNotFormatted, matchesPattern(".*RES_ID in \\('[0-9]+' , '-1' , '-1' , '-1'\\).*")); + + assertEquals(numberOfObservations, (Integer)actual.size()); + for(IIdType observationId : myObservationIds) { + myObservationDao.delete(observationId); + } + + for (IIdType patientId : myPatientIds) { + myPatientDao.delete(patientId); + } + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/util/LastNParameterHelper.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/util/LastNParameterHelper.java new file mode 100644 index 00000000000..c999194e926 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/util/LastNParameterHelper.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.jpa.searchparam.util; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class LastNParameterHelper { + + public static boolean isLastNParameter(String theParamName, FhirContext theContext) { + if (theParamName == null) { + return false; + } + if (theContext.getVersion().getVersion() == FhirVersionEnum.R5) { + if (theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_SUBJECT) || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_PATIENT) + || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CODE)) { + return true; + } else { + return false; + } + } else if (theContext.getVersion().getVersion() == FhirVersionEnum.R4) { + if (theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_SUBJECT) || theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_PATIENT) + || theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_CODE)) { + return true; + } else { + return false; + } + } else if (theContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) { + if (theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_SUBJECT) || theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_PATIENT) + || theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_CODE)) { + return true; + } else { + return false; + } + } else { + throw new InvalidRequestException("$lastn operation is not implemented for FHIR Version " + theContext.getVersion().getVersion().getFhirVersionString()); + } + } +}