diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_0_1/1842-reduce-joins.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_0_1/1842-reduce-joins.yaml new file mode 100644 index 00000000000..2373566bd23 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_0_1/1842-reduce-joins.yaml @@ -0,0 +1,7 @@ +--- +type: fix +issue: 1842 +title: "When performing queries with multiple chained search parameters, such as 'Observation?subject.identifier=FOO&specimen.identifier=BAR', + an unnecessary SQL join was introduced into the resulting query. This was inefficient, and made it particularly hard for the RDBMS optimizer + to pick an efficient query plan in some cases. This is not fixing a regression (this issue has always existed in HAPI FHIR JPA) but it + was deemed sufficiently important to merit a dedicated point release." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_0_1/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_0_1/version.yaml new file mode 100644 index 00000000000..3a2c9d5339e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_0_1/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2020-05-15" +codename: "Labrador" diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index 8e26ceb3b42..25d4c0e5e4f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -35,13 +35,12 @@ import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilder; import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderFactory; -import ca.uhn.fhir.jpa.dao.predicate.QueryRoot; +import ca.uhn.fhir.jpa.dao.predicate.querystack.QueryStack; import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinEnum; import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinKey; import ca.uhn.fhir.jpa.entity.ResourceSearchView; import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails; import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import ca.uhn.fhir.jpa.model.entity.ResourceLink; @@ -71,6 +70,7 @@ import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; @@ -99,7 +99,6 @@ import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.From; import javax.persistence.criteria.Join; -import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Order; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; @@ -111,6 +110,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import static org.apache.commons.lang3.StringUtils.defaultString; @@ -133,7 +133,6 @@ public class SearchBuilder implements ISearchBuilder { private static final List EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>()); private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class); private static final ResourcePersistentId NO_MORE = new ResourcePersistentId(-1L); - private final QueryRoot myQueryRoot = new QueryRoot(); private final String myResourceName; private final Class myResourceType; private final IDao myCallingDao; @@ -143,6 +142,7 @@ public class SearchBuilder implements ISearchBuilder { protected IResourceTagDao myResourceTagDao; @PersistenceContext(type = PersistenceContextType.TRANSACTION) protected EntityManager myEntityManager; + private QueryStack myQueryStack; @Autowired private DaoConfig myDaoConfig; @Autowired @@ -253,8 +253,9 @@ public class SearchBuilder implements ISearchBuilder { } private void init(SearchParameterMap theParams, String theSearchUuid, RequestPartitionId theRequestPartitionId) { - myParams = theParams; myCriteriaBuilder = myEntityManager.getCriteriaBuilder(); + myQueryStack = new QueryStack(myCriteriaBuilder, myResourceName, theParams, theRequestPartitionId); + myParams = theParams; mySearchUuid = theSearchUuid; myPredicateBuilder = new PredicateBuilder(this, myPredicateBuilderFactory); myRequestPartitionId = theRequestPartitionId; @@ -262,7 +263,6 @@ public class SearchBuilder implements ISearchBuilder { private TypedQuery createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount, RequestDetails theRequest) { - CriteriaQuery outerQuery; /* * Sort * @@ -272,36 +272,24 @@ public class SearchBuilder implements ISearchBuilder { if (sort != null) { assert !theCount; - outerQuery = myCriteriaBuilder.createQuery(Long.class); - myQueryRoot.push(outerQuery); - if (theCount) { - outerQuery.multiselect(myCriteriaBuilder.countDistinct(myQueryRoot.getRoot())); - } else { - outerQuery.multiselect(myQueryRoot.get("myId").as(Long.class)); - } + myQueryStack.pushResourceTableQuery(); - List orders = Lists.newArrayList(); - - createSort(myCriteriaBuilder, myQueryRoot, sort, orders); + List orders = createSort(myCriteriaBuilder, myQueryStack, sort); if (orders.size() > 0) { - outerQuery.orderBy(orders); + myQueryStack.orderBy(orders); } } else { - outerQuery = myCriteriaBuilder.createQuery(Long.class); - myQueryRoot.push(outerQuery); if (theCount) { - outerQuery.multiselect(myCriteriaBuilder.countDistinct(myQueryRoot.getRoot())); + myQueryStack.pushResourceTableCountQuery(); } else { - outerQuery.multiselect(myQueryRoot.get("myId").as(Long.class)); - // KHS This distinct call is causing performance issues in large installations -// outerQuery.distinct(true); + myQueryStack.pushResourceTableQuery(); } } if (myParams.getEverythingMode() != null) { - Join join = myQueryRoot.join("myResourceLinks", JoinType.LEFT); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.REFERENCE, null); if (myParams.get(IAnyResource.SP_RES_ID) != null) { StringParam idParam = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0); @@ -310,11 +298,11 @@ public class SearchBuilder implements ISearchBuilder { myAlsoIncludePids = new ArrayList<>(1); } myAlsoIncludePids.add(pid); - myQueryRoot.addPredicate(myCriteriaBuilder.equal(join.get("myTargetResourcePid").as(Long.class), pid.getIdAsLong())); + myQueryStack.addPredicate(myCriteriaBuilder.equal(join.get("myTargetResourcePid").as(Long.class), pid.getIdAsLong())); } else { Predicate targetTypePredicate = myCriteriaBuilder.equal(join.get("myTargetResourceType").as(String.class), myResourceName); - Predicate sourceTypePredicate = myCriteriaBuilder.equal(myQueryRoot.get("myResourceType").as(String.class), myResourceName); - myQueryRoot.addPredicate(myCriteriaBuilder.or(sourceTypePredicate, targetTypePredicate)); + Predicate sourceTypePredicate = myCriteriaBuilder.equal(myQueryStack.get("myResourceType").as(String.class), myResourceName); + myQueryStack.addPredicate(myCriteriaBuilder.or(sourceTypePredicate, targetTypePredicate)); } } else { @@ -345,41 +333,20 @@ public class SearchBuilder implements ISearchBuilder { pids = Collections.singletonList(new ResourcePersistentId(-1L)); } - myQueryRoot.addPredicate(myQueryRoot.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(pids))); - } - - /* - * Add a predicate to make sure we only include non-deleted resources, and only include - * resources of the right type. - * - * If we have any joins to index tables, we get this behaviour already guaranteed so we don't - * need an explicit predicate for it. - */ - if (!myQueryRoot.hasIndexJoins()) { - if (myParams.getEverythingMode() == null) { - myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myResourceType"), myResourceName)); - } - myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted"))); - if (!myRequestPartitionId.isAllPartitions()) { - if (myRequestPartitionId.getPartitionId() != null) { - myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myPartitionIdValue").as(Integer.class), myRequestPartitionId.getPartitionId())); - } else { - myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myPartitionIdValue").as(Integer.class))); - } - } + myQueryStack.addPredicate(myQueryStack.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(pids))); } // Last updated DateRangeParam lu = myParams.getLastUpdated(); - List lastUpdatedPredicates = createLastUpdatedPredicates(lu, myCriteriaBuilder, myQueryRoot.getRoot()); - myQueryRoot.addPredicates(lastUpdatedPredicates); - - myQueryRoot.where(myCriteriaBuilder.and(myQueryRoot.getPredicateArray())); + List lastUpdatedPredicates = createLastUpdatedPredicates(lu, myCriteriaBuilder); + myQueryStack.addPredicates(lastUpdatedPredicates); /* * Now perform the search */ + CriteriaQuery outerQuery = (CriteriaQuery) myQueryStack.pop(); final TypedQuery query = myEntityManager.createQuery(outerQuery); + assert myQueryStack.isEmpty(); if (theMaximumResults != null) { query.setMaxResults(theMaximumResults); @@ -392,32 +359,35 @@ public class SearchBuilder implements ISearchBuilder { * @return Returns {@literal true} if any search parameter sorts were found, or false if * no sorts were found, or only non-search parameters ones (e.g. _id, _lastUpdated) */ - private boolean createSort(CriteriaBuilder theBuilder, QueryRoot theQueryRoot, SortSpec theSort, List theOrders) { + private List createSort(CriteriaBuilder theBuilder, QueryStack theQueryStack, SortSpec theSort) { if (theSort == null || isBlank(theSort.getParamName())) { - return false; + return Collections.emptyList(); } + List orders = new ArrayList<>(1); if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) { - From forcedIdJoin = theQueryRoot.join("myForcedId", JoinType.LEFT); + From forcedIdJoin = theQueryStack.createJoin(SearchBuilderJoinEnum.FORCED_ID, null); if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { - theOrders.add(theBuilder.asc(forcedIdJoin.get("myForcedId"))); - theOrders.add(theBuilder.asc(theQueryRoot.get("myId"))); + orders.add(theBuilder.asc(forcedIdJoin.get("myForcedId"))); + orders.add(theBuilder.asc(theQueryStack.get("myId"))); } else { - theOrders.add(theBuilder.desc(forcedIdJoin.get("myForcedId"))); - theOrders.add(theBuilder.desc(theQueryRoot.get("myId"))); + orders.add(theBuilder.desc(forcedIdJoin.get("myForcedId"))); + orders.add(theBuilder.desc(theQueryStack.get("myId"))); } - return createSort(theBuilder, theQueryRoot, theSort.getChain(), theOrders); + orders.addAll(createSort(theBuilder, theQueryStack, theSort.getChain())); + return orders; } if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) { if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { - theOrders.add(theBuilder.asc(theQueryRoot.get("myUpdated"))); + orders.add(theBuilder.asc(theQueryStack.get("myUpdated"))); } else { - theOrders.add(theBuilder.desc(theQueryRoot.get("myUpdated"))); + orders.add(theBuilder.desc(theQueryStack.get("myUpdated"))); } - return createSort(theBuilder, theQueryRoot, theSort.getChain(), theOrders); + orders.addAll(createSort(theBuilder, theQueryStack, theSort.getChain())); + return orders; } RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName); @@ -426,43 +396,35 @@ public class SearchBuilder implements ISearchBuilder { throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'"); } - String joinAttrName; String[] sortAttrName; SearchBuilderJoinEnum joinType; switch (param.getParamType()) { case STRING: - joinAttrName = "myParamsString"; sortAttrName = new String[]{"myValueExact"}; joinType = SearchBuilderJoinEnum.STRING; break; case DATE: - joinAttrName = "myParamsDate"; sortAttrName = new String[]{"myValueLow"}; joinType = SearchBuilderJoinEnum.DATE; break; case REFERENCE: - joinAttrName = "myResourceLinks"; sortAttrName = new String[]{"myTargetResourcePid"}; joinType = SearchBuilderJoinEnum.REFERENCE; break; case TOKEN: - joinAttrName = "myParamsToken"; sortAttrName = new String[]{"mySystem", "myValue"}; joinType = SearchBuilderJoinEnum.TOKEN; break; case NUMBER: - joinAttrName = "myParamsNumber"; sortAttrName = new String[]{"myValue"}; joinType = SearchBuilderJoinEnum.NUMBER; break; case URI: - joinAttrName = "myParamsUri"; sortAttrName = new String[]{"myUri"}; joinType = SearchBuilderJoinEnum.URI; break; case QUANTITY: - joinAttrName = "myParamsQuantity"; sortAttrName = new String[]{"myValue"}; joinType = SearchBuilderJoinEnum.QUANTITY; break; @@ -478,37 +440,40 @@ public class SearchBuilder implements ISearchBuilder { * sorting on, we'll also sort with it. Otherwise we need a new join. */ SearchBuilderJoinKey key = new SearchBuilderJoinKey(theSort.getParamName(), joinType); - Join join = theQueryRoot.getIndexJoin(key); - if (join == null) { - join = theQueryRoot.join(joinAttrName, JoinType.LEFT); + Optional> joinOpt = theQueryStack.getExistingJoin(key); + + From join; + if (!joinOpt.isPresent()) { + join = theQueryStack.createJoin(joinType, theSort.getParamName()); if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { - theQueryRoot.addPredicate(join.get("mySourcePath").as(String.class).in(param.getPathsSplit())); + theQueryStack.addPredicate(join.get("mySourcePath").as(String.class).in(param.getPathsSplit())); } else { if (myDaoConfig.getDisableHashBasedSearches()) { Predicate joinParam1 = theBuilder.equal(join.get("myParamName"), theSort.getParamName()); - theQueryRoot.addPredicate(joinParam1); + theQueryStack.addPredicate(joinParam1); } else { Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionSettings, myRequestPartitionId, myResourceName, theSort.getParamName()); Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity); - theQueryRoot.addPredicate(joinParam1); + theQueryStack.addPredicate(joinParam1); } } } else { ourLog.debug("Reusing join for {}", theSort.getParamName()); + join = joinOpt.get(); } for (String next : sortAttrName) { if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { - theOrders.add(theBuilder.asc(join.get(next))); + orders.add(theBuilder.asc(join.get(next))); } else { - theOrders.add(theBuilder.desc(join.get(next))); + orders.add(theBuilder.desc(join.get(next))); } } - createSort(theBuilder, theQueryRoot, theSort.getChain(), theOrders); + orders.addAll(createSort(theBuilder, theQueryStack, theSort.getChain())); - return true; + return orders; } @@ -616,7 +581,7 @@ public class SearchBuilder implements ISearchBuilder { } List pids = new ArrayList<>(thePids); - new QueryChunker().chunk(pids, t->{ + new QueryChunker().chunk(pids, t -> { doLoadPids(t, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position, theDetails); }); @@ -885,17 +850,16 @@ public class SearchBuilder implements ISearchBuilder { } private void addPredicateCompositeStringUnique(@Nonnull SearchParameterMap theParams, String theIndexedString, RequestPartitionId theRequestPartitionId) { - Join join = myQueryRoot.join("myParamsCompositeStringUnique", JoinType.LEFT); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.COMPOSITE_UNIQUE, null); if (!theRequestPartitionId.isAllPartitions()) { Integer partitionId = theRequestPartitionId.getPartitionId(); Predicate predicate = myCriteriaBuilder.equal(join.get("myPartitionIdValue").as(Integer.class), partitionId); - myQueryRoot.addPredicate(predicate); + myQueryStack.addPredicate(predicate); } - myQueryRoot.setHasIndexJoins(); Predicate predicate = myCriteriaBuilder.equal(join.get("myIndexString"), theIndexedString); - myQueryRoot.addPredicate(predicate); + myQueryStack.addPredicateWithImplicitTypeSelection(predicate); // Remove any empty parameters remaining after this theParams.clean(); @@ -924,8 +888,8 @@ public class SearchBuilder implements ISearchBuilder { return myCriteriaBuilder; } - public QueryRoot getQueryRoot() { - return myQueryRoot; + public QueryStack getQueryStack() { + return myQueryStack; } public Class getResourceType() { @@ -941,6 +905,22 @@ public class SearchBuilder implements ISearchBuilder { myDaoConfig = theDaoConfig; } + private List createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder) { + List lastUpdatedPredicates = new ArrayList<>(); + if (theLastUpdated != null) { + if (theLastUpdated.getLowerBoundAsInstant() != null) { + ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant())); + Predicate predicateLower = builder.greaterThanOrEqualTo(myQueryStack.getLastUpdatedColumn(), theLastUpdated.getLowerBoundAsInstant()); + lastUpdatedPredicates.add(predicateLower); + } + if (theLastUpdated.getUpperBoundAsInstant() != null) { + Predicate predicateUpper = builder.lessThanOrEqualTo(myQueryStack.getLastUpdatedColumn(), theLastUpdated.getUpperBoundAsInstant()); + lastUpdatedPredicates.add(predicateUpper); + } + } + return lastUpdatedPredicates; + } + public class IncludesIterator extends BaseIterator implements Iterator { private final RequestDetails myRequest; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java index 35530f06c85..51a187155f8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java @@ -24,11 +24,10 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.predicate.querystack.QueryStack; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.BasePartitionable; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -43,8 +42,6 @@ import javax.annotation.PostConstruct; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Expression; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; -import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Predicate; import java.math.BigDecimal; import java.math.MathContext; @@ -54,7 +51,7 @@ import java.util.List; abstract class BasePredicateBuilder { private static final Logger ourLog = LoggerFactory.getLogger(BasePredicateBuilder.class); final CriteriaBuilder myCriteriaBuilder; - final QueryRoot myQueryRoot; + final QueryStack myQueryStack; final Class myResourceType; final String myResourceName; final SearchParameterMap myParams; @@ -68,7 +65,7 @@ abstract class BasePredicateBuilder { BasePredicateBuilder(SearchBuilder theSearchBuilder) { myCriteriaBuilder = theSearchBuilder.getBuilder(); - myQueryRoot = theSearchBuilder.getQueryRoot(); + myQueryStack = theSearchBuilder.getQueryStack(); myResourceType = theSearchBuilder.getResourceType(); myResourceName = theSearchBuilder.getResourceName(); myParams = theSearchBuilder.getParams(); @@ -79,44 +76,8 @@ abstract class BasePredicateBuilder { myDontUseHashesForSearch = myDaoConfig.getDisableHashBasedSearches(); } - @SuppressWarnings("unchecked") - Join createJoin(SearchBuilderJoinEnum theType, String theSearchParameterName) { - Join join = null; - switch (theType) { - case DATE: - join = myQueryRoot.join("myParamsDate", JoinType.LEFT); - break; - case NUMBER: - join = myQueryRoot.join("myParamsNumber", JoinType.LEFT); - break; - case QUANTITY: - join = myQueryRoot.join("myParamsQuantity", JoinType.LEFT); - break; - case REFERENCE: - join = myQueryRoot.join("myResourceLinks", JoinType.LEFT); - break; - case STRING: - join = myQueryRoot.join("myParamsString", JoinType.LEFT); - break; - case URI: - join = myQueryRoot.join("myParamsUri", JoinType.LEFT); - break; - case TOKEN: - join = myQueryRoot.join("myParamsToken", JoinType.LEFT); - break; - case COORDS: - join = myQueryRoot.join("myParamsCoords", JoinType.LEFT); - break; - } - - SearchBuilderJoinKey key = new SearchBuilderJoinKey(theSearchParameterName, theType); - myQueryRoot.putIndex(key, join); - - return (Join) join; - } - void addPredicateParamMissingForReference(String theResourceName, String theParamName, boolean theMissing, RequestPartitionId theRequestPartitionId) { - Join paramPresentJoin = myQueryRoot.join("mySearchParamPresents", JoinType.LEFT); + From paramPresentJoin = myQueryStack.createJoin(SearchBuilderJoinEnum.PRESENCE, null); Expression hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class); Long hash = SearchParamPresent.calculateHashPresence(myPartitionSettings, theRequestPartitionId, theResourceName, theParamName, !theMissing); @@ -126,22 +87,20 @@ abstract class BasePredicateBuilder { addPartitionIdPredicate(theRequestPartitionId, paramPresentJoin, predicates); - myQueryRoot.setHasIndexJoins(); - myQueryRoot.addPredicates(predicates); + myQueryStack.addPredicatesWithImplicitTypeSelection(predicates); } - void addPredicateParamMissingForNonReference(String theResourceName, String theParamName, boolean theMissing, Join theJoin, RequestPartitionId theRequestPartitionId) { + void addPredicateParamMissingForNonReference(String theResourceName, String theParamName, boolean theMissing, From theJoin, RequestPartitionId theRequestPartitionId) { if (!theRequestPartitionId.isAllPartitions()) { if (theRequestPartitionId.getPartitionId() != null) { - myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue"), theRequestPartitionId.getPartitionId())); + myQueryStack.addPredicate(myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue"), theRequestPartitionId.getPartitionId())); } else { - myQueryRoot.addPredicate(myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue"))); + myQueryStack.addPredicate(myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue"))); } } - myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myResourceType"), theResourceName)); - myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myParamName"), theParamName)); - myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myMissing"), theMissing)); - myQueryRoot.setHasIndexJoins(); + myQueryStack.addPredicateWithImplicitTypeSelection(myCriteriaBuilder.equal(theJoin.get("myResourceType"), theResourceName)); + myQueryStack.addPredicate(myCriteriaBuilder.equal(theJoin.get("myParamName"), theParamName)); + myQueryStack.addPredicate(myCriteriaBuilder.equal(theJoin.get("myMissing"), theMissing)); } Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From theFrom, Predicate thePredicate, RequestPartitionId theRequestPartitionId) { @@ -232,7 +191,7 @@ abstract class BasePredicateBuilder { } else { partitionPredicate = myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue").as(Integer.class)); } - myQueryRoot.addPredicate(partitionPredicate); + myQueryStack.addPredicate(partitionPredicate); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/IndexJoins.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/IndexJoins.java index f3636677935..d699bd4ae23 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/IndexJoins.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/IndexJoins.java @@ -20,8 +20,6 @@ package ca.uhn.fhir.jpa.dao.predicate; * #L% */ -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import com.google.common.collect.Maps; import javax.persistence.criteria.Join; @@ -30,7 +28,7 @@ import java.util.Map; public class IndexJoins { Map> myIndexJoins = Maps.newHashMap(); - public void put(SearchBuilderJoinKey theKey, Join theJoin) { + public void put(SearchBuilderJoinKey theKey, Join theJoin) { myIndexJoins.put(theKey, theJoin); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilder.java index 69d77e3dc0a..b6a4c9edd5b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilder.java @@ -97,10 +97,10 @@ public class PredicateBuilder { } Subquery createLinkSubquery(String theParameterName, String theTargetResourceType, ArrayList theOrValues, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { - return myPredicateBuilderReference.createLinkSubquery(true, theParameterName, theTargetResourceType, theOrValues, theRequest, theRequestPartitionId); + return myPredicateBuilderReference.createLinkSubquery(theParameterName, theTargetResourceType, theOrValues, theRequest, theRequestPartitionId); } - Predicate createResourceLinkPathPredicate(String theTargetResourceType, String theParamReference, Join theJoin) { + Predicate createResourceLinkPathPredicate(String theTargetResourceType, String theParamReference, Join theJoin) { return myPredicateBuilderReference.createResourceLinkPathPredicate(theTargetResourceType, theParamReference, theJoin); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java index 2110d9f1cf0..3e64048178f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java @@ -23,7 +23,6 @@ package ca.uhn.fhir.jpa.dao.predicate; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.util.CoordCalculator; import ca.uhn.fhir.jpa.util.SearchBox; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -39,7 +38,6 @@ import org.springframework.stereotype.Component; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; @@ -151,7 +149,7 @@ public class PredicateBuilderCoords extends BasePredicateBuilder implements IPre List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - Join join = createJoin(SearchBuilderJoinEnum.COORDS, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.COORDS, theParamName); if (theList.get(0).getMissing() != null) { addPredicateParamMissingForNonReference(theResourceName, theParamName, theList.get(0).getMissing(), join, theRequestPartitionId); @@ -173,8 +171,7 @@ public class PredicateBuilderCoords extends BasePredicateBuilder implements IPre } Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.addPredicate(retVal); - myQueryRoot.setHasIndexJoins(); + myQueryStack.addPredicateWithImplicitTypeSelection(retVal); return retVal; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java index d999c293603..702819eaa9d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java @@ -23,7 +23,6 @@ package ca.uhn.fhir.jpa.dao.predicate; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.rest.param.DateParam; @@ -37,7 +36,6 @@ import org.springframework.stereotype.Component; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.Date; @@ -50,7 +48,7 @@ import java.util.Map; public class PredicateBuilderDate extends BasePredicateBuilder implements IPredicateBuilder { private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderDate.class); - private Map> myJoinMap; + private Map> myJoinMap; PredicateBuilderDate(SearchBuilder theSearchBuilder) { super(theSearchBuilder); @@ -69,9 +67,9 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi } String key = theResourceName + " " + theParamName; - Join join = myJoinMap.get(key); + From join = myJoinMap.get(key); if (join == null) { - join = createJoin(SearchBuilderJoinEnum.DATE, theParamName); + join = myQueryStack.createJoin(SearchBuilderJoinEnum.DATE, theParamName); myJoinMap.put(key, join); newJoin = true; } @@ -95,12 +93,11 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi Predicate orPredicates = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.setHasIndexJoins(); if (newJoin) { Predicate identityAndValuePredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, orPredicates, theRequestPartitionId); - myQueryRoot.addPredicate(identityAndValuePredicate); + myQueryStack.addPredicateWithImplicitTypeSelection(identityAndValuePredicate); } else { - myQueryRoot.addPredicate(orPredicates); + myQueryStack.addPredicateWithImplicitTypeSelection(orPredicates); } return orPredicates; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java index 0194343d205..24466e085ff 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java @@ -23,7 +23,6 @@ package ca.uhn.fhir.jpa.dao.predicate; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.ParamPrefixEnum; @@ -33,7 +32,7 @@ import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import javax.persistence.criteria.Expression; -import javax.persistence.criteria.Join; +import javax.persistence.criteria.From; import javax.persistence.criteria.Predicate; import java.math.BigDecimal; import java.util.ArrayList; @@ -57,7 +56,7 @@ class PredicateBuilderNumber extends BasePredicateBuilder implements IPredicateB SearchFilterParser.CompareOperation operation, RequestPartitionId theRequestPartitionId) { - Join join = createJoin(SearchBuilderJoinEnum.NUMBER, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.NUMBER, theParamName); if (theList.get(0).getMissing() != null) { addPredicateParamMissingForNonReference(theResourceName, theParamName, theList.get(0).getMissing(), join, theRequestPartitionId); @@ -109,8 +108,7 @@ class PredicateBuilderNumber extends BasePredicateBuilder implements IPredicateB } Predicate predicate = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.setHasIndexJoins(); - myQueryRoot.addPredicate(predicate); + myQueryStack.addPredicateWithImplicitTypeSelection(predicate); return predicate; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java index 9c40570dd85..2a98b9f7711 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java @@ -24,7 +24,6 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.base.composite.BaseQuantityDt; import ca.uhn.fhir.rest.param.ParamPrefixEnum; @@ -35,7 +34,6 @@ import org.springframework.stereotype.Component; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Expression; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; import java.math.BigDecimal; import java.util.ArrayList; @@ -59,14 +57,14 @@ class PredicateBuilderQuantity extends BasePredicateBuilder implements IPredicat SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - Join join = createJoin(SearchBuilderJoinEnum.QUANTITY, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.QUANTITY, theParamName); if (theList.get(0).getMissing() != null) { addPredicateParamMissingForNonReference(theResourceName, theParamName, theList.get(0).getMissing(), join, theRequestPartitionId); return null; } - List codePredicates = new ArrayList(); + List codePredicates = new ArrayList<>(); addPartitionIdPredicate(theRequestPartitionId, join, codePredicates); for (IQueryParameterType nextOr : theList) { @@ -75,8 +73,7 @@ class PredicateBuilderQuantity extends BasePredicateBuilder implements IPredicat } Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.setHasIndexJoins(); - myQueryRoot.addPredicate(retVal); + myQueryStack.addPredicateWithImplicitTypeSelection(retVal); return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java index f55d5fc3c4b..b5241f9f5bc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java @@ -38,14 +38,12 @@ import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceLink; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; @@ -61,6 +59,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.param.CompositeParam; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.HasParam; @@ -121,7 +120,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { @Autowired DaoRegistry myDaoRegistry; @Autowired - PartitionSettings myPartitionSettings; + PartitionSettings myPartitionSettings; @Autowired private IInterceptorBroadcaster myInterceptorBroadcaster; @@ -141,7 +140,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { - //Is this just to ensure the chain has been split correctly??? + // This just to ensure the chain has been split correctly assert theParamName.contains(".") == false; if ((operation != null) && @@ -155,7 +154,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { return null; } - Join join = createJoin(SearchBuilderJoinEnum.REFERENCE, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.REFERENCE, theParamName); List targetIds = new ArrayList<>(); List targetQualifiedUrls = new ArrayList<>(); @@ -251,17 +250,12 @@ class PredicateBuilderReference extends BasePredicateBuilder { codePredicates.add(myCriteriaBuilder.and(pathPredicate, pidPredicate)); } - myQueryRoot.setHasIndexJoins(); if (codePredicates.size() > 0) { Predicate predicate = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.addPredicate(predicate); + myQueryStack.addPredicateWithImplicitTypeSelection(predicate); return predicate; } else { - // Add a predicate that will never match - Predicate pidPredicate = join.get("myTargetResourcePid").in(-1L); - myQueryRoot.clearPredicates(); - myQueryRoot.addPredicate(pidPredicate); - return pidPredicate; + return myQueryStack.addNeverMatchingPredicate(); } } @@ -269,7 +263,124 @@ class PredicateBuilderReference extends BasePredicateBuilder { * This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain * on the device. */ - private Predicate addPredicateReferenceWithChain(String theResourceName, String theParamName, List theList, Join theJoin, List theCodePredicates, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + private Predicate addPredicateReferenceWithChain(String theResourceName, String theParamName, List theList, From theJoin, List theCodePredicates, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + + /* + * Which resource types can the given chained parameter actually link to? This might be a list + * where the chain is unqualified, as in: Observation?subject.identifier=(...) + * since subject can link to several possible target types. + * + * If the user has qualified the chain, as in: Observation?subject:Patient.identifier=(...) + * this is just a simple 1-entry list. + */ + final List> resourceTypes = determineCandidateResourceTypesForChain(theResourceName, theParamName, theReferenceParam); + + /* + * Handle chain on _type + */ + if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) { + return createChainPredicateOnType(theResourceName, theParamName, theJoin, theReferenceParam, resourceTypes); + } + + boolean foundChainMatch = false; + List> candidateTargetTypes = new ArrayList<>(); + for (Class nextType : resourceTypes) { + String chain = theReferenceParam.getChain(); + + String remainingChain = null; + int chainDotIndex = chain.indexOf('.'); + if (chainDotIndex != -1) { + remainingChain = chain.substring(chainDotIndex + 1); + chain = chain.substring(0, chainDotIndex); + } + + RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType); + String subResourceName = typeDef.getName(); + + IDao dao = myDaoRegistry.getResourceDao(nextType); + if (dao == null) { + ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName()); + continue; + } + + int qualifierIndex = chain.indexOf(':'); + String qualifier = null; + if (qualifierIndex != -1) { + qualifier = chain.substring(qualifierIndex); + chain = chain.substring(0, qualifierIndex); + } + + boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain); + RuntimeSearchParam param = null; + if (!isMeta) { + param = mySearchParamRegistry.getSearchParamByName(typeDef, chain); + if (param == null) { + ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param); + continue; + } + } + + ArrayList orValues = Lists.newArrayList(); + + for (IQueryParameterType next : theList) { + String nextValue = next.getValueAsQueryToken(myContext); + IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue); + if (chainValue == null) { + continue; + } + foundChainMatch = true; + orValues.add(chainValue); + } + + // If this is false, we throw an exception below so no sense doing any further processing + if (foundChainMatch) { + Subquery subQ = createLinkSubquery(chain, subResourceName, orValues, theRequest, theRequestPartitionId); + + Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin); + Predicate pidPredicate = theJoin.get("myTargetResourcePid").in(subQ); + Predicate andPredicate = myCriteriaBuilder.and(pathPredicate, pidPredicate); + theCodePredicates.add(andPredicate); + candidateTargetTypes.add(nextType); + } + } + + if (!foundChainMatch) { + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + theReferenceParam.getChain())); + } + + if (candidateTargetTypes.size() > 1) { + warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, candidateTargetTypes); + } + + Predicate predicate = myCriteriaBuilder.or(toArray(theCodePredicates)); + myQueryStack.addPredicate(predicate); + return predicate; + } + + private Predicate createChainPredicateOnType(String theResourceName, String theParamName, From theJoin, ReferenceParam theReferenceParam, List> theResourceTypes) { + String typeValue = theReferenceParam.getValue(); + + Class wantedType; + try { + wantedType = myContext.getResourceDefinition(typeValue).getImplementingClass(); + } catch (DataFormatException e) { + throw newInvalidResourceTypeException(typeValue); + } + if (!theResourceTypes.contains(wantedType)) { + throw newInvalidTargetTypeForChainException(theResourceName, theParamName, typeValue); + } + + Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin); + Predicate sourceTypeParameter = myCriteriaBuilder.equal(theJoin.get("mySourceResourceType"), myResourceName); + Predicate targetTypeParameter = myCriteriaBuilder.equal(theJoin.get("myTargetResourceType"), typeValue); + + Predicate composite = myCriteriaBuilder.and(pathPredicate, sourceTypeParameter, targetTypeParameter); + myQueryStack.addPredicate(composite); + return composite; + } + + @Nonnull + private List> determineCandidateResourceTypesForChain(String theResourceName, String theParamName, ReferenceParam theReferenceParam) { final List> resourceTypes; if (!theReferenceParam.hasResourceType()) { @@ -339,101 +450,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { } } - - // Handle chain on _type - if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) { - String typeValue = theReferenceParam.getValue(); - - Class wantedType; - try { - wantedType = myContext.getResourceDefinition(typeValue).getImplementingClass(); - } catch (DataFormatException e) { - throw newInvalidResourceTypeException(typeValue); - } - if (!resourceTypes.contains(wantedType)) { - throw newInvalidTargetTypeForChainException(theResourceName, theParamName, typeValue); - } - - Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin); - Predicate sourceTypeParameter = myCriteriaBuilder.equal(theJoin.get("mySourceResourceType"), myResourceName); - Predicate targetTypeParameter = myCriteriaBuilder.equal(theJoin.get("myTargetResourceType"), typeValue); - - Predicate composite = myCriteriaBuilder.and(pathPredicate, sourceTypeParameter, targetTypeParameter); - myQueryRoot.addPredicate(composite); - return composite; - } - - boolean foundChainMatch = false; - List> candidateTargetTypes = new ArrayList<>(); - for (Class nextType : resourceTypes) { - String chain = theReferenceParam.getChain(); - - String remainingChain = null; - int chainDotIndex = chain.indexOf('.'); - if (chainDotIndex != -1) { - remainingChain = chain.substring(chainDotIndex + 1); - chain = chain.substring(0, chainDotIndex); - } - - RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType); - String subResourceName = typeDef.getName(); - - IDao dao = myDaoRegistry.getResourceDao(nextType); - if (dao == null) { - ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName()); - continue; - } - - int qualifierIndex = chain.indexOf(':'); - String qualifier = null; - if (qualifierIndex != -1) { - qualifier = chain.substring(qualifierIndex); - chain = chain.substring(0, qualifierIndex); - } - - boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain); - RuntimeSearchParam param = null; - if (!isMeta) { - param = mySearchParamRegistry.getSearchParamByName(typeDef, chain); - if (param == null) { - ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param); - continue; - } - } - - ArrayList orValues = Lists.newArrayList(); - - for (IQueryParameterType next : theList) { - String nextValue = next.getValueAsQueryToken(myContext); - IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue); - if (chainValue == null) { - continue; - } - foundChainMatch = true; - orValues.add(chainValue); - } - - - Subquery subQ = createLinkSubquery(foundChainMatch, chain, subResourceName, orValues, theRequest, theRequestPartitionId); - - Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin); - Predicate pidPredicate = theJoin.get("myTargetResourcePid").in(subQ); - Predicate andPredicate = myCriteriaBuilder.and(pathPredicate, pidPredicate); - theCodePredicates.add(andPredicate); - candidateTargetTypes.add(nextType); - } - - if (!foundChainMatch) { - throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + theReferenceParam.getChain())); - } - - if (candidateTargetTypes.size() > 1) { - warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, candidateTargetTypes); - } - - Predicate predicate = myCriteriaBuilder.or(toArray(theCodePredicates)); - myQueryRoot.addPredicate(predicate); - return predicate; + return resourceTypes; } private void warnAboutPerformanceOnUnqualifiedResources(String theParamName, RequestDetails theRequest, @Nullable List> theCandidateTargetTypes) { @@ -510,33 +527,30 @@ class PredicateBuilderReference extends BasePredicateBuilder { return chainValue; } - Subquery createLinkSubquery(boolean theFoundChainMatch, String theChain, String theSubResourceName, List theOrValues, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { - Subquery subQ = myQueryRoot.subquery(Long.class); + Subquery createLinkSubquery(String theChain, String theSubResourceName, List theOrValues, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + /* * We're doing a chain call, so push the current query root * and predicate list down and put new ones at the top of the * stack and run a subquery */ - myQueryRoot.push(subQ); - subQ.select(myQueryRoot.get("myId").as(Long.class)); + RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theSubResourceName, theChain); + if (nextParamDef != null && !theChain.startsWith("_")) { + myQueryStack.pushIndexTableSubQuery(); + } else { + myQueryStack.pushResourceTableSubQuery(theSubResourceName); + } List> andOrParams = new ArrayList<>(); andOrParams.add(theOrValues); - // Create the subquery predicates - myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myResourceType"), theSubResourceName)); - myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted"))); - - if (theFoundChainMatch) { - searchForIdsWithAndOr(theSubResourceName, theChain, andOrParams, theRequest, theRequestPartitionId); - subQ.where(myQueryRoot.getPredicateArray()); - } + searchForIdsWithAndOr(theSubResourceName, theChain, andOrParams, theRequest, theRequestPartitionId); /* * Pop the old query root and predicate list back */ - myQueryRoot.pop(); - return subQ; + return (Subquery) myQueryStack.pop(); + } void searchForIdsWithAndOr(String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { @@ -656,18 +670,18 @@ class PredicateBuilderReference extends BasePredicateBuilder { // TODO: we clear the predicates below because the filter builds up // its own collection of predicates. It'd probably be good at some // point to do something more fancy... - ArrayList holdPredicates = new ArrayList<>(myQueryRoot.getPredicates()); + ArrayList holdPredicates = new ArrayList<>(myQueryStack.getPredicates()); Predicate filterPredicate = processFilter(filter, theResourceName, theRequest, theRequestPartitionId); - myQueryRoot.clearPredicates(); - myQueryRoot.addPredicates(holdPredicates); - myQueryRoot.addPredicate(filterPredicate); + myQueryStack.clearPredicates(); + myQueryStack.addPredicates(holdPredicates); + myQueryStack.addPredicate(filterPredicate); // Because filters can have an OR at the root, we never know for sure that we haven't done an optimized // search that doesn't check the resource type. This could be improved in the future, but for now it's // safest to just clear this flag. The test "testRetrieveDifferentTypeEq" will fail if we don't clear // this here. - myQueryRoot.clearHasIndexJoins(); + myQueryStack.clearHasImplicitTypeSelection(); } } @@ -844,13 +858,13 @@ class PredicateBuilderReference extends BasePredicateBuilder { Predicate predicate; if ((operation == null) || (operation == SearchFilterParser.CompareOperation.eq)) { - predicate = myQueryRoot.get("myLanguage").as(String.class).in(values); + predicate = myQueryStack.get("myLanguage").as(String.class).in(values); } else if (operation == SearchFilterParser.CompareOperation.ne) { - predicate = myQueryRoot.get("myLanguage").as(String.class).in(values).not(); + predicate = myQueryStack.get("myLanguage").as(String.class).in(values).not(); } else { throw new InvalidRequestException("Unsupported operator specified in language query, only \"eq\" and \"ne\" are supported"); } - myQueryRoot.addPredicate(predicate); + myQueryStack.addPredicate(predicate); if (operation != null) { return predicate; } @@ -874,7 +888,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { throw new InvalidRequestException(msg); } - Join join = myQueryRoot.join("myProvenance", JoinType.LEFT); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.PROVENANCE, Constants.PARAM_SOURCE); List codePredicates = new ArrayList<>(); @@ -894,7 +908,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { } Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.addPredicate(retVal); + myQueryStack.addPredicate(retVal); return retVal; } @@ -962,13 +976,13 @@ class PredicateBuilderReference extends BasePredicateBuilder { } Subquery subQ = myPredicateBuilder.createLinkSubquery(paramName, targetResourceType, orValues, theRequest, theRequestPartitionId); - Join join = myQueryRoot.join("myResourceLinksAsTarget", JoinType.LEFT); + Join join = (Join) myQueryStack.createJoin(SearchBuilderJoinEnum.HAS, "_has"); Predicate pathPredicate = myPredicateBuilder.createResourceLinkPathPredicate(targetResourceType, paramReference, join); Predicate sourceTypePredicate = myCriteriaBuilder.equal(join.get("myTargetResourceType"), theResourceType); Predicate sourcePidPredicate = join.get("mySourceResourcePid").in(subQ); Predicate andPredicate = myCriteriaBuilder.and(pathPredicate, sourcePidPredicate, sourceTypePredicate); - myQueryRoot.addPredicate(andPredicate); + myQueryStack.addPredicate(andPredicate); } } @@ -987,15 +1001,15 @@ class PredicateBuilderReference extends BasePredicateBuilder { RuntimeSearchParam left = theParamDef.getCompositeOf().get(0); IQueryParameterType leftValue = cp.getLeftValue(); - myQueryRoot.addPredicate(createCompositeParamPart(theResourceName, myQueryRoot.getRoot(), left, leftValue, theRequestPartitionId)); + myQueryStack.addPredicate(createCompositeParamPart(theResourceName, myQueryStack.getRootForComposite(), left, leftValue, theRequestPartitionId)); RuntimeSearchParam right = theParamDef.getCompositeOf().get(1); IQueryParameterType rightValue = cp.getRightValue(); - myQueryRoot.addPredicate(createCompositeParamPart(theResourceName, myQueryRoot.getRoot(), right, rightValue, theRequestPartitionId)); + myQueryStack.addPredicate(createCompositeParamPart(theResourceName, myQueryStack.getRootForComposite(), right, rightValue, theRequestPartitionId)); } - private Predicate createCompositeParamPart(String theResourceName, Root theRoot, RuntimeSearchParam theParam, IQueryParameterType leftValue, RequestPartitionId theRequestPartitionId) { + private Predicate createCompositeParamPart(String theResourceName, Root theRoot, RuntimeSearchParam theParam, IQueryParameterType leftValue, RequestPartitionId theRequestPartitionId) { Predicate retVal = null; switch (theParam.getParamType()) { case STRING: { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java index 2d4ec8d8a0e..5e6d09ae06b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java @@ -23,9 +23,8 @@ package ca.uhn.fhir.jpa.dao.predicate; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import org.hl7.fhir.r4.model.IdType; import org.slf4j.Logger; @@ -36,7 +35,6 @@ import org.springframework.stereotype.Component; import javax.annotation.Nullable; import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -60,10 +58,10 @@ class PredicateBuilderResourceId extends BasePredicateBuilder { @Nullable Predicate addPredicateResourceId(List> theValues, String theResourceName, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - Predicate nextPredicate = createPredicate(myQueryRoot.getRoot(), theResourceName, theValues, theOperation, theRequestPartitionId); + Predicate nextPredicate = createPredicate(theResourceName, theValues, theOperation, theRequestPartitionId); if (nextPredicate != null) { - myQueryRoot.addPredicate(nextPredicate); + myQueryStack.addPredicate(nextPredicate); return nextPredicate; } @@ -71,7 +69,7 @@ class PredicateBuilderResourceId extends BasePredicateBuilder { } @Nullable - private Predicate createPredicate(Root theRoot, String theResourceName, List> theValues, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + private Predicate createPredicate(String theResourceName, List> theValues, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { Predicate nextPredicate = null; Set allOrPids = null; @@ -110,7 +108,7 @@ class PredicateBuilderResourceId extends BasePredicateBuilder { if (allOrPids != null && allOrPids.isEmpty()) { // This will never match - nextPredicate = myCriteriaBuilder.equal(theRoot.get("myId").as(Long.class), -1); + nextPredicate = myCriteriaBuilder.equal(myQueryStack.getResourcePidColumn(), -1); } else if (allOrPids != null) { @@ -120,11 +118,11 @@ class PredicateBuilderResourceId extends BasePredicateBuilder { switch (operation) { default: case eq: - codePredicates.add(theRoot.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(allOrPids))); + codePredicates.add(myQueryStack.getResourcePidColumn().in(ResourcePersistentId.toLongList(allOrPids))); nextPredicate = myCriteriaBuilder.and(toArray(codePredicates)); break; case ne: - codePredicates.add(theRoot.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(allOrPids)).not()); + codePredicates.add(myQueryStack.getResourcePidColumn().in(ResourcePersistentId.toLongList(allOrPids)).not()); nextPredicate = myCriteriaBuilder.and(toArray(codePredicates)); break; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java index 7bbfa5c6c8e..2d97ade264b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java @@ -24,7 +24,6 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.util.StringNormalizer; import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -38,7 +37,6 @@ import org.springframework.stereotype.Component; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; @@ -46,6 +44,7 @@ import java.util.List; @Component @Scope("prototype") class PredicateBuilderString extends BasePredicateBuilder implements IPredicateBuilder { + @Autowired DaoConfig myDaoConfig; @@ -60,7 +59,7 @@ class PredicateBuilderString extends BasePredicateBuilder implements IPredicateB SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - Join join = createJoin(SearchBuilderJoinEnum.STRING, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.STRING, theParamName); if (theList.get(0).getMissing() != null) { addPredicateParamMissingForNonReference(theResourceName, theParamName, theList.get(0).getMissing(), join, theRequestPartitionId); @@ -77,8 +76,7 @@ class PredicateBuilderString extends BasePredicateBuilder implements IPredicateB Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.setHasIndexJoins(); - myQueryRoot.addPredicate(retVal); + myQueryStack.addPredicateWithImplicitTypeSelection(retVal); return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java index 389204c408f..7691515e765 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.dao.predicate; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.SearchBuilder; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTag; import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; @@ -41,8 +40,6 @@ import org.springframework.stereotype.Component; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; -import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; @@ -137,14 +134,14 @@ class PredicateBuilderTag extends BasePredicateBuilder { if (paramInverted) { ourLog.debug("Searching for _tag:not"); - Subquery subQ = myQueryRoot.subquery(Long.class); + Subquery subQ = myQueryStack.subqueryForTagNegation(); Root subQfrom = subQ.from(ResourceTag.class); subQ.select(subQfrom.get("myResourceId").as(Long.class)); - myQueryRoot.addPredicate( + myQueryStack.addPredicate( myCriteriaBuilder.not( myCriteriaBuilder.in( - myQueryRoot.get("myId") + myQueryStack.get("myId") ).value(subQ) ) ); @@ -162,7 +159,7 @@ class PredicateBuilderTag extends BasePredicateBuilder { } - Join tagJoin = myQueryRoot.join("myTags", JoinType.LEFT); + From tagJoin = myQueryStack.createJoin(SearchBuilderJoinEnum.RESOURCE_TAGS, null); From defJoin = tagJoin.join("myTag"); Predicate tagListPredicate = createPredicateTagList(defJoin, myCriteriaBuilder, tagType, tokens); @@ -172,7 +169,7 @@ class PredicateBuilderTag extends BasePredicateBuilder { addPartitionIdPredicate(theRequestPartitionId, tagJoin, predicates); } - myQueryRoot.addPredicates(predicates); + myQueryStack.addPredicates(predicates); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java index 6a91e496fd1..cd4fe0e4079 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java @@ -30,7 +30,6 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; @@ -53,7 +52,6 @@ import org.springframework.stereotype.Component; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Expression; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import java.util.ArrayList; @@ -91,7 +89,7 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu RequestPartitionId theRequestPartitionId) { if (theList.get(0).getMissing() != null) { - Join join = createJoin(SearchBuilderJoinEnum.TOKEN, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.TOKEN, theParamName); addPredicateParamMissingForNonReference(theResourceName, theParamName, theList.get(0).getMissing(), join, theRequestPartitionId); return null; } @@ -130,7 +128,7 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu return null; } - Join join = createJoin(SearchBuilderJoinEnum.TOKEN, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.TOKEN, theParamName); addPartitionIdPredicate(theRequestPartitionId, join, codePredicates); Collection singleCode = createPredicateToken(tokens, theResourceName, theParamName, myCriteriaBuilder, join, theOperation, theRequestPartitionId); @@ -139,8 +137,7 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu Predicate spPredicate = myCriteriaBuilder.or(toArray(codePredicates)); - myQueryRoot.setHasIndexJoins(); - myQueryRoot.addPredicate(spPredicate); + myQueryStack.addPredicateWithImplicitTypeSelection(spPredicate); return spPredicate; } @@ -375,7 +372,13 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu break; } - Predicate predicate = hashField.in(values); + Predicate predicate; + if (values.size() == 1) { + predicate = myCriteriaBuilder.equal(hashField, values.get(0)); + } else { + predicate = hashField.in(values); + } + if (theModifier == TokenParamModifier.NOT) { Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName)); Predicate disjunctionPredicate = theBuilder.not(predicate); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java index 45488ccf759..52f7503d1a2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java @@ -25,7 +25,6 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.param.UriParamQualifierEnum; @@ -35,7 +34,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import javax.persistence.criteria.Join; +import javax.persistence.criteria.From; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.Collection; @@ -59,7 +58,7 @@ class PredicateBuilderUri extends BasePredicateBuilder implements IPredicateBuil SearchFilterParser.CompareOperation operation, RequestPartitionId theRequestPartitionId) { - Join join = createJoin(SearchBuilderJoinEnum.URI, theParamName); + From join = myQueryStack.createJoin(SearchBuilderJoinEnum.URI, theParamName); if (theList.get(0).getMissing() != null) { addPredicateParamMissingForNonReference(theResourceName, theParamName, theList.get(0).getMissing(), join, theRequestPartitionId); @@ -169,8 +168,7 @@ class PredicateBuilderUri extends BasePredicateBuilder implements IPredicateBuil */ if (codePredicates.isEmpty()) { Predicate predicate = myCriteriaBuilder.isNull(join.get("myMissing").as(String.class)); - myQueryRoot.setHasIndexJoins(); - myQueryRoot.addPredicate(predicate); + myQueryStack.addPredicateWithImplicitTypeSelection(predicate); return null; } @@ -181,8 +179,7 @@ class PredicateBuilderUri extends BasePredicateBuilder implements IPredicateBuil join, orPredicate, theRequestPartitionId); - myQueryRoot.setHasIndexJoins(); - myQueryRoot.addPredicate(outerPredicate); + myQueryStack.addPredicateWithImplicitTypeSelection(outerPredicate); return outerPredicate; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/QueryRoot.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/QueryRoot.java deleted file mode 100644 index 5afdbcb5a6d..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/QueryRoot.java +++ /dev/null @@ -1,106 +0,0 @@ -package ca.uhn.fhir.jpa.dao.predicate; - -/*- - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * 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.jpa.model.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; - -import javax.persistence.criteria.*; -import java.util.List; -import java.util.Stack; - -public class QueryRoot { - private final Stack myQueryRootStack = new Stack<>(); - private boolean myHasIndexJoins; - - public void push(AbstractQuery theResourceTableQuery) { - myQueryRootStack.push(new QueryRootEntry(theResourceTableQuery)); - } - - private QueryRootEntry top() { - return myQueryRootStack.peek(); - } - - void pop() { - myQueryRootStack.pop(); - } - - public Root getRoot() { - return top().getRoot(); - } - - public Path get(String theAttributeName) { - return top().get(theAttributeName); - } - - public Join join(String theAttributeName, JoinType theJoinType) { - return top().join(theAttributeName, theJoinType); - } - - public Join getIndexJoin(SearchBuilderJoinKey theKey) { - return top().getIndexJoin(theKey); - } - - public void addPredicate(Predicate thePredicate) { - top().addPredicate(thePredicate); - } - - public void addPredicates(List thePredicates) { - top().addPredicates(thePredicates); - } - - public Predicate[] getPredicateArray() { - return top().getPredicateArray(); - } - - void putIndex(SearchBuilderJoinKey theKey, Join theJoin) { - myHasIndexJoins = true; - top().putIndex(theKey, theJoin); - } - - void clearPredicates() { - top().clearPredicates(); - } - - List getPredicates() { - return top().getPredicates(); - } - - public void where(Predicate theAnd) { - top().where(theAnd); - } - - Subquery subquery(Class theClass) { - return top().subquery(theClass); - } - - public boolean hasIndexJoins() { - return myHasIndexJoins; - } - - public void setHasIndexJoins() { - myHasIndexJoins = true; - } - - public void clearHasIndexJoins() { - myHasIndexJoins = false; - } -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/QueryRootEntry.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/QueryRootEntry.java deleted file mode 100644 index 55a49cb6e21..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/QueryRootEntry.java +++ /dev/null @@ -1,89 +0,0 @@ -package ca.uhn.fhir.jpa.dao.predicate; - -/*- - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * 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.jpa.model.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; - -import javax.persistence.criteria.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class QueryRootEntry { - private final AbstractQuery myResourceTableQuery; - private final Root myResourceTableRoot; - private final ArrayList myPredicates = new ArrayList<>(); - private final IndexJoins myIndexJoins = new IndexJoins(); - - public QueryRootEntry(AbstractQuery theResourceTableQuery) { - myResourceTableQuery = theResourceTableQuery; - myResourceTableRoot = theResourceTableQuery.from(ResourceTable.class); - } - - public Root getRoot() { - return myResourceTableRoot; - } - - public Path get(String theAttributeName) { - return myResourceTableRoot.get(theAttributeName); - } - - public Join join(String theAttributeName, JoinType theJoinType) { - return myResourceTableRoot.join(theAttributeName, theJoinType); - } - - public Join getIndexJoin(SearchBuilderJoinKey theKey) { - return myIndexJoins.get(theKey); - } - - public void addPredicate(Predicate thePredicate) { - myPredicates.add(thePredicate); - } - - public void addPredicates(List thePredicates) { - myPredicates.addAll(thePredicates); - } - - public Predicate[] getPredicateArray() { - return myPredicates.toArray(new Predicate[0]); - } - - void putIndex(SearchBuilderJoinKey theKey, Join theJoin) { - myIndexJoins.put(theKey, theJoin); - } - - void clearPredicates() { - myPredicates.clear(); - } - - List getPredicates() { - return Collections.unmodifiableList(myPredicates); - } - - public void where(Predicate theAnd) { - myResourceTableQuery.where(theAnd); - } - - Subquery subquery(Class theClass) { - return myResourceTableQuery.subquery(theClass); - } -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinEnum.java index 48bcb346b61..6c16d5e0843 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinEnum.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinEnum.java @@ -28,6 +28,8 @@ public enum SearchBuilderJoinEnum { STRING, TOKEN, URI, - COORDS + COORDS, + HAS, + FORCED_ID, PRESENCE, COMPOSITE_UNIQUE, RESOURCE_TAGS, PROVENANCE } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntry.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntry.java new file mode 100644 index 00000000000..c70f9be97b4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntry.java @@ -0,0 +1,124 @@ +package ca.uhn.fhir.jpa.dao.predicate.querystack; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * 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.jpa.dao.predicate.IndexJoins; +import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinEnum; +import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinKey; + +import javax.persistence.criteria.AbstractQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +abstract class QueryRootEntry { + private final ArrayList myPredicates = new ArrayList<>(); + private final IndexJoins myIndexJoins = new IndexJoins(); + private final CriteriaBuilder myCriteriaBuilder; + private boolean myHasImplicitTypeSelection; + + QueryRootEntry(CriteriaBuilder theCriteriaBuilder) { + myCriteriaBuilder = theCriteriaBuilder; + } + + boolean isHasImplicitTypeSelection() { + return myHasImplicitTypeSelection; + } + + void setHasImplicitTypeSelection(boolean theHasImplicitTypeSelection) { + myHasImplicitTypeSelection = theHasImplicitTypeSelection; + } + + Optional> getIndexJoin(SearchBuilderJoinKey theKey) { + return Optional.ofNullable(myIndexJoins.get(theKey)); + } + + void addPredicate(Predicate thePredicate) { + myPredicates.add(thePredicate); + } + + void addPredicates(List thePredicates) { + myPredicates.addAll(thePredicates); + } + + Predicate addNeverMatchingPredicate() { + Predicate predicate = myCriteriaBuilder.equal(getResourcePidColumn(), -1L); + clearPredicates(); + addPredicate(predicate); + return predicate; + } + + Predicate[] getPredicateArray() { + return myPredicates.toArray(new Predicate[0]); + } + + void putIndex(SearchBuilderJoinKey theKey, Join theJoin) { + myIndexJoins.put(theKey, theJoin); + } + + void clearPredicates() { + myPredicates.clear(); + } + + List getPredicates() { + return Collections.unmodifiableList(myPredicates); + } + + Path get(String theAttributeName) { + return getRoot().get(theAttributeName); + } + + AbstractQuery pop() { + Predicate[] predicateArray = getPredicateArray(); + if (predicateArray.length == 1) { + getQueryRoot().where(predicateArray[0]); + } else { + getQueryRoot().where(myCriteriaBuilder.and(predicateArray)); + } + + return getQueryRoot(); + } + + abstract void orderBy(List theOrders); + + abstract Expression getLastUpdatedColumn(); + + abstract From createJoin(SearchBuilderJoinEnum theType, String theSearchParameterName); + + abstract AbstractQuery getQueryRoot(); + + abstract Root getRoot(); + + abstract Expression getResourcePidColumn(); + + abstract Subquery subqueryForTagNegation(); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryIndexTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryIndexTable.java new file mode 100644 index 00000000000..17ce007b8f8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryIndexTable.java @@ -0,0 +1,145 @@ +package ca.uhn.fhir.jpa.dao.predicate.querystack; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * 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.jpa.dao.predicate.SearchBuilderJoinEnum; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndex; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import org.apache.commons.lang3.Validate; + +import javax.persistence.criteria.AbstractQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; +import java.util.Date; +import java.util.List; + +public class QueryRootEntryIndexTable extends QueryRootEntry { + private final Subquery myQuery; + private Root myRoot; + private SearchBuilderJoinEnum myParamType; + private Expression myResourcePidColumn; + + public QueryRootEntryIndexTable(CriteriaBuilder theCriteriaBuilder, QueryRootEntry theParent) { + super(theCriteriaBuilder); + + AbstractQuery queryRoot = theParent.getQueryRoot(); + myQuery = queryRoot.subquery(Long.class); + } + + @Override + void orderBy(List theOrders) { + throw new IllegalStateException(); + } + + @Override + Expression getLastUpdatedColumn() { + return getRoot().get("myUpdated").as(Date.class); + } + + @Override + From createJoin(SearchBuilderJoinEnum theType, String theSearchParameterName) { + if (myParamType == null) { + switch (theType) { + case REFERENCE: + myRoot = myQuery.from(ResourceLink.class); + myResourcePidColumn = myRoot.get("mySourceResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.REFERENCE; + break; + case NUMBER: + myRoot = myQuery.from(ResourceIndexedSearchParamNumber.class); + myResourcePidColumn = myRoot.get("myResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.NUMBER; + break; + case DATE: + myRoot = myQuery.from(ResourceIndexedSearchParamDate.class); + myResourcePidColumn = myRoot.get("myResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.DATE; + break; + case STRING: + myRoot = myQuery.from(ResourceIndexedSearchParamString.class); + myResourcePidColumn = myRoot.get("myResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.STRING; + break; + case TOKEN: + myRoot = myQuery.from(ResourceIndexedSearchParamToken.class); + myResourcePidColumn = myRoot.get("myResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.TOKEN; + break; + case QUANTITY: + myRoot = myQuery.from(ResourceIndexedSearchParamQuantity.class); + myResourcePidColumn = myRoot.get("myResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.QUANTITY; + break; + case URI: + myRoot = myQuery.from(ResourceIndexedSearchParamUri.class); + myResourcePidColumn = myRoot.get("myResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.URI; + break; + case COORDS: + myRoot = myQuery.from(ResourceIndexedSearchParamCoords.class); + myResourcePidColumn = myRoot.get("myResourcePid").as(Long.class); + myParamType = SearchBuilderJoinEnum.COORDS; + break; + default: + throw new IllegalStateException(); + } + + myQuery.select(myResourcePidColumn); + } + + Validate.isTrue(theType == myParamType, "Wanted %s but got %s for %s", myParamType, theType, theSearchParameterName); + return (From) myRoot; + } + + @Override + AbstractQuery getQueryRoot() { + Validate.isTrue(myQuery != null); + return myQuery; + } + + @Override + Root getRoot() { + Validate.isTrue(myRoot != null); + return myRoot; + } + + @Override + public Expression getResourcePidColumn() { + Validate.isTrue(myResourcePidColumn != null); + return myResourcePidColumn; + } + + @Override + public Subquery subqueryForTagNegation() { + throw new IllegalStateException(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryResourceTable.java new file mode 100644 index 00000000000..77b9f02792e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryRootEntryResourceTable.java @@ -0,0 +1,205 @@ +package ca.uhn.fhir.jpa.dao.predicate.querystack; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * 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.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinEnum; +import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinKey; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; + +import javax.persistence.criteria.AbstractQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; +import java.util.Date; +import java.util.List; + +class QueryRootEntryResourceTable extends QueryRootEntry { + + private final CriteriaBuilder myCriteriaBuilder; + private final AbstractQuery myQuery; + private final SearchParameterMap mySearchParameterMap; + private final RequestPartitionId myRequestPartitionId; + private final String myResourceType; + + /** + * This method will ddd a predicate to make sure we only include non-deleted resources, and only include + * resources of the right type. + * + * If we have any joins to index tables, we get this behaviour already guaranteed so we don't + * need an explicit predicate for it. + */ + @Override + AbstractQuery pop() { + + if (!isHasImplicitTypeSelection()) { + if (mySearchParameterMap.getEverythingMode() == null) { + addPredicate(myCriteriaBuilder.equal(getRoot().get("myResourceType"), myResourceType)); + } + addPredicate(myCriteriaBuilder.isNull(getRoot().get("myDeleted"))); + if (!myRequestPartitionId.isAllPartitions()) { + if (myRequestPartitionId.getPartitionId() != null) { + addPredicate(myCriteriaBuilder.equal(getRoot().get("myPartitionIdValue").as(Integer.class), myRequestPartitionId.getPartitionId())); + } else { + addPredicate(myCriteriaBuilder.isNull(getRoot().get("myPartitionIdValue").as(Integer.class))); + } + } + } + + return super.pop(); + } + + private final Root myResourceTableRoot; + + /** + * Root query constructor + */ + QueryRootEntryResourceTable(CriteriaBuilder theCriteriaBuilder, boolean theCountQuery, SearchParameterMap theSearchParameterMap, String theResourceType, RequestPartitionId theRequestPartitionId) { + super(theCriteriaBuilder); + myCriteriaBuilder = theCriteriaBuilder; + mySearchParameterMap = theSearchParameterMap; + myRequestPartitionId = theRequestPartitionId; + myResourceType = theResourceType; + + CriteriaQuery query = myCriteriaBuilder.createQuery(Long.class); + myResourceTableRoot = query.from(ResourceTable.class); + + if (theCountQuery) { + query.multiselect(myCriteriaBuilder.countDistinct(myResourceTableRoot)); + } else { + query.multiselect(get("myId").as(Long.class)); + } + myQuery = query; + } + + /** + * Subquery constructor + */ + QueryRootEntryResourceTable(CriteriaBuilder theCriteriaBuilder, QueryRootEntry theParent, SearchParameterMap theSearchParameterMap, String theResourceType, RequestPartitionId theRequestPartitionId) { + super(theCriteriaBuilder); + myCriteriaBuilder = theCriteriaBuilder; + mySearchParameterMap = theSearchParameterMap; + myRequestPartitionId = theRequestPartitionId; + myResourceType = theResourceType; + + AbstractQuery queryRoot = theParent.getQueryRoot(); + Subquery query = queryRoot.subquery(Long.class); + myQuery = query; + myResourceTableRoot = myQuery.from(ResourceTable.class); + query.select(myResourceTableRoot.get("myId").as(Long.class)); + } + + @Override + void orderBy(List theOrders) { + assert myQuery instanceof CriteriaQuery; + + ((CriteriaQuery)myQuery).orderBy(theOrders); + } + + @Override + Expression getLastUpdatedColumn() { + return myResourceTableRoot.get("myUpdated").as(Date.class); + } + + @SuppressWarnings("unchecked") + @Override + From createJoin(SearchBuilderJoinEnum theType, String theSearchParameterName) { + Join join = null; + switch (theType) { + case DATE: + join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT); + break; + case NUMBER: + join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT); + break; + case QUANTITY: + join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT); + break; + case REFERENCE: + join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); + break; + case STRING: + join = myResourceTableRoot.join("myParamsString", JoinType.LEFT); + break; + case URI: + join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT); + break; + case TOKEN: + join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); + break; + case COORDS: + join = myResourceTableRoot.join("myParamsCoords", JoinType.LEFT); + break; + case HAS: + join = myResourceTableRoot.join("myResourceLinksAsTarget", JoinType.LEFT); + break; + case PROVENANCE: + join = myResourceTableRoot.join("myProvenance", JoinType.LEFT); + break; + case FORCED_ID: + join = myResourceTableRoot.join("myForcedId", JoinType.LEFT); + break; + case PRESENCE: + join = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); + break; + case COMPOSITE_UNIQUE: + join = myResourceTableRoot.join("myParamsCompositeStringUnique", JoinType.LEFT); + break; + case RESOURCE_TAGS: + join = myResourceTableRoot.join("myTags", JoinType.LEFT); + break; + + } + + SearchBuilderJoinKey key = new SearchBuilderJoinKey(theSearchParameterName, theType); + putIndex(key, join); + + return (From) join; + } + + @Override + AbstractQuery getQueryRoot() { + return myQuery; + } + + @Override + Root getRoot() { + return myResourceTableRoot; + } + + @Override + public Expression getResourcePidColumn() { + return myResourceTableRoot.get("myId").as(Long.class); + } + + @Override + public Subquery subqueryForTagNegation() { + return myQuery.subquery(Long.class); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryStack.java new file mode 100644 index 00000000000..9adad4a92de --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/querystack/QueryStack.java @@ -0,0 +1,284 @@ +package ca.uhn.fhir.jpa.dao.predicate.querystack; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * 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.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinEnum; +import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinKey; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import org.apache.commons.lang3.Validate; + +import javax.persistence.criteria.AbstractQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.Stack; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/** + * This class represents a SQL SELECT statement that is selecting for resource PIDs, ie. + * the RES_ID column on the HFJ_RESOURCE ({@link ca.uhn.fhir.jpa.model.entity.ResourceTable}) + * table. + *

+ * We add predicates (WHERE A=B) to it, and can join other tables to it as well. At the root of the query + * we are typically doing a select RES_ID from HFJ_RESOURCE where (....) and this class + * is used to build the where clause. In the case of subqueries though, we may be performing a + * select on a different table since many tables have a column with a FK dependency on RES_ID. + *

+ */ +public class QueryStack { + + private final Stack myQueryRootStack = new Stack<>(); + private final CriteriaBuilder myCriteriaBuilder; + private final SearchParameterMap mySearchParameterMap; + private final RequestPartitionId myRequestPartitionId; + private final String myResourceType; + + /** + * Constructor + */ + public QueryStack(CriteriaBuilder theCriteriaBuilder, String theResourceType, SearchParameterMap theSearchParameterMap, RequestPartitionId theRequestPartitionId) { + assert theCriteriaBuilder != null; + assert isNotBlank(theResourceType); + assert theSearchParameterMap != null; + assert theRequestPartitionId != null; + myCriteriaBuilder = theCriteriaBuilder; + mySearchParameterMap = theSearchParameterMap; + myRequestPartitionId = theRequestPartitionId; + myResourceType = theResourceType; + } + + /** + * Add a new select RES_ID from HFJ_RESOURCE to the stack. All predicates added to the {@literal QueryRootStack} + * will be added to this select clause until {@link #pop()} is called. + *

+ * This method must only be called when the stack is empty. + *

+ */ + public void pushResourceTableQuery() { + assert myQueryRootStack.isEmpty(); + myQueryRootStack.push(new QueryRootEntryResourceTable(myCriteriaBuilder, false, mySearchParameterMap, myResourceType, myRequestPartitionId)); + } + + /** + * Add a new select count(RES_ID) from HFJ_RESOURCE to the stack. All predicates added to the {@literal QueryRootStack} + * will be added to this select clause until {@link #pop()} is called. + *

+ * This method must only be called when the stack is empty. + *

+ */ + public void pushResourceTableCountQuery() { + assert myQueryRootStack.isEmpty(); + myQueryRootStack.push(new QueryRootEntryResourceTable(myCriteriaBuilder, true, mySearchParameterMap, myResourceType, myRequestPartitionId)); + } + + /** + * Add a new select RES_ID from HFJ_RESOURCE to the stack. All predicates added to the {@literal QueryRootStack} + * will be added to this select clause until {@link #pop()} is called. + *

+ * This method must only be called when the stack is NOT empty. + *

+ */ + public void pushResourceTableSubQuery(String theResourceType) { + assert !myQueryRootStack.isEmpty(); + myQueryRootStack.push(new QueryRootEntryResourceTable(myCriteriaBuilder, top(), mySearchParameterMap, theResourceType, myRequestPartitionId)); + } + + /** + * Add a new select RES_ID from (....) to the stack, where the specific table being selected on will be + * determined based on the first call to {@link #createJoin(SearchBuilderJoinEnum, String)}. All predicates added + * to the {@literal QueryRootStack} will be added to this select clause until {@link #pop()} is called. + *

+ * This method must only be called when the stack is NOT empty. + *

+ */ + public void pushIndexTableSubQuery() { + assert !myQueryRootStack.isEmpty(); + myQueryRootStack.push(new QueryRootEntryIndexTable(myCriteriaBuilder, top())); + } + + /** + * This method must be called once all predicates have been added + */ + public AbstractQuery pop() { + QueryRootEntry element = myQueryRootStack.pop(); + return element.pop(); + } + + /** + * Creates a new SQL join from the current select statement to another table, using the resource PID as the + * joining key + */ + public From createJoin(SearchBuilderJoinEnum theType, String theSearchParameterName) { + return top().createJoin(theType, theSearchParameterName); + } + + /** + * Returns a join that was previously created by a call to {@link #createJoin(SearchBuilderJoinEnum, String)}, + * if one exists for the given key. + */ + public Optional> getExistingJoin(SearchBuilderJoinKey theKey) { + return top().getIndexJoin(theKey); + } + + /** + * Gets an attribute (aka a column) from the current select statement. + * + * @param theAttributeName Must be the name of a java field for the entity/table being selected on + */ + public Path get(String theAttributeName) { + return top().get(theAttributeName); + } + + /** + * Adds a predicate to the current select statement + */ + public void addPredicate(Predicate thePredicate) { + top().addPredicate(thePredicate); + } + + /** + * Adds a predicate and marks it as having implicit type selection in it. In other words, call this method if a + * this predicate will ensure: + *
    + *
  • Only Resource PIDs for the correct resource type will be selected
  • + *
  • Only Resource PIDs for non-deleted resources will be selected
  • + *
+ * Setting this flag is a performance optimization, since it avoids the need for us to explicitly + * add predicates for the two conditions above. + */ + public void addPredicateWithImplicitTypeSelection(Predicate thePredicate) { + setHasImplicitTypeSelection(); + addPredicate(thePredicate); + } + + /** + * Adds predicates and marks them as having implicit type selection in it. In other words, call this method if a + * this predicate will ensure: + *
    + *
  • Only Resource PIDs for the correct resource type will be selected
  • + *
  • Only Resource PIDs for non-deleted resources will be selected
  • + *
+ * Setting this flag is a performance optimization, since it avoids the need for us to explicitly + * add predicates for the two conditions above. + */ + public void addPredicatesWithImplicitTypeSelection(List thePredicates) { + setHasImplicitTypeSelection(); + addPredicates(thePredicates); + } + + /** + * Adds predicate(s) to the current select statement + */ + public void addPredicates(List thePredicates) { + top().addPredicates(thePredicates); + } + + /** + * Clear all predicates from the current select statement + */ + public void clearPredicates() { + top().clearPredicates(); + } + + /** + * Fetch all the current predicates + *

+ * TODO This should really be package protected, but it is called externally in one spot - We need to clean that up + * at some point. + */ + public List getPredicates() { + return top().getPredicates(); + } + + private void setHasImplicitTypeSelection() { + top().setHasImplicitTypeSelection(true); + } + + /** + * @see #setHasImplicitTypeSelection() + */ + public void clearHasImplicitTypeSelection() { + top().setHasImplicitTypeSelection(false); + } + + public boolean isEmpty() { + return myQueryRootStack.isEmpty(); + } + + /** + * Add an SQL order by expression + */ + public void orderBy(List theOrders) { + top().orderBy(theOrders); + } + + /** + * Fetch the column for the current table root that corresponds to the resource's lastUpdated time + */ + public Expression getLastUpdatedColumn() { + return top().getLastUpdatedColumn(); + } + + /** + * Fetch the column for the current table root that corresponds to the resource's PID + */ + public Expression getResourcePidColumn() { + return top().getResourcePidColumn(); + } + + public Subquery subqueryForTagNegation() { + return top().subqueryForTagNegation(); + } + + private QueryRootEntry top() { + Validate.isTrue(!myQueryRootStack.empty()); + return myQueryRootStack.peek(); + } + + /** + * TODO This class should avoid leaking the internal query root, but we need to do so for how composite search params are + * currently implemented. These only half work in the first place so I'm not going to worry about the fact that + * they rely on a leaky abstraction right now.. But when we get around to implementing composites properly, + * let's not continue this. JA 2020-05-12 + */ + public Root getRootForComposite() { + return top().getRoot(); + } + + + /** + * Add a predicate that will never match any resources + */ + public Predicate addNeverMatchingPredicate() { + return top().addNeverMatchingPredicate(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java index da4738d3d10..354145e77e1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java @@ -1225,7 +1225,9 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { ourLog.info("Created patient, got it: {}", id); + myCaptureQueriesListener.clear(); myPatientDao.deleteByUrl("Patient?organization._profile=http://foo", mySrd); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertGone(id); myOrganizationDao.deleteByUrl("Organization?_profile=http://foo", mySrd); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java index 2eac85ff739..174c7cd327c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java @@ -434,8 +434,7 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest { IPreResourceAccessDetails accessDetails = theArgs.get(IPreResourceAccessDetails.class); - // FIXME: restore -// assertThat(accessDetails.size(), greaterThan(0)); + assertThat(accessDetails.size(), greaterThan(0)); List currentPassIds = new ArrayList<>(); for (int i = 0; i < accessDetails.size(); i++) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index e99884c753a..d85e44ca69e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -21,9 +21,11 @@ import org.junit.Test; import java.util.List; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.contains; public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class); @@ -457,6 +459,35 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } + + + @Test + public void testSearchOnChainedToken() { + Patient patient = new Patient(); + patient.setId("P"); + patient.addIdentifier().setSystem("sys").setValue("val"); + myPatientDao.update(patient); + + Observation obs = new Observation(); + obs.setId("O"); + obs.getSubject().setReference("Patient/P"); + myObservationDao.update(obs); + + SearchParameterMap map = SearchParameterMap.newSynchronous(Observation.SP_SUBJECT, new ReferenceParam("identifier", "sys|val")); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myObservationDao.search(map); + assertThat(toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder("Observation/O")); + + assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); + + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(1, StringUtils.countMatches(myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true).toLowerCase(), "join")); + } + + @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index 309f26ae770..c80a75cb55d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -405,24 +405,30 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { IBundleProvider results; map = new SearchParameterMap(); + map.setLoadSynchronous(true); map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "foo|bar").setChain("identifier")); + myCaptureQueriesListener.clear(); results = myEncounterDao.search(map); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(0); ids = toUnqualifiedVersionlessIdValues(results); assertThat(ids, hasItems(enc1Id, enc2Id)); map = new SearchParameterMap(); + map.setLoadSynchronous(true); map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject:Patient", "foo|bar").setChain("identifier")); results = myEncounterDao.search(map); ids = toUnqualifiedVersionlessIdValues(results); assertThat(ids, hasItems(enc1Id)); map = new SearchParameterMap(); + map.setLoadSynchronous(true); map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject:Group", "foo|bar").setChain("identifier")); results = myEncounterDao.search(map); ids = toUnqualifiedVersionlessIdValues(results); assertThat(ids, hasItems(enc2Id)); map = new SearchParameterMap(); + map.setLoadSynchronous(true); map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "04823543").setChain("identifier")); results = myEncounterDao.search(map); ids = toUnqualifiedVersionlessIdValues(results); @@ -3021,7 +3027,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { String searchQuery = queries.get(0); assertEquals(searchQuery, 3, StringUtils.countMatches(searchQuery.toUpperCase(), "HFJ_SPIDX_TOKEN")); - assertEquals(searchQuery, 5, StringUtils.countMatches(searchQuery.toUpperCase(), "LEFT OUTER JOIN")); + assertEquals(searchQuery, 4, StringUtils.countMatches(searchQuery.toUpperCase(), "LEFT OUTER JOIN")); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java index 10850b15144..81c8476c782 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java @@ -692,7 +692,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueries(); String selectQuery = myCaptureQueriesListener.getSelectQueries().get(1).getSql(true, true); - assertThat(selectQuery, containsString("HASH_VALUE in")); + assertThat(selectQuery, containsString("HASH_VALUE=")); assertThat(selectQuery, not(containsString("HASH_SYS"))); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java index b3f58ae11fb..751483876fa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java @@ -9,16 +9,20 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; import org.junit.After; import org.junit.AfterClass; import org.junit.Test; import java.util.List; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -320,6 +324,46 @@ public class FhirResourceDaoR4SortTest extends BaseJpaR4Test { assertThat(ids, contains("Patient/AA", "Patient/AB", "Patient/BA", "Patient/BB")); } + @Test + public void testSortWithChainedSearch() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + + Patient pCA = new Patient(); + pCA.setId("CA"); + pCA.addIdentifier().setSystem("PCA").setValue("PCA"); + myPatientDao.update(pCA); + + Observation obs1 = new Observation(); + obs1.setId("OBS1"); + obs1.getSubject().setReference("Patient/CA"); + obs1.setEffective(new DateTimeType("2000-01-01")); + myObservationDao.update(obs1); + + Observation obs2 = new Observation(); + obs2.setId("OBS2"); + obs2.getSubject().setReference("Patient/CA"); + obs2.setEffective(new DateTimeType("2000-02-02")); + myObservationDao.update(obs2); + + SearchParameterMap map; + List ids; + + runInTransaction(()->{ + ourLog.info("Dates:\n * {}", myResourceIndexedSearchParamDateDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * "))); + }); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Observation.SP_SUBJECT, new ReferenceParam("Patient", "identifier", "PCA|PCA")); + map.setSort(new SortSpec("date").setOrder(SortOrderEnum.DESC)); + myCaptureQueriesListener.clear(); + ids = toUnqualifiedVersionlessIdValues(myObservationDao.search(map)); + ourLog.info("IDS: {}", ids); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertThat(ids.toString(), ids, contains("Observation/OBS2", "Observation/OBS1")); + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java index eb8c7f80f19..6e15570adf9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -398,7 +398,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); assertThat(unformattedSql, stringContainsInOrder( "IDX_STRING='Patient?identifier=urn%7C111'", - "HASH_SYS_AND_VALUE in ('-3122824860083758210')" + "HASH_SYS_AND_VALUE='-3122824860083758210'" )); assertThat(unformattedSql, not(containsString(("RES_DELETED_AT")))); assertThat(unformattedSql, not(containsString(("RES_TYPE")))); @@ -535,7 +535,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); assertThat(unformattedSql, stringContainsInOrder( "IDX_STRING='ServiceRequest?identifier=sys%7C111&patient=Patient%2F" + ptId.getIdPart() + "&performer=Practitioner%2F" + practId.getIdPart() + "'", - "HASH_SYS_AND_VALUE in ('6795110643554413877')" + "HASH_SYS_AND_VALUE='6795110643554413877'" )); assertThat(unformattedSql, not(containsString(("RES_DELETED_AT")))); assertThat(unformattedSql, not(containsString(("RES_TYPE")))); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java index 86226fe4ca4..15911d306db 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java @@ -66,7 +66,7 @@ public class SearchWithInterceptorR4Test extends BaseJpaR4Test { String query = list.get(0).getSql(true, false); ourLog.info("Query: {}", query); - assertThat(query, containsString("HASH_SYS_AND_VALUE in ('3788488238034018567')")); + assertThat(query, containsString("HASH_SYS_AND_VALUE='3788488238034018567'")); } finally { myInterceptorRegistry.unregisterInterceptor(interceptor); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index 1cdb6f4ca37..2a2083226f4 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -639,4 +639,18 @@ public class SearchParameterMap implements Serializable { public int size() { return mySearchParameterMap.size(); } + + public static SearchParameterMap newSynchronous() { + SearchParameterMap retVal = new SearchParameterMap(); + retVal.setLoadSynchronous(true); + return retVal; + } + + public static SearchParameterMap newSynchronous(String theName, IQueryParameterType theParam) { + SearchParameterMap retVal = new SearchParameterMap(); + retVal.setLoadSynchronous(true); + retVal.add(theName, theParam); + return retVal; + } + }