Combo permutations
This commit is contained in:
parent
315a05eeb8
commit
9143f5995b
|
@ -21,6 +21,7 @@ You may use the following command to get detailed help on the options:
|
||||||
Note the arguments:
|
Note the arguments:
|
||||||
|
|
||||||
* `-d [dialect]` – This indicates the database dialect to use. See the detailed help for a list of options
|
* `-d [dialect]` – This indicates the database dialect to use. See the detailed help for a list of options
|
||||||
|
* `--enable-heavyweight-migrations` – If this flag is set, additional migration tasks will be executed that are considered unnecessary to execute on a database with a significant amount of data loaded. This option is not generally necessary.
|
||||||
|
|
||||||
# Oracle Support
|
# Oracle Support
|
||||||
|
|
||||||
|
|
|
@ -2779,16 +2779,16 @@ public class QueryStack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
public void addPredicateCompositeUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) {
|
||||||
ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder();
|
ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder();
|
||||||
Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexString);
|
Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexStrings);
|
||||||
mySqlBuilder.addPredicate(predicate);
|
mySqlBuilder.addPredicate(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addPredicateCompositeNonUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
public void addPredicateCompositeNonUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) {
|
||||||
ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder =
|
ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder =
|
||||||
mySqlBuilder.addComboNonUniquePredicateBuilder();
|
mySqlBuilder.addComboNonUniquePredicateBuilder();
|
||||||
Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexString);
|
Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexStrings);
|
||||||
mySqlBuilder.addPredicate(predicate);
|
mySqlBuilder.addPredicate(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||||
import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
|
import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
|
||||||
import ca.uhn.fhir.jpa.util.BaseIterator;
|
import ca.uhn.fhir.jpa.util.BaseIterator;
|
||||||
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
|
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
|
||||||
|
import ca.uhn.fhir.jpa.util.PermutationBuilder;
|
||||||
import ca.uhn.fhir.jpa.util.QueryChunker;
|
import ca.uhn.fhir.jpa.util.QueryChunker;
|
||||||
import ca.uhn.fhir.jpa.util.SqlQueryList;
|
import ca.uhn.fhir.jpa.util.SqlQueryList;
|
||||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||||
|
@ -83,6 +84,7 @@ import ca.uhn.fhir.rest.api.SortOrderEnum;
|
||||||
import ca.uhn.fhir.rest.api.SortSpec;
|
import ca.uhn.fhir.rest.api.SortSpec;
|
||||||
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
|
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
|
||||||
import ca.uhn.fhir.rest.param.DateParam;
|
import ca.uhn.fhir.rest.param.DateParam;
|
||||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||||
import ca.uhn.fhir.rest.param.ParameterUtil;
|
import ca.uhn.fhir.rest.param.ParameterUtil;
|
||||||
|
@ -125,6 +127,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -137,6 +140,7 @@ import java.util.stream.Collectors;
|
||||||
import static ca.uhn.fhir.jpa.model.util.JpaConstants.UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE;
|
import static ca.uhn.fhir.jpa.model.util.JpaConstants.UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE;
|
||||||
import static ca.uhn.fhir.jpa.search.builder.QueryStack.LOCATION_POSITION;
|
import static ca.uhn.fhir.jpa.search.builder.QueryStack.LOCATION_POSITION;
|
||||||
import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with;
|
import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
@ -1910,10 +1914,12 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
||||||
if (comboParam != null) {
|
if (comboParam != null) {
|
||||||
Collections.sort(comboParamNames);
|
Collections.sort(comboParamNames);
|
||||||
|
|
||||||
if (!validateParamValuesAreValidForComboParam(theParams, comboParamNames)) {
|
if (!validateParamValuesAreValidForComboParam(theRequest, theParams, comboParamNames)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: abort if too many permutations
|
||||||
|
|
||||||
applyComboSearchParam(theQueryStack, theParams, theRequest, comboParamNames, comboParam);
|
applyComboSearchParam(theQueryStack, theParams, theRequest, comboParamNames, comboParam);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1927,101 +1933,111 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
||||||
// Since we're going to remove elements below
|
// Since we're going to remove elements below
|
||||||
theParams.values().forEach(this::ensureSubListsAreWritable);
|
theParams.values().forEach(this::ensureSubListsAreWritable);
|
||||||
|
|
||||||
StringBuilder theSearchBuilder = new StringBuilder();
|
List<List<IQueryParameterType>> inputs = new ArrayList<>();
|
||||||
theSearchBuilder.append(myResourceName);
|
|
||||||
theSearchBuilder.append("?");
|
|
||||||
|
|
||||||
boolean first = true;
|
|
||||||
for (String nextParamName : theComboParamNames) {
|
for (String nextParamName : theComboParamNames) {
|
||||||
List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
|
List<IQueryParameterType> nextValues = theParams.get(nextParamName).remove(0);
|
||||||
|
inputs.add(nextValues);
|
||||||
|
}
|
||||||
|
|
||||||
// This should never happen, but this safety check was added along the way and
|
List<List<IQueryParameterType>> inputPermutations = PermutationBuilder.calculatePermutations(inputs);
|
||||||
// presumably must save us in some specific race condition. I am preserving it
|
List<String> indexStrings = new ArrayList<>(PermutationBuilder.calculatePermutationCount(inputs));
|
||||||
// in a refactor of this code base. 20240429
|
for (List<IQueryParameterType> nextPermutation : inputPermutations) {
|
||||||
if (nextValues.isEmpty()) {
|
|
||||||
ourLog.error(
|
|
||||||
"query parameter {} is unexpectedly empty. Encountered while considering {} index for {}",
|
|
||||||
nextParamName,
|
|
||||||
theComboParam.getName(),
|
|
||||||
theRequest.getCompleteUrl());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<? extends IQueryParameterType> nextAnd = nextValues.remove(0);
|
StringBuilder searchStringBuilder = new StringBuilder();
|
||||||
IQueryParameterType nextOr = nextAnd.remove(0);
|
searchStringBuilder.append(myResourceName);
|
||||||
String nextOrValue = nextOr.getValueAsQueryToken(myContext);
|
searchStringBuilder.append("?");
|
||||||
|
|
||||||
RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName);
|
boolean first = true;
|
||||||
if (theComboParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE) {
|
for (int paramIndex = 0; paramIndex < theComboParamNames.size(); paramIndex++) {
|
||||||
if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.STRING) {
|
|
||||||
nextOrValue = StringUtil.normalizeStringForSearchIndexing(nextOrValue);
|
String nextParamName = theComboParamNames.get(paramIndex);
|
||||||
|
IQueryParameterType nextOr = nextPermutation.get(paramIndex);
|
||||||
|
String nextOrValue = nextOr.getValueAsQueryToken(myContext);
|
||||||
|
|
||||||
|
RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName);
|
||||||
|
if (theComboParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE) {
|
||||||
|
if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.STRING) {
|
||||||
|
nextOrValue = StringUtil.normalizeStringForSearchIndexing(nextOrValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
searchStringBuilder.append('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
nextParamName = UrlUtil.escapeUrlParam(nextParamName);
|
||||||
|
nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
|
||||||
|
|
||||||
|
searchStringBuilder.append(nextParamName).append('=').append(nextOrValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (first) {
|
String indexString = searchStringBuilder.toString();
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
theSearchBuilder.append('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
nextParamName = UrlUtil.escapeUrlParam(nextParamName);
|
|
||||||
nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
|
|
||||||
|
|
||||||
theSearchBuilder.append(nextParamName).append('=').append(nextOrValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (theSearchBuilder != null) {
|
|
||||||
String indexString = theSearchBuilder.toString();
|
|
||||||
ourLog.debug(
|
ourLog.debug(
|
||||||
"Checking for {} combo index for query: {}", theComboParam.getComboSearchParamType(), indexString);
|
"Checking for {} combo index for query: {}", theComboParam.getComboSearchParamType(), indexString);
|
||||||
|
|
||||||
// Interceptor broadcast: JPA_PERFTRACE_INFO
|
indexStrings.add(indexString);
|
||||||
StorageProcessingMessage msg = new StorageProcessingMessage()
|
|
||||||
.setMessage("Using " + theComboParam.getComboSearchParamType() + " index for query for search: "
|
|
||||||
+ indexString);
|
|
||||||
HookParams params = new HookParams()
|
|
||||||
.add(RequestDetails.class, theRequest)
|
|
||||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
|
||||||
.add(StorageProcessingMessage.class, msg);
|
|
||||||
CompositeInterceptorBroadcaster.doCallHooks(
|
|
||||||
myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
|
|
||||||
|
|
||||||
switch (theComboParam.getComboSearchParamType()) {
|
|
||||||
case UNIQUE:
|
|
||||||
theQueryStack.addPredicateCompositeUnique(indexString, myRequestPartitionId);
|
|
||||||
break;
|
|
||||||
case NON_UNIQUE:
|
|
||||||
theQueryStack.addPredicateCompositeNonUnique(indexString, myRequestPartitionId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any empty parameters remaining after this
|
|
||||||
theParams.clean();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Just to make sure we're stable for tests
|
||||||
|
indexStrings.sort(Comparator.naturalOrder());
|
||||||
|
|
||||||
|
// Interceptor broadcast: JPA_PERFTRACE_INFO
|
||||||
|
String indexStringForLog = indexStrings.size() > 1 ? indexStrings.toString() : indexStrings.get(0);
|
||||||
|
StorageProcessingMessage msg = new StorageProcessingMessage()
|
||||||
|
.setMessage("Using " + theComboParam.getComboSearchParamType() + " index(es) for query for search: "
|
||||||
|
+ indexStringForLog);
|
||||||
|
HookParams params = new HookParams()
|
||||||
|
.add(RequestDetails.class, theRequest)
|
||||||
|
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||||
|
.add(StorageProcessingMessage.class, msg);
|
||||||
|
CompositeInterceptorBroadcaster.doCallHooks(
|
||||||
|
myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
|
||||||
|
|
||||||
|
switch (requireNonNull(theComboParam.getComboSearchParamType())) {
|
||||||
|
case UNIQUE:
|
||||||
|
theQueryStack.addPredicateCompositeUnique(indexStrings, myRequestPartitionId);
|
||||||
|
break;
|
||||||
|
case NON_UNIQUE:
|
||||||
|
theQueryStack.addPredicateCompositeNonUnique(indexStrings, myRequestPartitionId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any empty parameters remaining after this
|
||||||
|
theParams.clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateParamValuesAreValidForComboParam(
|
private boolean validateParamValuesAreValidForComboParam(
|
||||||
@Nonnull SearchParameterMap theParams, List<String> comboParamNames) {
|
RequestDetails theRequest, @Nonnull SearchParameterMap theParams, List<String> comboParamNames) {
|
||||||
boolean paramValuesAreValidForCombo = true;
|
boolean paramValuesAreValidForCombo = true;
|
||||||
for (String nextParamName : comboParamNames) {
|
for (String nextParamName : comboParamNames) {
|
||||||
List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
|
List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
|
||||||
|
|
||||||
// Multiple AND parameters are not supported for unique combo params
|
if (nextValues.isEmpty()) {
|
||||||
if (nextValues.get(0).size() != 1) {
|
|
||||||
ourLog.debug(
|
|
||||||
"Search is not a candidate for unique combo searching - Multiple AND expressions found for the same parameter");
|
|
||||||
paramValuesAreValidForCombo = false;
|
paramValuesAreValidForCombo = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<IQueryParameterType> nextAndValue = nextValues.get(0);
|
for (List<IQueryParameterType> nextAndValue : nextValues) {
|
||||||
for (IQueryParameterType nextOrValue : nextAndValue) {
|
for (IQueryParameterType nextOrValue : nextAndValue) {
|
||||||
if (nextOrValue instanceof DateParam) {
|
if (nextOrValue instanceof DateParam) {
|
||||||
if (((DateParam) nextOrValue).getPrecision() != TemporalPrecisionEnum.DAY) {
|
DateParam dateParam = (DateParam) nextOrValue;
|
||||||
ourLog.debug(
|
if (dateParam.getPrecision() != TemporalPrecisionEnum.DAY) {
|
||||||
"Search is not a candidate for unique combo searching - Date search with non-DAY precision");
|
String message = "Search with params " + comboParamNames + " is not a candidate for combo searching - Date search with non-DAY precision for parameter '" + nextParamName + "'";
|
||||||
paramValuesAreValidForCombo = false;
|
firePerformanceInfo(theRequest, message);
|
||||||
break;
|
paramValuesAreValidForCombo = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextOrValue instanceof BaseParamWithPrefix) {
|
||||||
|
BaseParamWithPrefix<?> paramWithPrefix = (BaseParamWithPrefix<?>) nextOrValue;
|
||||||
|
if (paramWithPrefix.getPrefix() != null) {
|
||||||
|
String message = "Search with params " + comboParamNames + " is not a candidate for combo searching - Parameter '" + nextParamName + "' has prefix: '" + paramWithPrefix.getPrefix().getValue() + "'";
|
||||||
|
firePerformanceInfo(theRequest, message);
|
||||||
|
paramValuesAreValidForCombo = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2416,8 +2432,20 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void firePerformanceInfo(RequestDetails theRequest, String theMessage) {
|
||||||
|
// Only log at debug level since these messages aren't considered important enough
|
||||||
|
// that we should be cluttering the system log, but they are important to the
|
||||||
|
// specific query being executed to we'll INFO level them there
|
||||||
|
ourLog.debug(theMessage);
|
||||||
|
firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_INFO);
|
||||||
|
}
|
||||||
|
|
||||||
private void firePerformanceWarning(RequestDetails theRequest, String theMessage) {
|
private void firePerformanceWarning(RequestDetails theRequest, String theMessage) {
|
||||||
ourLog.warn(theMessage);
|
ourLog.warn(theMessage);
|
||||||
|
firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void firePerformanceMessage(RequestDetails theRequest, String theMessage, Pointcut pointcut) {
|
||||||
StorageProcessingMessage message = new StorageProcessingMessage();
|
StorageProcessingMessage message = new StorageProcessingMessage();
|
||||||
message.setMessage(theMessage);
|
message.setMessage(theMessage);
|
||||||
HookParams params = new HookParams()
|
HookParams params = new HookParams()
|
||||||
|
@ -2425,7 +2453,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
||||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||||
.add(StorageProcessingMessage.class, message);
|
.add(StorageProcessingMessage.class, message);
|
||||||
CompositeInterceptorBroadcaster.doCallHooks(
|
CompositeInterceptorBroadcaster.doCallHooks(
|
||||||
myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
|
myInterceptorBroadcaster, theRequest, pointcut, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getMaximumPageSize() {
|
public static int getMaximumPageSize() {
|
||||||
|
|
|
@ -25,8 +25,12 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
|
||||||
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
|
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
|
||||||
import com.healthmarketscience.sqlbuilder.BinaryCondition;
|
import com.healthmarketscience.sqlbuilder.BinaryCondition;
|
||||||
import com.healthmarketscience.sqlbuilder.Condition;
|
import com.healthmarketscience.sqlbuilder.Condition;
|
||||||
|
import com.healthmarketscience.sqlbuilder.InCondition;
|
||||||
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
|
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ComboNonUniqueSearchParameterPredicateBuilder extends BaseSearchParamPredicateBuilder {
|
public class ComboNonUniqueSearchParameterPredicateBuilder extends BaseSearchParamPredicateBuilder {
|
||||||
|
|
||||||
private final DbColumn myColumnHashComplete;
|
private final DbColumn myColumnHashComplete;
|
||||||
|
@ -40,12 +44,21 @@ public class ComboNonUniqueSearchParameterPredicateBuilder extends BaseSearchPar
|
||||||
myColumnHashComplete = getTable().addColumn("HASH_COMPLETE");
|
myColumnHashComplete = getTable().addColumn("HASH_COMPLETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Condition createPredicateHashComplete(RequestPartitionId theRequestPartitionId, String theIndexString) {
|
public Condition createPredicateHashComplete(RequestPartitionId theRequestPartitionId, List<String> theIndexStrings) {
|
||||||
PartitionablePartitionId partitionId =
|
PartitionablePartitionId partitionId =
|
||||||
PartitionablePartitionId.toStoragePartition(theRequestPartitionId, getPartitionSettings());
|
PartitionablePartitionId.toStoragePartition(theRequestPartitionId, getPartitionSettings());
|
||||||
long hash = ResourceIndexedComboTokenNonUnique.calculateHashComplete(
|
Condition predicate;
|
||||||
getPartitionSettings(), partitionId, theIndexString);
|
if (theIndexStrings.size() == 1) {
|
||||||
BinaryCondition predicate = BinaryCondition.equalTo(myColumnHashComplete, generatePlaceholder(hash));
|
long hash = ResourceIndexedComboTokenNonUnique.calculateHashComplete(
|
||||||
|
getPartitionSettings(), partitionId, theIndexStrings.get(0));
|
||||||
|
predicate = BinaryCondition.equalTo(myColumnHashComplete, generatePlaceholder(hash));
|
||||||
|
} else {
|
||||||
|
List<Long> hashes = theIndexStrings
|
||||||
|
.stream()
|
||||||
|
.map(t -> ResourceIndexedComboTokenNonUnique.calculateHashComplete(getPartitionSettings(), partitionId, t))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
predicate = new InCondition(myColumnHashComplete, generatePlaceholders(hashes));
|
||||||
|
}
|
||||||
return combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
|
return combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,11 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
|
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
|
||||||
import com.healthmarketscience.sqlbuilder.BinaryCondition;
|
import com.healthmarketscience.sqlbuilder.BinaryCondition;
|
||||||
import com.healthmarketscience.sqlbuilder.Condition;
|
import com.healthmarketscience.sqlbuilder.Condition;
|
||||||
|
import com.healthmarketscience.sqlbuilder.InCondition;
|
||||||
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
|
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ComboUniqueSearchParameterPredicateBuilder extends BaseSearchParamPredicateBuilder {
|
public class ComboUniqueSearchParameterPredicateBuilder extends BaseSearchParamPredicateBuilder {
|
||||||
|
|
||||||
private final DbColumn myColumnString;
|
private final DbColumn myColumnString;
|
||||||
|
@ -38,8 +41,13 @@ public class ComboUniqueSearchParameterPredicateBuilder extends BaseSearchParamP
|
||||||
myColumnString = getTable().addColumn("IDX_STRING");
|
myColumnString = getTable().addColumn("IDX_STRING");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Condition createPredicateIndexString(RequestPartitionId theRequestPartitionId, String theIndexString) {
|
public Condition createPredicateIndexString(RequestPartitionId theRequestPartitionId, List<String> theIndexStrings) {
|
||||||
BinaryCondition predicate = BinaryCondition.equalTo(myColumnString, generatePlaceholder(theIndexString));
|
Condition predicate;
|
||||||
|
if (theIndexStrings.size() == 1) {
|
||||||
|
predicate = BinaryCondition.equalTo(myColumnString, generatePlaceholder(theIndexStrings.get(0)));
|
||||||
|
} else {
|
||||||
|
predicate = new InCondition(myColumnString, generatePlaceholders(theIndexStrings));
|
||||||
|
}
|
||||||
return combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
|
return combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package ca.uhn.fhir.jpa.util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for
|
||||||
|
*/
|
||||||
|
public class PermutationBuilder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non instantiable
|
||||||
|
*/
|
||||||
|
private PermutationBuilder() {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of lists of options, returns a list of every possible combination of options.
|
||||||
|
* For example, given the input lists:
|
||||||
|
* <pre>
|
||||||
|
* [
|
||||||
|
* [ A0, A1 ],
|
||||||
|
* [ B0, B1, B2 ]
|
||||||
|
* ]
|
||||||
|
* </pre>
|
||||||
|
* This method will return the following output lists:
|
||||||
|
* <pre>
|
||||||
|
* [
|
||||||
|
* [ A0, B0 ],
|
||||||
|
* [ A1, B0 ],
|
||||||
|
* [ A0, B1 ],
|
||||||
|
* [ A1, B1 ],
|
||||||
|
* [ A0, B2 ],
|
||||||
|
* [ A1, B2 ]
|
||||||
|
* ]
|
||||||
|
* </pre>
|
||||||
|
* <p>
|
||||||
|
* This method may or may not return a newly created list.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theInput A list of lists to calculate permutations of
|
||||||
|
* @param <T> The type associated with {@literal theInput}. The actual type doesn't matter, this method does not look at the
|
||||||
|
* values at all other than to copy them to the output lists.
|
||||||
|
* @since 7.4.0
|
||||||
|
*/
|
||||||
|
public static <T> List<List<T>> calculatePermutations(List<List<T>> theInput) {
|
||||||
|
if (theInput.size() == 1) {
|
||||||
|
return theInput;
|
||||||
|
}
|
||||||
|
List<List<T>> listToPopulate = new ArrayList<>(calculatePermutationCount(theInput));
|
||||||
|
int[] indices = new int[theInput.size()];
|
||||||
|
doCalculatePermutationsIntoIndicesArrayAndPopulateList(0, indices, theInput, listToPopulate);
|
||||||
|
return listToPopulate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively called/calling method which navigates across a list of input ArrayLists and populates the array of indices
|
||||||
|
*
|
||||||
|
* @param thePositionX The offset within {@literal theInput}. We navigate recursively from 0 through {@literal theInput.size()}
|
||||||
|
* @param theIndices The array to populate. This array has a length matching the size of {@literal theInput} and each entry
|
||||||
|
* represents an offset within the list at {@literal theInput.get(arrayIndex)}
|
||||||
|
* @param theInput The input List of ArrayLists
|
||||||
|
* @param theOutput A list to populate with all permutations
|
||||||
|
* @param <T> The type associated with {@literal theInput}. The actual type doesn't matter, this method does not look at the
|
||||||
|
* values at all other than to copy them to the output lists.
|
||||||
|
*/
|
||||||
|
private static <T> void doCalculatePermutationsIntoIndicesArrayAndPopulateList(int thePositionX, int[] theIndices, List<List<T>> theInput, List<List<T>> theOutput) {
|
||||||
|
if (thePositionX != theInput.size()) {
|
||||||
|
// If we're not at the end of the list of input vectors, recursively self-invoke once for each
|
||||||
|
// possible option at the current position in the list of input vectors.
|
||||||
|
for (int positionY = 0; positionY < theInput.get(thePositionX).size(); positionY++) {
|
||||||
|
theIndices[thePositionX] = positionY;
|
||||||
|
doCalculatePermutationsIntoIndicesArrayAndPopulateList(thePositionX + 1, theIndices, theInput, theOutput);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we're at the end of the list of input vectors, then we've been passed the
|
||||||
|
List<T> nextList = new ArrayList<>(theInput.size());
|
||||||
|
for (int positionX = 0; positionX < theInput.size(); positionX++) {
|
||||||
|
nextList.add(theInput.get(positionX).get(theIndices[positionX]));
|
||||||
|
}
|
||||||
|
theOutput.add(nextList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of permutations that {@link #calculatePermutations(List)} will
|
||||||
|
* calculate for the given list.
|
||||||
|
*
|
||||||
|
* @throws ArithmeticException If the number of permutations exceeds {@link Integer#MAX_VALUE}
|
||||||
|
* @since 7.4.0
|
||||||
|
*/
|
||||||
|
public static <T> int calculatePermutationCount(List<List<T>> theLists) throws ArithmeticException {
|
||||||
|
int retVal = !theLists.isEmpty() ? 1 : 0;
|
||||||
|
for (List<T> theList : theLists) {
|
||||||
|
retVal = Math.multiplyExact(retVal, theList.size());
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package ca.uhn.fhir.jpa.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class PermutationBuilderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCalculatePermutations() {
|
||||||
|
List<List<String>> input = List.of(
|
||||||
|
List.of("A0", "A1"),
|
||||||
|
List.of("B0", "B1", "B2"),
|
||||||
|
List.of("C0", "C1")
|
||||||
|
);
|
||||||
|
|
||||||
|
List<List<String>> output = PermutationBuilder.calculatePermutations(input);
|
||||||
|
assertThat(output).containsExactlyInAnyOrder(
|
||||||
|
List.of("A0", "B0", "C0"),
|
||||||
|
List.of("A1", "B0", "C0"),
|
||||||
|
List.of("A0", "B1", "C0"),
|
||||||
|
List.of("A1", "B1", "C0"),
|
||||||
|
List.of("A0", "B2", "C0"),
|
||||||
|
List.of("A1", "B2", "C0"),
|
||||||
|
List.of("A0", "B0", "C1"),
|
||||||
|
List.of("A1", "B0", "C1"),
|
||||||
|
List.of("A0", "B1", "C1"),
|
||||||
|
List.of("A1", "B1", "C1"),
|
||||||
|
List.of("A0", "B2", "C1"),
|
||||||
|
List.of("A1", "B2", "C1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCalculatePermutationCount() {
|
||||||
|
List<List<String>> input = List.of(
|
||||||
|
List.of("A0", "A1"),
|
||||||
|
List.of("B0", "B1", "B2"),
|
||||||
|
List.of("C0", "C1")
|
||||||
|
);
|
||||||
|
assertEquals(12, PermutationBuilder.calculatePermutationCount(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCalculatePermutationCountEmpty() {
|
||||||
|
List<List<String>> input = List.of();
|
||||||
|
assertEquals(0, PermutationBuilder.calculatePermutationCount(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCalculatePermutationCountOverflow() {
|
||||||
|
List<Integer> ranges = IntStream.range(0, 10001).boxed().toList();
|
||||||
|
List<List<Integer>> input = IntStream.range(0, 20).boxed().map(t -> ranges).toList();
|
||||||
|
assertThrows(ArithmeticException.class, () -> PermutationBuilder.calculatePermutationCount(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,21 +7,31 @@ import ca.uhn.fhir.jpa.searchparam.submit.interceptor.SearchParamValidatingInter
|
||||||
import ca.uhn.fhir.jpa.util.SqlQuery;
|
import ca.uhn.fhir.jpa.util.SqlQuery;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.param.DateParam;
|
import ca.uhn.fhir.rest.param.DateParam;
|
||||||
|
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
|
||||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||||
|
import ca.uhn.fhir.rest.param.StringAndListParam;
|
||||||
import ca.uhn.fhir.rest.param.StringParam;
|
import ca.uhn.fhir.rest.param.StringParam;
|
||||||
|
import ca.uhn.fhir.rest.param.TokenAndListParam;
|
||||||
|
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
import ca.uhn.fhir.rest.param.TokenParam;
|
||||||
import ca.uhn.fhir.util.HapiExtensions;
|
import ca.uhn.fhir.util.HapiExtensions;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.BooleanType;
|
import org.hl7.fhir.r4.model.BooleanType;
|
||||||
|
import org.hl7.fhir.r4.model.DateTimeType;
|
||||||
import org.hl7.fhir.r4.model.DateType;
|
import org.hl7.fhir.r4.model.DateType;
|
||||||
import org.hl7.fhir.r4.model.Enumerations;
|
import org.hl7.fhir.r4.model.Enumerations;
|
||||||
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
|
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
|
||||||
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
import org.hl7.fhir.r4.model.Organization;
|
import org.hl7.fhir.r4.model.Organization;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.hl7.fhir.r4.model.SearchParameter;
|
import org.hl7.fhir.r4.model.SearchParameter;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
@ -106,8 +116,8 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getIndexString));
|
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getIndexString));
|
||||||
assertEquals(4, indexedTokens.size());
|
assertEquals(4, indexedTokens.size());
|
||||||
String expected;
|
String expected;
|
||||||
expected = "Patient?gender=http%3A%2F%2Ffoo1%7Cbar1&given=FAMILY1";
|
expected = "Patient?gender=http%3A%2F%2Ffoo1%7Cbar1&given=FAMILY1";
|
||||||
assertEquals(expected, indexedTokens.get(0).getIndexString());
|
assertEquals(expected, indexedTokens.get(0).getIndexString());
|
||||||
expected = "Patient?gender=http%3A%2F%2Ffoo1%7Cbar1&given=FAMILY2";
|
expected = "Patient?gender=http%3A%2F%2Ffoo1%7Cbar1&given=FAMILY2";
|
||||||
assertEquals(expected, indexedTokens.get(1).getIndexString());
|
assertEquals(expected, indexedTokens.get(1).getIndexString());
|
||||||
expected = "Patient?gender=http%3A%2F%2Ffoo2%7Cbar2&given=FAMILY1";
|
expected = "Patient?gender=http%3A%2F%2Ffoo2%7Cbar2&given=FAMILY1";
|
||||||
|
@ -133,14 +143,14 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||||
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
||||||
assertEquals(2, indexedTokens.size());
|
assertEquals(2, indexedTokens.size());
|
||||||
String expected = "Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1";
|
String expected = "Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1";
|
||||||
assertEquals(expected, indexedTokens.get(0).getIndexString());
|
assertEquals(expected, indexedTokens.get(0).getIndexString());
|
||||||
assertEquals(-7504889232313729794L, indexedTokens.get(0).getHashComplete().longValue());
|
assertEquals(-2634469377090377342L, indexedTokens.get(0).getHashComplete().longValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||||
params.add("given", new StringParam("gIVEn1"));
|
params.add("given", new StringParam("gIVEn1"));
|
||||||
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||||
myCaptureQueriesListener.clear();
|
myCaptureQueriesListener.clear();
|
||||||
|
@ -149,12 +159,12 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
myCaptureQueriesListener.logSelectQueries();
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
assertThat(myCaptureQueriesListener.getSelectQueries().stream().map(t->t.getSql(true, false)).toList()).contains(
|
assertThat(myCaptureQueriesListener.getSelectQueries().stream().map(t -> t.getSql(true, false)).toList()).contains(
|
||||||
"SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '-7504889232313729794')"
|
"SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '-2634469377090377342')"
|
||||||
);
|
);
|
||||||
|
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index for query for search: Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index(es) for query for search: Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
|
||||||
// Remove 1, add another
|
// Remove 1, add another
|
||||||
|
@ -165,7 +175,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
assertNotNull(id3);
|
assertNotNull(id3);
|
||||||
|
|
||||||
params = SearchParameterMap.newSynchronous();
|
params = SearchParameterMap.newSynchronous();
|
||||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||||
params.add("given", new StringParam("gIVEn1"));
|
params.add("given", new StringParam("gIVEn1"));
|
||||||
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||||
results = myPatientDao.search(params, mySrd);
|
results = myPatientDao.search(params, mySrd);
|
||||||
|
@ -175,6 +185,23 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyParamLists() {
|
||||||
|
createStringAndTokenCombo_NameAndGender();
|
||||||
|
|
||||||
|
IIdType id1 = createPatient1(null);
|
||||||
|
IIdType id2 = createPatient2(null);
|
||||||
|
|
||||||
|
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||||
|
params.add("family", new StringAndListParam());
|
||||||
|
params.add("given", new StringAndListParam());
|
||||||
|
params.add("gender", new TokenAndListParam());
|
||||||
|
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||||
|
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||||
|
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue(), id2.toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStringAndToken_CreateAndUpdate() {
|
public void testStringAndToken_CreateAndUpdate() {
|
||||||
createStringAndTokenCombo_NameAndGender();
|
createStringAndTokenCombo_NameAndGender();
|
||||||
|
@ -191,13 +218,13 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
assertEquals(1, myCaptureQueriesListener.countCommits());
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
runInTransaction(()->{
|
runInTransaction(() -> {
|
||||||
List<String> indexes = myResourceIndexedComboTokensNonUniqueDao
|
List<String> indexes = myResourceIndexedComboTokensNonUniqueDao
|
||||||
.findAll()
|
.findAll()
|
||||||
.stream()
|
.stream()
|
||||||
.map(ResourceIndexedComboTokenNonUnique::getIndexString)
|
.map(ResourceIndexedComboTokenNonUnique::getIndexString)
|
||||||
.toList();
|
.toList();
|
||||||
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -217,13 +244,13 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
assertEquals(1, myCaptureQueriesListener.countCommits());
|
assertEquals(1, myCaptureQueriesListener.countCommits());
|
||||||
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
assertEquals(0, myCaptureQueriesListener.countRollbacks());
|
||||||
|
|
||||||
runInTransaction(()->{
|
runInTransaction(() -> {
|
||||||
List<String> indexes = myResourceIndexedComboTokensNonUniqueDao
|
List<String> indexes = myResourceIndexedComboTokensNonUniqueDao
|
||||||
.findAll()
|
.findAll()
|
||||||
.stream()
|
.stream()
|
||||||
.map(ResourceIndexedComboTokenNonUnique::getIndexString)
|
.map(ResourceIndexedComboTokenNonUnique::getIndexString)
|
||||||
.toList();
|
.toList();
|
||||||
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY2%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY2&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -243,12 +270,12 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||||
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
||||||
assertEquals(2, indexedTokens.size());
|
assertEquals(2, indexedTokens.size());
|
||||||
assertEquals(-7504889232313729794L, indexedTokens.get(0).getHashComplete().longValue());
|
assertEquals(-2634469377090377342L, indexedTokens.get(0).getHashComplete().longValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||||
params.add("given", new StringParam("gIVEn1"));
|
params.add("given", new StringParam("gIVEn1"));
|
||||||
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||||
params.add("birthdate", new DateParam("2021-02-02"));
|
params.add("birthdate", new DateParam("2021-02-02"));
|
||||||
|
@ -259,11 +286,43 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
String sql = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false);
|
String sql = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false);
|
||||||
String expected = "SELECT t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_IDX_CMB_TOK_NU t0 ON (t1.RES_ID = t0.RES_ID) INNER JOIN HFJ_SPIDX_DATE t2 ON (t1.RES_ID = t2.RES_ID) WHERE ((t0.HASH_COMPLETE = '-7504889232313729794') AND ((t2.HASH_IDENTITY = '5247847184787287691') AND ((t2.SP_VALUE_LOW_DATE_ORDINAL >= '20210202') AND (t2.SP_VALUE_HIGH_DATE_ORDINAL <= '20210202'))))";
|
String expected = "SELECT t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_IDX_CMB_TOK_NU t0 ON (t1.RES_ID = t0.RES_ID) INNER JOIN HFJ_SPIDX_DATE t2 ON (t1.RES_ID = t2.RES_ID) WHERE ((t0.HASH_COMPLETE = '-2634469377090377342') AND ((t2.HASH_IDENTITY = '5247847184787287691') AND ((t2.SP_VALUE_LOW_DATE_ORDINAL >= '20210202') AND (t2.SP_VALUE_HIGH_DATE_ORDINAL <= '20210202'))))";
|
||||||
assertEquals(expected, sql);
|
assertEquals(expected, sql);
|
||||||
|
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index for query for search: Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index(es) for query for search: Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
||||||
|
myMessages.clear();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStringAndToken_MultipleAnd() {
|
||||||
|
createStringAndTokenCombo_NameAndGender();
|
||||||
|
|
||||||
|
IIdType id1 = createPatient(withFamily("SIMPSON"), withGiven("HOMER"), withGiven("JAY"), withGender("male"));
|
||||||
|
assertNotNull(id1);
|
||||||
|
|
||||||
|
logAllNonUniqueIndexes();
|
||||||
|
logAllStringIndexes();
|
||||||
|
|
||||||
|
myMessages.clear();
|
||||||
|
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||||
|
params.add("family", new StringParam("Simpson"));
|
||||||
|
params.add("given", new StringAndListParam().addAnd(new StringParam("Homer")).addAnd(new StringParam("Jay")));
|
||||||
|
params.add("gender", new TokenParam("male"));
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||||
|
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
|
String sql = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false);
|
||||||
|
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 INNER JOIN HFJ_SPIDX_STRING t1 ON (t0.RES_ID = t1.RES_ID) WHERE ((t0.HASH_COMPLETE = '7545664593829342272') AND ((t1.HASH_NORM_PREFIX = '6206712800146298788') AND (t1.SP_VALUE_NORMALIZED LIKE 'JAY%')))";
|
||||||
|
assertEquals(expected, sql);
|
||||||
|
|
||||||
|
logCapturedMessages();
|
||||||
|
assertThat(myMessages.toString()).contains("Using NON_UNIQUE index(es) for query for search: Patient?family=SIMPSON&gender=male&given=HOMER");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -271,7 +330,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStringAndDate_Create() {
|
public void testStringAndDate_Create() {
|
||||||
createStringAndTokenCombo_NameAndBirthdate();
|
createStringAndDateCombo_NameAndBirthdate();
|
||||||
|
|
||||||
IIdType id1 = createPatient1(null);
|
IIdType id1 = createPatient1(null);
|
||||||
assertNotNull(id1);
|
assertNotNull(id1);
|
||||||
|
@ -303,45 +362,10 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||||
|
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index for query for search: Patient?birthdate=2021-02-02&family=FAMILY1]");
|
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index(es) for query for search: Patient?birthdate=2021-02-02&family=FAMILY1]");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Can't create or search for combo params with partial dates
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testStringAndDate_Create_PartialDate() {
|
|
||||||
createStringAndTokenCombo_NameAndBirthdate();
|
|
||||||
|
|
||||||
Patient pt1 = new Patient();
|
|
||||||
pt1.getNameFirstRep().setFamily("Family1").addGiven("Given1");
|
|
||||||
pt1.setBirthDateElement(new DateType("2021-02"));
|
|
||||||
IIdType id1 = myPatientDao.create(pt1, mySrd).getId().toUnqualified();
|
|
||||||
|
|
||||||
logAllNonUniqueIndexes();
|
|
||||||
runInTransaction(() -> {
|
|
||||||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
|
||||||
assertEquals(0, indexedTokens.size());
|
|
||||||
});
|
|
||||||
|
|
||||||
myMessages.clear();
|
|
||||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
|
||||||
params.add("family", new StringParam("family1"));
|
|
||||||
params.add("birthdate", new DateParam("2021-02"));
|
|
||||||
myCaptureQueriesListener.clear();
|
|
||||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
|
||||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
|
||||||
myCaptureQueriesListener.logSelectQueries();
|
|
||||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
|
||||||
|
|
||||||
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
|
||||||
assertThat(sql).doesNotContain("HFJ_IDX_CMB_TOK_NU");
|
|
||||||
|
|
||||||
logCapturedMessages();
|
|
||||||
assertThat(myMessages).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStringAndReference_Create() {
|
public void testStringAndReference_Create() {
|
||||||
createStringAndReferenceCombo_FamilyAndOrganization();
|
createStringAndReferenceCombo_FamilyAndOrganization();
|
||||||
|
@ -356,12 +380,12 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||||
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
||||||
assertEquals(2, indexedTokens.size());
|
assertEquals(2, indexedTokens.size());
|
||||||
assertEquals("Patient?family=FAMILY1%5C%7C&organization=Organization%2Fmy-org", indexedTokens.get(0).getIndexString());
|
assertEquals("Patient?family=FAMILY1&organization=Organization%2Fmy-org", indexedTokens.get(0).getIndexString());
|
||||||
});
|
});
|
||||||
|
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||||
params.add("organization", new ReferenceParam(ORG_ID_QUALIFIED));
|
params.add("organization", new ReferenceParam(ORG_ID_QUALIFIED));
|
||||||
myCaptureQueriesListener.clear();
|
myCaptureQueriesListener.clear();
|
||||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||||
|
@ -369,7 +393,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
myCaptureQueriesListener.logSelectQueries();
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '2277801301223576208')";
|
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '2591238402961312979')";
|
||||||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,8 +418,8 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
|
|
||||||
myCaptureQueriesListener.logSelectQueries();
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
String expected;
|
String expected;
|
||||||
expected = "select rt1_0.RES_ID,rt1_0.RES_TYPE,rt1_0.FHIR_ID from HFJ_RESOURCE rt1_0 where rt1_0.FHIR_ID='my-org'";
|
expected = "select rt1_0.RES_ID,rt1_0.RES_TYPE,rt1_0.FHIR_ID from HFJ_RESOURCE rt1_0 where rt1_0.FHIR_ID='my-org'";
|
||||||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||||
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false);
|
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, false);
|
||||||
assertThat(sql).contains("SP_VALUE_NORMALIZED LIKE 'FAMILY1%'");
|
assertThat(sql).contains("SP_VALUE_NORMALIZED LIKE 'FAMILY1%'");
|
||||||
assertThat(sql).contains("t1.TARGET_RESOURCE_ID");
|
assertThat(sql).contains("t1.TARGET_RESOURCE_ID");
|
||||||
|
@ -403,6 +427,33 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
assertThat(myMessages.get(0)).contains("This search uses an unqualified resource");
|
assertThat(myMessages.get(0)).contains("This search uses an unqualified resource");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOrQuery() {
|
||||||
|
createTokenAndReferenceCombo_FamilyAndOrganization();
|
||||||
|
|
||||||
|
createPatient(withId("PAT"), withActiveTrue());
|
||||||
|
createObservation(withId("O1"), withSubject("Patient/PAT"), withStatus("final"));
|
||||||
|
createObservation(withId("O2"), withSubject("Patient/PAT"), withStatus("registered"));
|
||||||
|
|
||||||
|
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||||
|
params.add("patient", new ReferenceParam("Patient/PAT"));
|
||||||
|
params.add("status", new TokenOrListParam(null, "preliminary", "final", "amended"));
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||||
|
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertThat(actual).contains("Observation/O1");
|
||||||
|
|
||||||
|
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE IN ('2445648980345828396','-6884698528022589694','-8034948665712960724') )";
|
||||||
|
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||||
|
|
||||||
|
logCapturedMessages();
|
||||||
|
assertThat(myMessages.toString()).contains("Observation?patient=Patient%2FPAT&status=amended", "Observation?patient=Patient%2FPAT&status=final", "Observation?patient=Patient%2FPAT&status=preliminary");
|
||||||
|
myMessages.clear();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void createOrg() {
|
private void createOrg() {
|
||||||
Organization org = new Organization();
|
Organization org = new Organization();
|
||||||
org.setName("Some Org");
|
org.setName("Some Org");
|
||||||
|
@ -410,7 +461,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
myOrganizationDao.update(org, mySrd);
|
myOrganizationDao.update(org, mySrd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createStringAndTokenCombo_NameAndBirthdate() {
|
private void createStringAndDateCombo_NameAndBirthdate() {
|
||||||
SearchParameter sp = new SearchParameter();
|
SearchParameter sp = new SearchParameter();
|
||||||
sp.setId("SearchParameter/patient-family");
|
sp.setId("SearchParameter/patient-family");
|
||||||
sp.setType(Enumerations.SearchParamType.STRING);
|
sp.setType(Enumerations.SearchParamType.STRING);
|
||||||
|
@ -455,7 +506,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
sp.setId("SearchParameter/patient-family");
|
sp.setId("SearchParameter/patient-family");
|
||||||
sp.setType(Enumerations.SearchParamType.STRING);
|
sp.setType(Enumerations.SearchParamType.STRING);
|
||||||
sp.setCode("family");
|
sp.setCode("family");
|
||||||
sp.setExpression("Patient.name.family + '|'");
|
sp.setExpression("Patient.name.family");
|
||||||
sp.setStatus(PublicationStatus.ACTIVE);
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
sp.addBase("Patient");
|
sp.addBase("Patient");
|
||||||
mySearchParameterDao.update(sp, mySrd);
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
@ -507,7 +558,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
sp.setId("SearchParameter/patient-family");
|
sp.setId("SearchParameter/patient-family");
|
||||||
sp.setType(Enumerations.SearchParamType.STRING);
|
sp.setType(Enumerations.SearchParamType.STRING);
|
||||||
sp.setCode("family");
|
sp.setCode("family");
|
||||||
sp.setExpression("Patient.name.family + '|'");
|
sp.setExpression("Patient.name.family");
|
||||||
sp.setStatus(PublicationStatus.ACTIVE);
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
sp.addBase("Patient");
|
sp.addBase("Patient");
|
||||||
mySearchParameterDao.update(sp, mySrd);
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
@ -542,6 +593,45 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createTokenAndReferenceCombo_FamilyAndOrganization() {
|
||||||
|
SearchParameter sp = new SearchParameter();
|
||||||
|
sp.setId("SearchParameter/Observation-status");
|
||||||
|
sp.setType(Enumerations.SearchParamType.TOKEN);
|
||||||
|
sp.setCode(Observation.SP_STATUS);
|
||||||
|
sp.setExpression("Observation.status");
|
||||||
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
|
sp.addBase("Observation");
|
||||||
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
|
||||||
|
sp = new SearchParameter();
|
||||||
|
sp.setId("SearchParameter/clinical-patient");
|
||||||
|
sp.setType(Enumerations.SearchParamType.REFERENCE);
|
||||||
|
sp.setCode(Observation.SP_PATIENT);
|
||||||
|
sp.setExpression("Observation.subject.where(resolve() is Patient)");
|
||||||
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
|
sp.addBase("Observation");
|
||||||
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
|
||||||
|
sp = new SearchParameter();
|
||||||
|
sp.setId("SearchParameter/observation-status-and-patient");
|
||||||
|
sp.setType(Enumerations.SearchParamType.COMPOSITE);
|
||||||
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
|
sp.addBase("Observation");
|
||||||
|
sp.addComponent()
|
||||||
|
.setExpression("Observation")
|
||||||
|
.setDefinition("SearchParameter/Observation-status");
|
||||||
|
sp.addComponent()
|
||||||
|
.setExpression("Observation")
|
||||||
|
.setDefinition("SearchParameter/clinical-patient");
|
||||||
|
sp.addExtension()
|
||||||
|
.setUrl(HapiExtensions.EXT_SP_UNIQUE)
|
||||||
|
.setValue(new BooleanType(false));
|
||||||
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
|
||||||
|
mySearchParamRegistry.forceRefresh();
|
||||||
|
|
||||||
|
myMessages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
private IIdType createPatient1(String theOrgId) {
|
private IIdType createPatient1(String theOrgId) {
|
||||||
Patient pt1 = new Patient();
|
Patient pt1 = new Patient();
|
||||||
|
@ -561,5 +651,143 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
||||||
return myPatientDao.create(pt2, mySrd).getId().toUnqualified();
|
return myPatientDao.create(pt2, mySrd).getId().toUnqualified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A few tests where the combo index should not be used
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
public class ShouldNotUseComboIndexTest {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void before() {
|
||||||
|
SearchParameter sp = new SearchParameter();
|
||||||
|
sp.setId("SearchParameter/Observation-date");
|
||||||
|
sp.setType(Enumerations.SearchParamType.DATE);
|
||||||
|
sp.setCode("date");
|
||||||
|
sp.setExpression("Observation.effective");
|
||||||
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
|
sp.addBase("Observation");
|
||||||
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
|
||||||
|
sp = new SearchParameter();
|
||||||
|
sp.setId("SearchParameter/Observation-note-text");
|
||||||
|
sp.setType(Enumerations.SearchParamType.STRING);
|
||||||
|
sp.setCode("note-text");
|
||||||
|
sp.setExpression("Observation.note.text");
|
||||||
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
|
sp.addBase("Observation");
|
||||||
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
|
||||||
|
sp = new SearchParameter();
|
||||||
|
sp.setId("SearchParameter/observation-date-and-note-text");
|
||||||
|
sp.setType(Enumerations.SearchParamType.COMPOSITE);
|
||||||
|
sp.setStatus(PublicationStatus.ACTIVE);
|
||||||
|
sp.addBase("Observation");
|
||||||
|
sp.addComponent()
|
||||||
|
.setExpression("Observation")
|
||||||
|
.setDefinition("SearchParameter/Observation-date");
|
||||||
|
sp.addComponent()
|
||||||
|
.setExpression("Observation")
|
||||||
|
.setDefinition("SearchParameter/Observation-note-text");
|
||||||
|
sp.addExtension()
|
||||||
|
.setUrl(HapiExtensions.EXT_SP_UNIQUE)
|
||||||
|
.setValue(new BooleanType(false));
|
||||||
|
mySearchParameterDao.update(sp, mySrd);
|
||||||
|
|
||||||
|
mySearchParamRegistry.forceRefresh();
|
||||||
|
|
||||||
|
myMessages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can't create or search for combo params with dateTimes that don't have DAY precision
|
||||||
|
*/
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource(value = {
|
||||||
|
"2021-01-02T12:00:01.000Z , false",
|
||||||
|
"2021-01-02T12:00:01Z , false",
|
||||||
|
"2021-01-02 , true", // <-- DAY precision
|
||||||
|
"2021-01 , false",
|
||||||
|
"2021 , false",
|
||||||
|
})
|
||||||
|
public void testPartialDateTime(String theDateValue, boolean theShouldUseComboIndex) {
|
||||||
|
IIdType id1 = createObservation(theDateValue);
|
||||||
|
|
||||||
|
logAllNonUniqueIndexes();
|
||||||
|
runInTransaction(() -> {
|
||||||
|
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||||
|
if (theShouldUseComboIndex) {
|
||||||
|
assertEquals(1, indexedTokens.size());
|
||||||
|
} else {
|
||||||
|
assertEquals(0, indexedTokens.size());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SearchParameterMap params = SearchParameterMap
|
||||||
|
.newSynchronous()
|
||||||
|
.add("note-text", new StringParam("Hello"))
|
||||||
|
.add("date", new DateParam(theDateValue));
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||||
|
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
|
if (theShouldUseComboIndex) {
|
||||||
|
assertComboIndexUsed();
|
||||||
|
} else {
|
||||||
|
assertComboIndexNotUsed();
|
||||||
|
assertThat(myMessages.toString()).contains("INFO Search with params [date, note-text] is not a candidate for combo searching - Date search with non-DAY precision for parameter 'date'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(booleans = {true, false})
|
||||||
|
public void testDateModifier(boolean theUseComparator) {
|
||||||
|
IIdType id1 = createObservation("2021-01-02");
|
||||||
|
createObservation("2023-01-02");
|
||||||
|
|
||||||
|
SearchParameterMap params = SearchParameterMap
|
||||||
|
.newSynchronous()
|
||||||
|
.add("note-text", new StringParam("Hello"))
|
||||||
|
.add("date", new DateParam("2021-01-02").setPrefix(theUseComparator ? ParamPrefixEnum.LESSTHAN_OR_EQUALS : null));
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||||
|
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||||
|
myCaptureQueriesListener.logSelectQueries();
|
||||||
|
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
|
if (theUseComparator) {
|
||||||
|
assertComboIndexNotUsed();
|
||||||
|
assertThat(myMessages.toString()).contains("INFO Search with params [date, note-text] is not a candidate for combo searching - Parameter 'date' has prefix: 'le'");
|
||||||
|
} else {
|
||||||
|
assertComboIndexUsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertComboIndexUsed() {
|
||||||
|
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||||
|
assertThat(sql).contains("HFJ_IDX_CMB_TOK_NU");
|
||||||
|
|
||||||
|
assertThat(myMessages.toString()).contains("Using NON_UNIQUE index(es) for query");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertComboIndexNotUsed() {
|
||||||
|
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||||
|
assertThat(sql).doesNotContain("HFJ_IDX_CMB_TOK_NU");
|
||||||
|
|
||||||
|
assertThat(myMessages.toString()).doesNotContain("Using NON_UNIQUE index(es) for query");
|
||||||
|
}
|
||||||
|
|
||||||
|
private IIdType createObservation(String theDateValue) {
|
||||||
|
Observation pt1 = new Observation();
|
||||||
|
pt1.addNote().setText("Hello");
|
||||||
|
pt1.setEffective(new DateTimeType(theDateValue));
|
||||||
|
IIdType id1 = myObservationDao.create(pt1, mySrd).getId().toUnqualified();
|
||||||
|
return id1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
|
import ca.uhn.fhir.rest.param.DateOrListParam;
|
||||||
import ca.uhn.fhir.rest.param.DateParam;
|
import ca.uhn.fhir.rest.param.DateParam;
|
||||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenAndListParam;
|
import ca.uhn.fhir.rest.param.TokenAndListParam;
|
||||||
|
@ -471,10 +472,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1);
|
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1);
|
||||||
unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||||
assertThat(unformattedSql).contains("HASH_SYS_AND_VALUE IN ('4101160957635429999','-3122824860083758210')");
|
assertEquals("SELECT t0.RES_ID FROM HFJ_IDX_CMP_STRING_UNIQ t0 WHERE (t0.IDX_STRING IN ('Patient?identifier=urn%7C111','Patient?identifier=urn%7C222') )", unformattedSql);
|
||||||
assertThat(unformattedSql).doesNotContain(("IDX_STRING"));
|
|
||||||
assertThat(unformattedSql).doesNotContain(("RES_DELETED_AT"));
|
|
||||||
assertThat(unformattedSql).doesNotContain(("RES_TYPE"));
|
|
||||||
|
|
||||||
// Not matching the composite SP at all
|
// Not matching the composite SP at all
|
||||||
myCaptureQueriesListener.clear();
|
myCaptureQueriesListener.clear();
|
||||||
|
@ -1110,6 +1108,40 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOrQuery() {
|
||||||
|
myStorageSettings.setAdvancedHSearchIndexing(false);
|
||||||
|
createUniqueBirthdateAndGenderSps();
|
||||||
|
|
||||||
|
Patient pt1 = new Patient();
|
||||||
|
pt1.setGender(Enumerations.AdministrativeGender.MALE);
|
||||||
|
pt1.setBirthDateElement(new DateType("2011-01-01"));
|
||||||
|
IIdType id1 = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
Patient pt2 = new Patient();
|
||||||
|
pt2.setGender(Enumerations.AdministrativeGender.MALE);
|
||||||
|
pt2.setBirthDateElement(new DateType("2011-01-02"));
|
||||||
|
IIdType id2 = myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
myMessages.clear();
|
||||||
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
|
params.setLoadSynchronousUpTo(100);
|
||||||
|
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||||
|
params.add("birthdate", new DateOrListParam().addOr(new DateParam("2011-01-01")).addOr(new DateParam("2011-01-02")));
|
||||||
|
myCaptureQueriesListener.clear();
|
||||||
|
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||||
|
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||||
|
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1.getValue(), id2.getValue());
|
||||||
|
|
||||||
|
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false))
|
||||||
|
.contains("SELECT t0.RES_ID FROM HFJ_IDX_CMP_STRING_UNIQ t0 WHERE (t0.IDX_STRING IN ('Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale','Patient?birthdate=2011-01-02&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale') )");
|
||||||
|
logCapturedMessages();
|
||||||
|
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: [Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale, Patient?birthdate=2011-01-02&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale]");
|
||||||
|
myMessages.clear();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSearchSynchronousUsingUniqueComposite() {
|
public void testSearchSynchronousUsingUniqueComposite() {
|
||||||
myStorageSettings.setAdvancedHSearchIndexing(false);
|
myStorageSettings.setAdvancedHSearchIndexing(false);
|
||||||
|
@ -1136,7 +1168,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1.getValue());
|
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1.getValue());
|
||||||
|
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1164,7 +1196,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
String searchId = results.getUuid();
|
String searchId = results.getUuid();
|
||||||
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1);
|
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1);
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
|
||||||
// Other order
|
// Other order
|
||||||
|
@ -1188,7 +1220,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
results = myPatientDao.search(params, mySrd);
|
results = myPatientDao.search(params, mySrd);
|
||||||
assertThat(toUnqualifiedVersionlessIdValues(results)).isEmpty();
|
assertThat(toUnqualifiedVersionlessIdValues(results)).isEmpty();
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-03&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-03&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
@ -1270,7 +1302,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
IIdType id1 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG", mySrd).getId().toUnqualifiedVersionless();
|
IIdType id1 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG", mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
|
||||||
runInTransaction(() -> {
|
runInTransaction(() -> {
|
||||||
|
@ -1289,7 +1321,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
IIdType id2 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG", mySrd).getId().toUnqualifiedVersionless();
|
IIdType id2 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG", mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
runInTransaction(() -> {
|
runInTransaction(() -> {
|
||||||
List<ResourceIndexedComboStringUnique> uniques = myResourceIndexedComboStringUniqueDao.findAll();
|
List<ResourceIndexedComboStringUnique> uniques = myResourceIndexedComboStringUniqueDao.findAll();
|
||||||
|
@ -1646,7 +1678,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||||
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id2.getValue());
|
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id2.getValue());
|
||||||
|
|
||||||
logCapturedMessages();
|
logCapturedMessages();
|
||||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||||
myMessages.clear();
|
myMessages.clear();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue