Improve SQL IN(..) queries (#1214)
* First attempt, probably has compile issues * Fix qualified searches * Another test fix * More test fixes * Add changelog * Two more fixes * Revert SQL logging
This commit is contained in:
parent
28b4b812ac
commit
89b08cd627
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -64,7 +64,6 @@ import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||||
|
@ -92,6 +91,7 @@ import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.*;
|
import static org.apache.commons.lang3.StringUtils.*;
|
||||||
|
|
||||||
|
@ -105,6 +105,7 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
|
|
||||||
private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
|
private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class);
|
||||||
|
private static final int maxLoad = 800;
|
||||||
private static Long NO_MORE = -1L;
|
private static Long NO_MORE = -1L;
|
||||||
private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest;
|
private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest;
|
||||||
private static SearchParameterMap ourLastHandlerParamsForUnitTest;
|
private static SearchParameterMap ourLastHandlerParamsForUnitTest;
|
||||||
|
@ -112,15 +113,14 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
private static boolean ourTrackHandlersForUnitTest;
|
private static boolean ourTrackHandlersForUnitTest;
|
||||||
private final boolean myDontUseHashesForSearch;
|
private final boolean myDontUseHashesForSearch;
|
||||||
private final DaoConfig myDaoConfig;
|
private final DaoConfig myDaoConfig;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
protected IResourceTagDao myResourceTagDao;
|
protected IResourceTagDao myResourceTagDao;
|
||||||
|
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
|
||||||
|
protected EntityManager myEntityManager;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IResourceSearchViewDao myResourceSearchViewDao;
|
private IResourceSearchViewDao myResourceSearchViewDao;
|
||||||
@Autowired
|
@Autowired
|
||||||
private FhirContext myContext;
|
private FhirContext myContext;
|
||||||
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
|
|
||||||
protected EntityManager myEntityManager;
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IdHelperService myIdHelperService;
|
private IdHelperService myIdHelperService;
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
|
@ -137,7 +137,6 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
private MatchUrlService myMatchUrlService;
|
private MatchUrlService myMatchUrlService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
|
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
|
||||||
|
|
||||||
private List<Long> myAlsoIncludePids;
|
private List<Long> myAlsoIncludePids;
|
||||||
private CriteriaBuilder myBuilder;
|
private CriteriaBuilder myBuilder;
|
||||||
private BaseHapiFhirDao<?> myCallingDao;
|
private BaseHapiFhirDao<?> myCallingDao;
|
||||||
|
@ -823,6 +822,7 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
|
|
||||||
List<Predicate> codePredicates = new ArrayList<>();
|
List<Predicate> codePredicates = new ArrayList<>();
|
||||||
Join<ResourceTable, ResourceIndexedSearchParamToken> join = null;
|
Join<ResourceTable, ResourceIndexedSearchParamToken> join = null;
|
||||||
|
List<IQueryParameterType> tokens = new ArrayList<>();
|
||||||
for (IQueryParameterType nextOr : theList) {
|
for (IQueryParameterType nextOr : theList) {
|
||||||
|
|
||||||
if (nextOr instanceof TokenParam) {
|
if (nextOr instanceof TokenParam) {
|
||||||
|
@ -836,14 +836,17 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
if (join == null) {
|
if (join == null) {
|
||||||
join = createOrReuseJoin(JoinEnum.TOKEN, theParamName);
|
join = createOrReuseJoin(JoinEnum.TOKEN, theParamName);
|
||||||
}
|
}
|
||||||
Predicate singleCode = createPredicateToken(nextOr, theResourceName, theParamName, myBuilder, join);
|
|
||||||
codePredicates.add(singleCode);
|
tokens.add(nextOr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (codePredicates.isEmpty()) {
|
if (tokens.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Predicate> singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join);
|
||||||
|
codePredicates.addAll(singleCode);
|
||||||
|
|
||||||
Predicate spPredicate = myBuilder.or(toArray(codePredicates));
|
Predicate spPredicate = myBuilder.or(toArray(codePredicates));
|
||||||
myPredicates.add(spPredicate);
|
myPredicates.add(spPredicate);
|
||||||
}
|
}
|
||||||
|
@ -965,7 +968,9 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
}
|
}
|
||||||
case TOKEN: {
|
case TOKEN: {
|
||||||
From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> tokenJoin = theRoot.join("myParamsToken", JoinType.INNER);
|
From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> tokenJoin = theRoot.join("myParamsToken", JoinType.INNER);
|
||||||
retVal = createPredicateToken(leftValue, theResourceName, theParam.getName(), myBuilder, tokenJoin);
|
List<IQueryParameterType> tokens = Collections.singletonList(leftValue);
|
||||||
|
List<Predicate> tokenPredicates = createPredicateToken(tokens, theResourceName, theParam.getName(), myBuilder, tokenJoin);
|
||||||
|
retVal = myBuilder.and(tokenPredicates.toArray(new Predicate[0]));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DATE: {
|
case DATE: {
|
||||||
|
@ -1309,183 +1314,169 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
return orPredicates;
|
return orPredicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate createPredicateToken(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
|
private List<Predicate> createPredicateToken(Collection<IQueryParameterType> theParameters, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
|
||||||
From<?, ResourceIndexedSearchParamToken> theFrom) {
|
From<?, ResourceIndexedSearchParamToken> theFrom) {
|
||||||
String code;
|
final List<VersionIndependentConcept> codes = new ArrayList<>();
|
||||||
String system;
|
|
||||||
TokenParamModifier modifier = null;
|
TokenParamModifier modifier = null;
|
||||||
if (theParameter instanceof TokenParam) {
|
for (IQueryParameterType nextParameter : theParameters) {
|
||||||
TokenParam id = (TokenParam) theParameter;
|
|
||||||
system = id.getSystem();
|
String code;
|
||||||
code = (id.getValue());
|
String system;
|
||||||
modifier = id.getModifier();
|
if (nextParameter instanceof TokenParam) {
|
||||||
} else if (theParameter instanceof BaseIdentifierDt) {
|
TokenParam id = (TokenParam) nextParameter;
|
||||||
BaseIdentifierDt id = (BaseIdentifierDt) theParameter;
|
system = id.getSystem();
|
||||||
system = id.getSystemElement().getValueAsString();
|
code = (id.getValue());
|
||||||
code = (id.getValueElement().getValue());
|
modifier = id.getModifier();
|
||||||
} else if (theParameter instanceof BaseCodingDt) {
|
} else if (nextParameter instanceof BaseIdentifierDt) {
|
||||||
BaseCodingDt id = (BaseCodingDt) theParameter;
|
BaseIdentifierDt id = (BaseIdentifierDt) nextParameter;
|
||||||
system = id.getSystemElement().getValueAsString();
|
system = id.getSystemElement().getValueAsString();
|
||||||
code = (id.getCodeElement().getValue());
|
code = (id.getValueElement().getValue());
|
||||||
} else if (theParameter instanceof NumberParam) {
|
} else if (nextParameter instanceof BaseCodingDt) {
|
||||||
NumberParam number = (NumberParam) theParameter;
|
BaseCodingDt id = (BaseCodingDt) nextParameter;
|
||||||
system = null;
|
system = id.getSystemElement().getValueAsString();
|
||||||
code = number.getValueAsQueryToken(myContext);
|
code = (id.getCodeElement().getValue());
|
||||||
} else {
|
} else if (nextParameter instanceof NumberParam) {
|
||||||
throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass());
|
NumberParam number = (NumberParam) nextParameter;
|
||||||
|
system = null;
|
||||||
|
code = number.getValueAsQueryToken(myContext);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid token type: " + nextParameter.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
|
||||||
|
throw new InvalidRequestException(
|
||||||
|
"Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
|
||||||
|
throw new InvalidRequestException(
|
||||||
|
"Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Process token modifiers (:in, :below, :above)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (modifier == TokenParamModifier.IN) {
|
||||||
|
codes.addAll(myTerminologySvc.expandValueSet(code));
|
||||||
|
} else if (modifier == TokenParamModifier.ABOVE) {
|
||||||
|
system = determineSystemIfMissing(theParamName, code, system);
|
||||||
|
codes.addAll(myTerminologySvc.findCodesAbove(system, code));
|
||||||
|
} else if (modifier == TokenParamModifier.BELOW) {
|
||||||
|
system = determineSystemIfMissing(theParamName, code, system);
|
||||||
|
codes.addAll(myTerminologySvc.findCodesBelow(system, code));
|
||||||
|
} else {
|
||||||
|
codes.add(new VersionIndependentConcept(system, code));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
|
List<VersionIndependentConcept> sortedCodesList = codes
|
||||||
throw new InvalidRequestException(
|
.stream()
|
||||||
"Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
|
.filter(t -> t.getCode() != null || t.getSystem() != null)
|
||||||
}
|
.sorted()
|
||||||
|
.distinct()
|
||||||
if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
|
.collect(Collectors.toList());
|
||||||
throw new InvalidRequestException(
|
|
||||||
"Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Process token modifiers (:in, :below, :above)
|
|
||||||
*/
|
|
||||||
|
|
||||||
List<VersionIndependentConcept> codes;
|
|
||||||
if (modifier == TokenParamModifier.IN) {
|
|
||||||
codes = myTerminologySvc.expandValueSet(code);
|
|
||||||
} else if (modifier == TokenParamModifier.ABOVE) {
|
|
||||||
system = determineSystemIfMissing(theParamName, code, system);
|
|
||||||
codes = myTerminologySvc.findCodesAbove(system, code);
|
|
||||||
} else if (modifier == TokenParamModifier.BELOW) {
|
|
||||||
system = determineSystemIfMissing(theParamName, code, system);
|
|
||||||
codes = myTerminologySvc.findCodesBelow(system, code);
|
|
||||||
} else {
|
|
||||||
codes = Collections.singletonList(new VersionIndependentConcept(system, code));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codes.isEmpty()) {
|
if (codes.isEmpty()) {
|
||||||
// This will never match anything
|
// This will never match anything
|
||||||
return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false);
|
return Collections.singletonList(new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Predicate> retVal = new ArrayList<>();
|
||||||
|
|
||||||
|
// System only
|
||||||
|
List<VersionIndependentConcept> systemOnlyCodes = sortedCodesList.stream().filter(t -> isBlank(t.getCode())).collect(Collectors.toList());
|
||||||
|
if (!systemOnlyCodes.isEmpty()) {
|
||||||
|
retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemOnlyCodes, modifier, TokenModeEnum.SYSTEM_ONLY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code only
|
||||||
|
List<VersionIndependentConcept> codeOnlyCodes = sortedCodesList.stream().filter(t -> t.getSystem() == null).collect(Collectors.toList());
|
||||||
|
if (!codeOnlyCodes.isEmpty()) {
|
||||||
|
retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, codeOnlyCodes, modifier, TokenModeEnum.VALUE_ONLY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// System and code
|
||||||
|
List<VersionIndependentConcept> systemAndCodeCodes = sortedCodesList.stream().filter(t -> isNotBlank(t.getCode()) && t.getSystem() != null).collect(Collectors.toList());
|
||||||
|
if (!systemAndCodeCodes.isEmpty()) {
|
||||||
|
retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemAndCodeCodes, modifier, TokenModeEnum.SYSTEM_AND_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<VersionIndependentConcept> theTokens, TokenParamModifier theModifier, TokenModeEnum theTokenMode) {
|
||||||
if (myDontUseHashesForSearch) {
|
if (myDontUseHashesForSearch) {
|
||||||
ArrayList<Predicate> singleCodePredicates = new ArrayList<Predicate>();
|
final Path<String> systemExpression = theFrom.get("mySystem");
|
||||||
if (codes != null) {
|
final Path<String> valueExpression = theFrom.get("myValue");
|
||||||
|
|
||||||
List<Predicate> orPredicates = new ArrayList<Predicate>();
|
List<Predicate> orPredicates = new ArrayList<>();
|
||||||
Map<String, List<VersionIndependentConcept>> map = new HashMap<String, List<VersionIndependentConcept>>();
|
switch (theTokenMode) {
|
||||||
for (VersionIndependentConcept nextCode : codes) {
|
case SYSTEM_ONLY: {
|
||||||
List<VersionIndependentConcept> systemCodes = map.get(nextCode.getSystem());
|
List<String> systems = theTokens.stream().map(t -> t.getSystem()).collect(Collectors.toList());
|
||||||
if (null == systemCodes) {
|
Predicate orPredicate = systemExpression.in(systems);
|
||||||
systemCodes = new ArrayList<>();
|
orPredicates.add(orPredicate);
|
||||||
map.put(nextCode.getSystem(), systemCodes);
|
break;
|
||||||
}
|
|
||||||
systemCodes.add(nextCode);
|
|
||||||
}
|
}
|
||||||
// Use "in" in case of large numbers of codes due to param modifiers
|
case VALUE_ONLY:
|
||||||
final Path<String> systemExpression = theFrom.get("mySystem");
|
List<String> codes = theTokens.stream().map(t -> t.getCode()).collect(Collectors.toList());
|
||||||
final Path<String> valueExpression = theFrom.get("myValue");
|
Predicate orPredicate = valueExpression.in(codes);
|
||||||
for (Map.Entry<String, List<VersionIndependentConcept>> entry : map.entrySet()) {
|
orPredicates.add(orPredicate);
|
||||||
CriteriaBuilder.In<String> codePredicate = theBuilder.in(valueExpression);
|
break;
|
||||||
boolean haveAtLeastOneCode = false;
|
case SYSTEM_AND_VALUE:
|
||||||
for (VersionIndependentConcept nextCode : entry.getValue()) {
|
for (VersionIndependentConcept next : theTokens) {
|
||||||
if (isNotBlank(nextCode.getCode())) {
|
orPredicates.add(theBuilder.and(
|
||||||
codePredicate.value(nextCode.getCode());
|
toEqualOrIsNullPredicate(systemExpression, next.getSystem()),
|
||||||
haveAtLeastOneCode = true;
|
toEqualOrIsNullPredicate(valueExpression, next.getCode())
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
if (entry.getKey() != null) {
|
|
||||||
Predicate systemPredicate = theBuilder.equal(systemExpression, entry.getKey());
|
|
||||||
if (haveAtLeastOneCode) {
|
|
||||||
orPredicates.add(theBuilder.and(systemPredicate, codePredicate));
|
|
||||||
} else {
|
|
||||||
orPredicates.add(systemPredicate);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
orPredicates.add(codePredicate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0]));
|
|
||||||
if (modifier == TokenParamModifier.NOT) {
|
|
||||||
or = theBuilder.not(or);
|
|
||||||
}
|
|
||||||
singleCodePredicates.add(or);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Ok, this is a normal query
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(system)) {
|
|
||||||
if (modifier != null && modifier == TokenParamModifier.NOT) {
|
|
||||||
singleCodePredicates.add(theBuilder.notEqual(theFrom.get("mySystem"), system));
|
|
||||||
} else {
|
|
||||||
singleCodePredicates.add(theBuilder.equal(theFrom.get("mySystem"), system));
|
|
||||||
}
|
|
||||||
} else if (system == null) {
|
|
||||||
// don't check the system
|
|
||||||
} else {
|
|
||||||
// If the system is "", we only match on null systems
|
|
||||||
singleCodePredicates.add(theBuilder.isNull(theFrom.get("mySystem")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(code)) {
|
|
||||||
if (modifier != null && modifier == TokenParamModifier.NOT) {
|
|
||||||
singleCodePredicates.add(theBuilder.notEqual(theFrom.get("myValue"), code));
|
|
||||||
} else {
|
|
||||||
singleCodePredicates.add(theBuilder.equal(theFrom.get("myValue"), code));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
* As of HAPI FHIR 1.5, if the client searched for a token with a system but no specified value this means to
|
|
||||||
* match all tokens with the given value.
|
|
||||||
*
|
|
||||||
* I'm not sure I agree with this, but hey.. FHIR-I voted and this was the result :)
|
|
||||||
*/
|
|
||||||
// singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate singleCode = theBuilder.and(toArray(singleCodePredicates));
|
Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0]));
|
||||||
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode);
|
if (theModifier == TokenParamModifier.NOT) {
|
||||||
}
|
or = theBuilder.not(or);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, or);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Note: A null system value means "match any system", but
|
* Note: A null system value means "match any system", but
|
||||||
* an empty-string system value means "match values that
|
* an empty-string system value means "match values that
|
||||||
* explicitly have no system".
|
* explicitly have no system".
|
||||||
*/
|
*/
|
||||||
boolean haveSystem = codes.get(0).getSystem() != null;
|
|
||||||
boolean haveCode = isNotBlank(codes.get(0).getCode());
|
|
||||||
Expression<Long> hashField;
|
Expression<Long> hashField;
|
||||||
if (!haveSystem && !haveCode) {
|
List<Long> values;
|
||||||
// If we have neither, this isn't actually an expression so
|
switch (theTokenMode) {
|
||||||
// just return 1=1
|
case SYSTEM_ONLY:
|
||||||
return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, true);
|
hashField = theFrom.get("myHashSystem").as(Long.class);
|
||||||
} else if (haveSystem && haveCode) {
|
values = theTokens
|
||||||
hashField = theFrom.get("myHashSystemAndValue").as(Long.class);
|
.stream()
|
||||||
} else if (haveSystem) {
|
.map(t -> ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, t.getSystem()))
|
||||||
hashField = theFrom.get("myHashSystem").as(Long.class);
|
.collect(Collectors.toList());
|
||||||
} else {
|
break;
|
||||||
hashField = theFrom.get("myHashValue").as(Long.class);
|
case VALUE_ONLY:
|
||||||
}
|
hashField = theFrom.get("myHashValue").as(Long.class);
|
||||||
|
values = theTokens
|
||||||
List<Long> values = new ArrayList<>(codes.size());
|
.stream()
|
||||||
for (VersionIndependentConcept next : codes) {
|
.map(t -> ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, t.getCode()))
|
||||||
if (haveSystem && haveCode) {
|
.collect(Collectors.toList());
|
||||||
values.add(ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, next.getSystem(), next.getCode()));
|
break;
|
||||||
} else if (haveSystem) {
|
case SYSTEM_AND_VALUE:
|
||||||
values.add(ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, next.getSystem()));
|
default:
|
||||||
} else {
|
hashField = theFrom.get("myHashSystemAndValue").as(Long.class);
|
||||||
values.add(ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, next.getCode()));
|
values = theTokens
|
||||||
}
|
.stream()
|
||||||
|
.map(t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, t.getSystem(), t.getCode()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate predicate = hashField.in(values);
|
Predicate predicate = hashField.in(values);
|
||||||
if (modifier == TokenParamModifier.NOT) {
|
if (theModifier == TokenParamModifier.NOT) {
|
||||||
Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName));
|
Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName));
|
||||||
Predicate disjunctionPredicate = theBuilder.not(predicate);
|
Predicate disjunctionPredicate = theBuilder.not(predicate);
|
||||||
predicate = theBuilder.and(identityPredicate, disjunctionPredicate);
|
predicate = theBuilder.and(identityPredicate, disjunctionPredicate);
|
||||||
|
@ -1493,6 +1484,13 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
return predicate;
|
return predicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private <T> Expression<Boolean> toEqualOrIsNullPredicate(Path<T> theExpression, T theCode) {
|
||||||
|
if (theCode == null) {
|
||||||
|
return myBuilder.isNull(theExpression);
|
||||||
|
}
|
||||||
|
return myBuilder.equal(theExpression, theCode);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid) {
|
public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid) {
|
||||||
myParams = theParams;
|
myParams = theParams;
|
||||||
|
@ -1966,8 +1964,6 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
return tagMap;
|
return tagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int maxLoad = 800;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theIncludedPids, boolean theForHistoryOperation,
|
public void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theIncludedPids, boolean theForHistoryOperation,
|
||||||
EntityManager entityManager, FhirContext context, IDao theDao) {
|
EntityManager entityManager, FhirContext context, IDao theDao) {
|
||||||
|
@ -2319,6 +2315,35 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
return qp;
|
return qp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
|
||||||
|
String theResourceType) {
|
||||||
|
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
|
||||||
|
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
|
||||||
|
List<String> path = param.getPathsSplit();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SearchParameters can declare paths on multiple resources
|
||||||
|
* types. Here we only want the ones that actually apply.
|
||||||
|
*/
|
||||||
|
path = new ArrayList<>(path);
|
||||||
|
|
||||||
|
for (int i = 0; i < path.size(); i++) {
|
||||||
|
String nextPath = trim(path.get(i));
|
||||||
|
if (!nextPath.contains(theResourceType + ".")) {
|
||||||
|
path.remove(i);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return theFrom.get("mySourcePath").in(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TokenModeEnum {
|
||||||
|
SYSTEM_ONLY,
|
||||||
|
VALUE_ONLY,
|
||||||
|
SYSTEM_AND_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
public enum HandlerTypeEnum {
|
public enum HandlerTypeEnum {
|
||||||
UNIQUE_INDEX, STANDARD_QUERY
|
UNIQUE_INDEX, STANDARD_QUERY
|
||||||
}
|
}
|
||||||
|
@ -2673,29 +2698,6 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
return likeExpression.replace("%", "[%]") + "%";
|
return likeExpression.replace("%", "[%]") + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
|
|
||||||
String theResourceType) {
|
|
||||||
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
|
|
||||||
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
|
|
||||||
List<String> path = param.getPathsSplit();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* SearchParameters can declare paths on multiple resources
|
|
||||||
* types. Here we only want the ones that actually apply.
|
|
||||||
*/
|
|
||||||
path = new ArrayList<>(path);
|
|
||||||
|
|
||||||
for (int i = 0; i < path.size(); i++) {
|
|
||||||
String nextPath = trim(path.get(i));
|
|
||||||
if (!nextPath.contains(theResourceType + ".")) {
|
|
||||||
path.remove(i);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return theFrom.get("mySourcePath").in(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) {
|
private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) {
|
||||||
if (thePids.isEmpty()) {
|
if (thePids.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
|
@ -468,9 +468,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
|
||||||
ArrayList<VersionIndependentConcept> retVal = new ArrayList<>();
|
ArrayList<VersionIndependentConcept> retVal = new ArrayList<>();
|
||||||
for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent nextContains : expandedR4.getContains()) {
|
for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent nextContains : expandedR4.getContains()) {
|
||||||
retVal.add(
|
retVal.add(
|
||||||
new VersionIndependentConcept()
|
new VersionIndependentConcept(nextContains.getSystem(), nextContains.getCode()));
|
||||||
.setSystem(nextContains.getSystem())
|
|
||||||
.setCode(nextContains.getCode()));
|
|
||||||
}
|
}
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term;
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -20,42 +20,65 @@ package ca.uhn.fhir.jpa.term;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class VersionIndependentConcept {
|
import org.apache.commons.lang3.builder.CompareToBuilder;
|
||||||
|
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||||
|
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||||
|
|
||||||
private String mySystem;
|
public class VersionIndependentConcept implements Comparable<VersionIndependentConcept> {
|
||||||
private String myCode;
|
|
||||||
|
|
||||||
/**
|
private final String mySystem;
|
||||||
* Constructor
|
private final String myCode;
|
||||||
*/
|
private int myHashCode;
|
||||||
public VersionIndependentConcept() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
public VersionIndependentConcept(String theSystem, String theCode) {
|
public VersionIndependentConcept(String theSystem, String theCode) {
|
||||||
setSystem(theSystem);
|
mySystem = theSystem;
|
||||||
setCode(theCode);
|
myCode = theCode;
|
||||||
|
myHashCode = new HashCodeBuilder(17, 37)
|
||||||
|
.append(mySystem)
|
||||||
|
.append(myCode)
|
||||||
|
.toHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSystem() {
|
public String getSystem() {
|
||||||
return mySystem;
|
return mySystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VersionIndependentConcept setSystem(String theSystem) {
|
|
||||||
mySystem = theSystem;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCode() {
|
public String getCode() {
|
||||||
return myCode;
|
return myCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VersionIndependentConcept setCode(String theCode) {
|
@Override
|
||||||
myCode = theCode;
|
public boolean equals(Object theO) {
|
||||||
return this;
|
if (this == theO) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theO == null || getClass() != theO.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VersionIndependentConcept that = (VersionIndependentConcept) theO;
|
||||||
|
|
||||||
|
return new EqualsBuilder()
|
||||||
|
.append(mySystem, that.mySystem)
|
||||||
|
.append(myCode, that.myCode)
|
||||||
|
.isEquals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return myHashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(VersionIndependentConcept theOther) {
|
||||||
|
CompareToBuilder b = new CompareToBuilder();
|
||||||
|
b.append(mySystem, theOther.getSystem());
|
||||||
|
b.append(myCode, theOther.getCode());
|
||||||
|
return b.toComparison();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class CaptureQueriesListener implements ProxyDataSourceBuilder.SingleQuer
|
||||||
List<String> nextParams = new ArrayList<>(myParams);
|
List<String> nextParams = new ArrayList<>(myParams);
|
||||||
while (retVal.contains("?") && nextParams.size() > 0) {
|
while (retVal.contains("?") && nextParams.size() > 0) {
|
||||||
int idx = retVal.indexOf("?");
|
int idx = retVal.indexOf("?");
|
||||||
retVal = retVal.substring(0, idx) + nextParams.remove(0) + retVal.substring(idx + 1);
|
retVal = retVal.substring(0, idx) + "'" + nextParams.remove(0) + "'" + retVal.substring(idx + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.config;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
|
||||||
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||||
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
|
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
|
||||||
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
|
|
||||||
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
|
||||||
import org.apache.commons.dbcp2.BasicDataSource;
|
import org.apache.commons.dbcp2.BasicDataSource;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
|
@ -2030,7 +2030,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSearchStringParamWithLike() throws Exception {
|
public void testSearchStringParamWithLike() {
|
||||||
SearchParameterMap map = new SearchParameterMap();
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
map.add(Patient.SP_FAMILY, new StringOrListParam().addOr(new StringParam("AAA")).addOr(new StringParam("BBB")));
|
map.add(Patient.SP_FAMILY, new StringOrListParam().addOr(new StringParam("AAA")).addOr(new StringParam("BBB")));
|
||||||
map.setLoadSynchronous(true);
|
map.setLoadSynchronous(true);
|
||||||
|
@ -2047,6 +2047,95 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchTokenListLike() {
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("FOO");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("BAR");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("BAZ");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
CaptureQueriesListener.clear();
|
||||||
|
|
||||||
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
|
map.add(Patient.SP_IDENTIFIER, new TokenOrListParam().addOr(new TokenParam("FOO")).addOr(new TokenParam("BAR")));
|
||||||
|
map.setLoadSynchronous(true);
|
||||||
|
IBundleProvider search = myPatientDao.search(map);
|
||||||
|
|
||||||
|
List<String> queries = CaptureQueriesListener
|
||||||
|
.getLastNQueries()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getSql(true, true))
|
||||||
|
.filter(t -> t.contains("select"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
String resultingQueryFormatted = queries.get(queries.size() - 1);
|
||||||
|
ourLog.info("Resulting query formatted:\n{}", resultingQueryFormatted);
|
||||||
|
|
||||||
|
queries = CaptureQueriesListener
|
||||||
|
.getLastNQueries()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getSql(true, false))
|
||||||
|
.filter(t -> t.contains("select"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
String resultingQueryNotFormatted = queries.get(queries.size() - 1);
|
||||||
|
|
||||||
|
assertEquals(resultingQueryFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "HASH_VALUE"));
|
||||||
|
assertThat(resultingQueryNotFormatted, containsString("HASH_VALUE in ('3140583648400062149' , '4929264259256651518')"));
|
||||||
|
|
||||||
|
// Ensure that the search actually worked
|
||||||
|
assertEquals(2, search.size().intValue());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchTokenListWithMixedCombinations() {
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("FOO");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("BAR");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SAS").setValue("BAZ");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
CaptureQueriesListener.clear();
|
||||||
|
|
||||||
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
|
map.add(Patient.SP_IDENTIFIER, new TokenOrListParam().addOr(new TokenParam("SAS", null)).addOr(new TokenParam("FOO")).addOr(new TokenParam("BAR")));
|
||||||
|
map.setLoadSynchronous(true);
|
||||||
|
IBundleProvider search = myPatientDao.search(map);
|
||||||
|
|
||||||
|
List<String> queries = CaptureQueriesListener
|
||||||
|
.getLastNQueries()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getSql(true, true))
|
||||||
|
.filter(t -> t.contains("select"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
String resultingQueryFormatted = queries.get(queries.size() - 1);
|
||||||
|
ourLog.info("Resulting query formatted:\n{}", resultingQueryFormatted);
|
||||||
|
|
||||||
|
queries = CaptureQueriesListener
|
||||||
|
.getLastNQueries()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getSql(true, false))
|
||||||
|
.filter(t -> t.contains("select"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
String resultingQueryNotFormatted = queries.get(queries.size() - 1);
|
||||||
|
|
||||||
|
assertEquals(resultingQueryFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "HASH_VALUE"));
|
||||||
|
assertThat(resultingQueryNotFormatted, containsString("HASH_VALUE in ('3140583648400062149' , '4929264259256651518')"));
|
||||||
|
|
||||||
|
// Ensure that the search actually worked
|
||||||
|
assertEquals(3, search.size().intValue());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSearchStringParam() throws Exception {
|
public void testSearchStringParam() throws Exception {
|
||||||
IIdType pid1;
|
IIdType pid1;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package ca.uhn.fhir.jpa.dao.r4;
|
package ca.uhn.fhir.jpa.dao.r4;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.config.CaptureQueriesListener;
|
||||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.*;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
|
||||||
import ca.uhn.fhir.jpa.model.entity.*;
|
|
||||||
import ca.uhn.fhir.jpa.util.TestUtil;
|
import ca.uhn.fhir.jpa.util.TestUtil;
|
||||||
import ca.uhn.fhir.model.api.Include;
|
import ca.uhn.fhir.model.api.Include;
|
||||||
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
||||||
|
@ -39,6 +40,7 @@ import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
@ -386,6 +388,51 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchTokenListLike() {
|
||||||
|
|
||||||
|
Patient p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("FOO");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("BAR");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
p = new Patient();
|
||||||
|
p.addIdentifier().setSystem("SYS").setValue("BAZ");
|
||||||
|
myPatientDao.create(p);
|
||||||
|
CaptureQueriesListener.clear();
|
||||||
|
|
||||||
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
|
map.add(Patient.SP_IDENTIFIER, new TokenOrListParam().addOr(new TokenParam("FOO")).addOr(new TokenParam("BAR")));
|
||||||
|
map.setLoadSynchronous(true);
|
||||||
|
IBundleProvider search = myPatientDao.search(map);
|
||||||
|
|
||||||
|
List<String> queries = CaptureQueriesListener
|
||||||
|
.getLastNQueries()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getSql(true, true))
|
||||||
|
.filter(t -> t.contains("select"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
String resultingQueryFormatted = queries.get(queries.size() - 1);
|
||||||
|
ourLog.info("Resulting query formatted:\n{}", resultingQueryFormatted);
|
||||||
|
|
||||||
|
queries = CaptureQueriesListener
|
||||||
|
.getLastNQueries()
|
||||||
|
.stream()
|
||||||
|
.map(t -> t.getSql(true, false))
|
||||||
|
.filter(t -> t.contains("select"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
String resultingQueryNotFormatted = queries.get(queries.size() - 1);
|
||||||
|
|
||||||
|
assertEquals(resultingQueryFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "SP_VALUE"));
|
||||||
|
assertThat(resultingQueryNotFormatted, containsString("SP_VALUE in ('BAR' , 'FOO')"));
|
||||||
|
|
||||||
|
// Ensure that the search actually worked
|
||||||
|
assertEquals(2, search.size().intValue());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHasParameter() {
|
public void testHasParameter() {
|
||||||
IIdType pid0;
|
IIdType pid0;
|
||||||
|
@ -616,7 +663,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
|
||||||
expect1.setResource(resource);
|
expect1.setResource(resource);
|
||||||
expect1.calculateHashes();
|
expect1.calculateHashes();
|
||||||
|
|
||||||
assertThat("Got: \"" + results.toString()+"\"", results, containsInAnyOrder(expect0, expect1));
|
assertThat("Got: \"" + results.toString() + "\"", results, containsInAnyOrder(expect0, expect1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1059,7 +1106,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
|
||||||
QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1");
|
QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1");
|
||||||
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1);
|
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1);
|
||||||
IBundleProvider result = myObservationDao.search(map);
|
IBundleProvider result = myObservationDao.search(map);
|
||||||
assertThat("Got: "+ toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue()));
|
assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1091,7 +1138,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
|
||||||
CompositeParam<TokenParam, QuantityParam> val = new CompositeParam<>(v0, v1);
|
CompositeParam<TokenParam, QuantityParam> val = new CompositeParam<>(v0, v1);
|
||||||
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, val);
|
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, val);
|
||||||
IBundleProvider result = myObservationDao.search(map);
|
IBundleProvider result = myObservationDao.search(map);
|
||||||
assertThat("Got: "+ toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id2.getValue()));
|
assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id2.getValue()));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
TokenParam v0 = new TokenParam("http://foo", "code1");
|
TokenParam v0 = new TokenParam("http://foo", "code1");
|
||||||
|
@ -2245,18 +2292,25 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
|
||||||
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
|
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
|
||||||
myPatientDao.create(patient, mySrd);
|
myPatientDao.create(patient, mySrd);
|
||||||
|
|
||||||
|
patient = new Patient();
|
||||||
|
patient.addIdentifier().setSystem("urn:system").setValue(null);
|
||||||
|
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
|
||||||
|
myPatientDao.create(patient, mySrd);
|
||||||
|
|
||||||
patient = new Patient();
|
patient = new Patient();
|
||||||
patient.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002");
|
patient.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002");
|
||||||
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
|
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
|
||||||
myPatientDao.create(patient, mySrd);
|
myPatientDao.create(patient, mySrd);
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// Match system="urn:system" and value = *
|
||||||
SearchParameterMap map = new SearchParameterMap();
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null));
|
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null));
|
||||||
IBundleProvider retrieved = myPatientDao.search(map);
|
IBundleProvider retrieved = myPatientDao.search(map);
|
||||||
assertEquals(2, retrieved.size().intValue());
|
assertEquals(2, retrieved.size().intValue());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
// Match system="urn:system" and value = ""
|
||||||
SearchParameterMap map = new SearchParameterMap();
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", ""));
|
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", ""));
|
||||||
IBundleProvider retrieved = myPatientDao.search(map);
|
IBundleProvider retrieved = myPatientDao.search(map);
|
||||||
|
@ -3288,7 +3342,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
|
||||||
"Observation/YES21",
|
"Observation/YES21",
|
||||||
"Observation/YES22",
|
"Observation/YES22",
|
||||||
"Observation/YES23"
|
"Observation/YES23"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createObservationWithEffective(String theId, String theEffective) {
|
private void createObservationWithEffective(String theId, String theEffective) {
|
||||||
|
|
|
@ -48,6 +48,10 @@
|
||||||
using the string name of the datatype (e.g. "dateTime") in order to help
|
using the string name of the datatype (e.g. "dateTime") in order to help
|
||||||
building Parameters resources in a version-independent way.
|
building Parameters resources in a version-independent way.
|
||||||
</action>
|
</action>
|
||||||
|
<action type="add">
|
||||||
|
The JPA query builder has been optimized to take better advantage of SQL IN (..) expressions
|
||||||
|
when performing token searches with multiple OR values.
|
||||||
|
</action>
|
||||||
</release>
|
</release>
|
||||||
<release version="3.7.0" date="2019-02-06" description="Gale">
|
<release version="3.7.0" date="2019-02-06" description="Gale">
|
||||||
<action type="add">
|
<action type="add">
|
||||||
|
|
Loading…
Reference in New Issue