Merge pull request #1730 from jamesagnew/date-match-bug

Add integer date fields to ResourceIndexedSearchParamDate
This commit is contained in:
Tadgh 2020-04-30 08:53:35 -07:00 committed by GitHub
commit 8fce86bba5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 779 additions and 337 deletions

View File

@ -6,7 +6,7 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.time.DateUtils;
import ca.uhn.fhir.util.DateUtils;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.*;
@ -263,6 +263,67 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return this;
}
/**
* Return the current lower bound as an integer representative of the date.
*
* e.g. 2019-02-22T04:22:00-0500 -> 20120922
*/
public Integer getLowerBoundAsDateInteger() {
if (myLowerBound == null || myLowerBound.getValue() == null) {
return null;
}
int retVal = DateUtils.convertDatetoDayInteger(myLowerBound.getValue());
if (myLowerBound.getPrefix() != null) {
switch (myLowerBound.getPrefix()) {
case GREATERTHAN:
case STARTS_AFTER:
retVal += 1;
break;
case EQUAL:
case GREATERTHAN_OR_EQUALS:
break;
case LESSTHAN:
case APPROXIMATE:
case LESSTHAN_OR_EQUALS:
case ENDS_BEFORE:
case NOT_EQUAL:
throw new IllegalStateException("Invalid lower bound comparator: " + myLowerBound.getPrefix());
}
}
return retVal;
}
/**
* Return the current upper bound as an integer representative of the date
*
* e.g. 2019-02-22T04:22:00-0500 -> 2019122
*/
public Integer getUpperBoundAsDateInteger() {
if (myUpperBound == null || myUpperBound.getValue() == null) {
return null;
}
int retVal = DateUtils.convertDatetoDayInteger(myUpperBound.getValue());
if (myUpperBound.getPrefix() != null) {
switch (myUpperBound.getPrefix()) {
case LESSTHAN:
case ENDS_BEFORE:
retVal -= 1;
break;
case EQUAL:
case LESSTHAN_OR_EQUALS:
break;
case GREATERTHAN_OR_EQUALS:
case GREATERTHAN:
case APPROXIMATE:
case NOT_EQUAL:
case STARTS_AFTER:
throw new IllegalStateException("Invalid upper bound comparator: " + myUpperBound.getPrefix());
}
}
return retVal;
}
public Date getLowerBoundAsInstant() {
if (myLowerBound == null || myLowerBound.getValue() == null) {
return null;
@ -270,10 +331,7 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myLowerBound.getValue();
if (myLowerBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
Calendar cal = DateUtils.toCalendar(retVal);
cal.setTimeZone(TimeZone.getTimeZone("GMT-11:30"));
cal = DateUtils.truncate(cal, Calendar.DATE);
retVal = cal.getTime();
retVal = DateUtils.getLowestInstantFromDate(retVal);
}
if (myLowerBound.getPrefix() != null) {
@ -335,10 +393,7 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myUpperBound.getValue();
if (myUpperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
Calendar cal = DateUtils.toCalendar(retVal);
cal.setTimeZone(TimeZone.getTimeZone("GMT+11:30"));
cal = DateUtils.truncate(cal, Calendar.DATE);
retVal = cal.getTime();
retVal = DateUtils.getHighestInstantFromDate(retVal);
}
if (myUpperBound.getPrefix() != null) {

View File

@ -65,6 +65,8 @@ public final class DateUtils {
@SuppressWarnings("WeakerAccess")
public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
private static final String PATTERN_INTEGER_DATE = "yyyyMMdd";
private static final String[] DEFAULT_PATTERNS = new String[]{
PATTERN_RFC1123,
PATTERN_RFC1036,
@ -153,6 +155,35 @@ public final class DateUtils {
return null;
}
public static Date getHighestInstantFromDate(Date theDateValue) {
return getInstantFromDateWithTimezone(theDateValue, TimeZone.getTimeZone("GMT+11:30"));
}
public static Date getLowestInstantFromDate(Date theDateValue) {
return getInstantFromDateWithTimezone(theDateValue, TimeZone.getTimeZone("GMT-11:30"));
}
public static Date getInstantFromDateWithTimezone(Date theDateValue, TimeZone theTimezone) {
Calendar cal = org.apache.commons.lang3.time.DateUtils.toCalendar(theDateValue);
cal.setTimeZone(theTimezone);
cal = org.apache.commons.lang3.time.DateUtils.truncate(cal, Calendar.DATE);
return cal.getTime();
}
public static int convertDatetoDayInteger(final Date theDateValue) {
notNull(theDateValue, "Date value");
SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE);
String theDateString = format.format(theDateValue);
return Integer.parseInt(theDateString);
}
public static String convertDateToIso8601String(final Date theDateValue){
notNull(theDateValue, "Date value");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
return format.format(theDateValue);
}
/**
* Formats the given date according to the RFC 1123 pattern.
*

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1499
title: When performing a search with a DateParam that has DAY precision, rely on new ordinal date field for comparison
instead of attempting to find oldest and newest instant that could be valid.

View File

@ -2,10 +2,6 @@
The HAPI FHIR JPA Server fully implements most [FHIR search](https://www.hl7.org/fhir/search.html) operations for most versions of FHIR. However, there are some known limitations of the current implementation. Here is a partial list of search functionality that is not currently supported in HAPI FHIR:
### Date searches without timestamp
Searching by date with no timestamp currently doesn't match all records it should. See [Issue 1499](https://github.com/jamesagnew/hapi-fhir/issues/1499).
### Chains within _has
Chains within _has are not currently supported for performance reasons. For example, this search is not currently supported

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
@ -150,67 +151,95 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
return p;
}
private boolean isNullOrDayPrecision(DateParam theDateParam) {
return theDateParam == null || theDateParam.getPrecision().ordinal() == TemporalPrecisionEnum.DAY.ordinal();
}
private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamDate> theFrom,
DateRangeParam theRange,
SearchFilterParser.CompareOperation operation) {
Date lowerBound = theRange.getLowerBoundAsInstant();
Date upperBound = theRange.getUpperBoundAsInstant();
Predicate lt;
Predicate gt;
Date lowerBoundInstant = theRange.getLowerBoundAsInstant();
Date upperBoundInstant = theRange.getUpperBoundAsInstant();
DateParam lowerBound = theRange.getLowerBound();
DateParam upperBound = theRange.getUpperBound();
Integer lowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger();
Integer upperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger();
Comparable genericLowerBound;
Comparable genericUpperBound;
/**
* If all present search parameters are of DAY precision, and {@link DaoConfig#getUseOrdinalDatesForDayPrecisionSearches()} is true,
* then we attempt to use the ordinal field for date comparisons instead of the date field.
*/
boolean isOrdinalComparison = isNullOrDayPrecision(lowerBound) && isNullOrDayPrecision(upperBound) && myDaoConfig.getModelConfig().getUseOrdinalDatesForDayPrecisionSearches();
Predicate lt = null;
Predicate gt = null;
Predicate lb = null;
Predicate ub = null;
String lowValueField;
String highValueField;
if (isOrdinalComparison) {
lowValueField = "myValueLowDateOrdinal";
highValueField = "myValueHighDateOrdinal";
genericLowerBound = lowerBoundAsOrdinal;
genericUpperBound = upperBoundAsOrdinal;
} else {
lowValueField = "myValueLow";
highValueField = "myValueHigh";
genericLowerBound = lowerBoundInstant;
genericUpperBound = upperBoundInstant;
}
if (operation == SearchFilterParser.CompareOperation.lt) {
if (lowerBound == null) {
if (lowerBoundInstant == null) {
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
}
lb = theBuilder.lessThan(theFrom.get("myValueLow"), lowerBound);
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
lb = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound);
} else if (operation == SearchFilterParser.CompareOperation.le) {
if (upperBound == null) {
if (upperBoundInstant == null) {
throw new InvalidRequestException("upperBound value not correctly specified for compare operation");
}
lb = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound);
//im like 80% sure this should be ub and not lb, as it is an UPPER bound.
lb = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound);
} else if (operation == SearchFilterParser.CompareOperation.gt) {
if (upperBound == null) {
if (upperBoundInstant == null) {
throw new InvalidRequestException("upperBound value not correctly specified for compare operation");
}
lb = theBuilder.greaterThan(theFrom.get("myValueHigh"), upperBound);
} else if (operation == SearchFilterParser.CompareOperation.ge) {
if (lowerBound == null) {
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
}
lb = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound);
} else if (operation == SearchFilterParser.CompareOperation.ne) {
if ((lowerBound == null) ||
(upperBound == null)) {
lb = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound);
} else if (operation == SearchFilterParser.CompareOperation.ge) {
if (lowerBoundInstant == null) {
throw new InvalidRequestException("lowerBound value not correctly specified for compare operation");
}
lb = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);
} else if (operation == SearchFilterParser.CompareOperation.ne) {
if ((lowerBoundInstant == null) ||
(upperBoundInstant == null)) {
throw new InvalidRequestException("lowerBound and/or upperBound value not correctly specified for compare operation");
}
/*Predicate*/
lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), lowerBound);
/*Predicate*/
gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), upperBound);
lt = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound);
gt = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound);
lb = theBuilder.or(lt,
gt);
} else if ((operation == SearchFilterParser.CompareOperation.eq) ||
(operation == null)) {
if (lowerBound != null) {
/*Predicate*/
gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound);
/*Predicate*/
lt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), lowerBound);
if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) {
} else if ((operation == SearchFilterParser.CompareOperation.eq) || (operation == null)) {
if (lowerBoundInstant != null) {
gt = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound);
lt = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound);
if (lowerBound.getPrefix() == ParamPrefixEnum.STARTS_AFTER || lowerBound.getPrefix() == ParamPrefixEnum.EQUAL) {
lb = gt;
} else {
lb = theBuilder.or(gt, lt);
}
}
if (upperBound != null) {
/*Predicate*/
gt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), upperBound);
/*Predicate*/
lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound);
if (upperBoundInstant != null) {
gt = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound);
lt = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound);
if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) {
ub = lt;
} else {
@ -221,8 +250,11 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
throw new InvalidRequestException(String.format("Unsupported operator specified, operator=%s",
operation.name()));
}
ourLog.trace("Date range is {} - {}", lowerBound, upperBound);
if (isOrdinalComparison) {
ourLog.trace("Ordinal date range is {} - {} ", lowerBoundAsOrdinal, upperBoundAsOrdinal);
} else {
ourLog.trace("Date range is {} - {}", lowerBoundInstant, upperBoundInstant);
}
if (lb != null && ub != null) {
return (theBuilder.and(lb, ub));

View File

@ -4353,15 +4353,14 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
assertEquals(1, results.getResources(0, 10).size());
// We expect a new one because we don't cache the search URL for very long search URLs
assertEquals(2, mySearchEntityDao.count());
}
@Test
public void testDateSearchParametersShouldBeTimezoneIndependent() {
createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30");
createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00");
createObservationWithEffective("NO1", "2011-01-03T00:00:00+01:00");
createObservationWithEffective("YES00", "2011-01-02T23:00:00-11:30");
createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30");
createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00");
createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00");
@ -4394,6 +4393,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
List<String> values = toUnqualifiedVersionlessIdValues(results);
Collections.sort(values);
assertThat(values.toString(), values, contains(
"Observation/YES00",
"Observation/YES01",
"Observation/YES02",
"Observation/YES03",
@ -4420,6 +4420,68 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
));
}
@Test
public void testDateSearchParametersShouldBeHourIndependent() {
createObservationWithEffective("YES01", "2011-01-02T00:00:00");
createObservationWithEffective("YES02", "2011-01-02T01:00:00");
createObservationWithEffective("YES03", "2011-01-02T02:00:00");
createObservationWithEffective("YES04", "2011-01-02T03:00:00");
createObservationWithEffective("YES05", "2011-01-02T04:00:00");
createObservationWithEffective("YES06", "2011-01-02T05:00:00");
createObservationWithEffective("YES07", "2011-01-02T06:00:00");
createObservationWithEffective("YES08", "2011-01-02T07:00:00");
createObservationWithEffective("YES09", "2011-01-02T08:00:00");
createObservationWithEffective("YES10", "2011-01-02T09:00:00");
createObservationWithEffective("YES11", "2011-01-02T10:00:00");
createObservationWithEffective("YES12", "2011-01-02T11:00:00");
createObservationWithEffective("YES13", "2011-01-02T12:00:00");
createObservationWithEffective("YES14", "2011-01-02T13:00:00");
createObservationWithEffective("YES15", "2011-01-02T14:00:00");
createObservationWithEffective("YES16", "2011-01-02T15:00:00");
createObservationWithEffective("YES17", "2011-01-02T16:00:00");
createObservationWithEffective("YES18", "2011-01-02T17:00:00");
createObservationWithEffective("YES19", "2011-01-02T18:00:00");
createObservationWithEffective("YES20", "2011-01-02T19:00:00");
createObservationWithEffective("YES21", "2011-01-02T20:00:00");
createObservationWithEffective("YES22", "2011-01-02T21:00:00");
createObservationWithEffective("YES23", "2011-01-02T22:00:00");
createObservationWithEffective("YES24", "2011-01-02T23:00:00");
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Observation.SP_DATE, new DateParam("2011-01-02"));
IBundleProvider results = myObservationDao.search(map);
List<String> values = toUnqualifiedVersionlessIdValues(results);
Collections.sort(values);
assertThat(values.toString(), values, contains(
"Observation/YES01",
"Observation/YES02",
"Observation/YES03",
"Observation/YES04",
"Observation/YES05",
"Observation/YES06",
"Observation/YES07",
"Observation/YES08",
"Observation/YES09",
"Observation/YES10",
"Observation/YES11",
"Observation/YES12",
"Observation/YES13",
"Observation/YES14",
"Observation/YES15",
"Observation/YES16",
"Observation/YES17",
"Observation/YES18",
"Observation/YES19",
"Observation/YES20",
"Observation/YES21",
"Observation/YES22",
"Observation/YES23",
"Observation/YES24"
));
}
private void createObservationWithEffective(String theId, String theEffective) {
Observation obs = new Observation();
obs.setId(theId);

View File

@ -3280,8 +3280,8 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test {
@Test
public void testDateSearchParametersShouldBeTimezoneIndependent() {
createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30");
createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00");
createObservationWithEffective("NO1", "2011-01-01T23:00:00-11:30");
createObservationWithEffective("NO2", "2011-01-03T23:00:00+01:30");
createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30");
createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00");

View File

@ -887,10 +887,10 @@ public class InMemorySubscriptionMatcherR4Test {
public void testDateSearchParametersShouldBeTimezoneIndependent() {
List<Observation> nlist = new ArrayList<>();
nlist.add(createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30"));
nlist.add(createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00"));
List<Observation> ylist = new ArrayList<>();
nlist.add(createObservationWithEffective("YES00", "2011-01-02T23:00:00-11:30"));
ylist.add(createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30"));
ylist.add(createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00"));
ylist.add(createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00"));

View File

@ -0,0 +1,264 @@
package ca.uhn.fhir.jpa.migrate.taskdef;
/*-
* #%L
* HAPI FHIR JPA Server - Migration
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.VersionEnum;
import com.google.common.collect.ForwardingMap;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
public abstract class BaseColumnCalculatorTask extends BaseTableColumnTask {
protected static final Logger ourLog = LoggerFactory.getLogger(BaseColumnCalculatorTask.class);
private int myBatchSize = 10000;
private ThreadPoolExecutor myExecutor;
public void setBatchSize(int theBatchSize) {
myBatchSize = theBatchSize;
}
/**
* Constructor
*/
public BaseColumnCalculatorTask(VersionEnum theRelease, String theVersion) {
super(theRelease.toString(), theVersion);
}
/**
* Allows concrete implementations to decide if they should be skipped.
* @return a boolean indicating whether or not to skip execution of the task.
*/
protected abstract boolean shouldSkipTask();
@Override
public synchronized void doExecute() throws SQLException {
if (isDryRun() || shouldSkipTask()) {
return;
}
initializeExecutor();
try {
while(true) {
MyRowCallbackHandler rch = new MyRowCallbackHandler();
getTxTemplate().execute(t -> {
JdbcTemplate jdbcTemplate = newJdbcTemplate();
jdbcTemplate.setMaxRows(100000);
String sql = "SELECT * FROM " + getTableName() + " WHERE " + getWhereClause();
logInfo(ourLog, "Finding up to {} rows in {} that requires calculations, using query: {}", myBatchSize, getTableName(), sql);
jdbcTemplate.query(sql, rch);
rch.done();
return null;
});
rch.submitNext();
List<Future<?>> futures = rch.getFutures();
if (futures.isEmpty()) {
break;
}
logInfo(ourLog, "Waiting for {} tasks to complete", futures.size());
for (Future<?> next : futures) {
try {
next.get();
} catch (Exception e) {
throw new SQLException(e);
}
}
}
} finally {
destroyExecutor();
}
}
private void destroyExecutor() {
myExecutor.shutdownNow();
}
private void initializeExecutor() {
int maximumPoolSize = Runtime.getRuntime().availableProcessors();
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(maximumPoolSize);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("worker-" + "-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
logInfo(ourLog, "Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size());
StopWatch sw = new StopWatch();
try {
executorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
logInfo(ourLog, "Slot become available after {}ms", sw.getMillis());
}
};
myExecutor = new ThreadPoolExecutor(
1,
maximumPoolSize,
0L,
TimeUnit.MILLISECONDS,
executorQueue,
threadFactory,
rejectedExecutionHandler);
}
private Future<?> updateRows(List<Map<String, Object>> theRows) {
Runnable task = () -> {
StopWatch sw = new StopWatch();
getTxTemplate().execute(t -> {
// Loop through rows
assert theRows != null;
for (Map<String, Object> nextRow : theRows) {
Map<String, Object> newValues = new HashMap<>();
MandatoryKeyMap<String, Object> nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow);
// Apply calculators
for (Map.Entry<String, Function<MandatoryKeyMap<String, Object>, Object>> nextCalculatorEntry : myCalculators.entrySet()) {
String nextColumn = nextCalculatorEntry.getKey();
Function<MandatoryKeyMap<String, Object>, Object> nextCalculator = nextCalculatorEntry.getValue();
Object value = nextCalculator.apply(nextRowMandatoryKeyMap);
newValues.put(nextColumn, value);
}
// Generate update SQL
StringBuilder sqlBuilder = new StringBuilder();
List<Object> arguments = new ArrayList<>();
sqlBuilder.append("UPDATE ");
sqlBuilder.append(getTableName());
sqlBuilder.append(" SET ");
for (Map.Entry<String, Object> nextNewValueEntry : newValues.entrySet()) {
if (arguments.size() > 0) {
sqlBuilder.append(", ");
}
sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?");
arguments.add(nextNewValueEntry.getValue());
}
sqlBuilder.append(" WHERE SP_ID = ?");
arguments.add((Number) nextRow.get("SP_ID"));
// Apply update SQL
newJdbcTemplate().update(sqlBuilder.toString(), arguments.toArray());
}
return theRows.size();
});
logInfo(ourLog, "Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString());
};
return myExecutor.submit(task);
}
private class MyRowCallbackHandler implements RowCallbackHandler {
private List<Map<String, Object>> myRows = new ArrayList<>();
private List<Future<?>> myFutures = new ArrayList<>();
@Override
public void processRow(ResultSet rs) throws SQLException {
Map<String, Object> row = new ColumnMapRowMapper().mapRow(rs, 0);
myRows.add(row);
if (myRows.size() >= myBatchSize) {
submitNext();
}
}
private void submitNext() {
if (myRows.size() > 0) {
myFutures.add(updateRows(myRows));
myRows = new ArrayList<>();
}
}
public List<Future<?>> getFutures() {
return myFutures;
}
public void done() {
if (myRows.size() > 0) {
submitNext();
}
}
}
public static class MandatoryKeyMap<K, V> extends ForwardingMap<K, V> {
private final Map<K, V> myWrap;
public MandatoryKeyMap(Map<K, V> theWrap) {
myWrap = theWrap;
}
@Override
public V get(Object theKey) {
if (!containsKey(theKey)) {
throw new IllegalArgumentException("No key: " + theKey);
}
return super.get(theKey);
}
public String getString(String theKey) {
return (String) get(theKey);
}
public Date getDate(String theKey) {
return (Date) get(theKey);
}
@Override
protected Map<K, V> delegate() {
return myWrap;
}
public String getResourceType() {
return getString("RES_TYPE");
}
public String getParamName() {
return getString("SP_NAME");
}
}
}

View File

@ -25,11 +25,17 @@ import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.thymeleaf.util.StringUtils;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
public abstract class BaseTableColumnTask extends BaseTableTask {
private String myColumnName;
protected Map<String, Function<BaseColumnCalculatorTask.MandatoryKeyMap<String, Object>, Object>> myCalculators = new HashMap<>();
protected String myColumnName;
//If a concrete class decides to, they can define a custom WHERE clause for the task.
protected String myWhereClause;
public BaseTableColumnTask(String theProductVersion, String theSchemaVersion) {
super(theProductVersion, theSchemaVersion);
@ -40,11 +46,21 @@ public abstract class BaseTableColumnTask extends BaseTableTask {
return this;
}
public String getColumnName() {
return myColumnName;
}
protected void setWhereClause(String theWhereClause) {
this.myWhereClause = theWhereClause;
}
protected String getWhereClause() {
if (myWhereClause == null) {
return getColumnName() + " IS NULL";
} else {
return myWhereClause;
}
}
@Override
public void validate() {
super.validate();
@ -63,4 +79,10 @@ public abstract class BaseTableColumnTask extends BaseTableTask {
super.generateHashCode(theBuilder);
theBuilder.append(myColumnName);
}
public BaseTableColumnTask addCalculator(String theColumnName, Function<BaseColumnCalculatorTask.MandatoryKeyMap<String, Object>, Object> theConsumer) {
Validate.isTrue(myCalculators.containsKey(theColumnName) == false);
myCalculators.put(theColumnName, theConsumer);
return this;
}
}

View File

@ -27,6 +27,8 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
public abstract class BaseTableTask extends BaseTask {
private String myTableName;
public BaseTableTask(String theProductVersion, String theSchemaVersion) {
super(theProductVersion, theSchemaVersion);
}

View File

@ -167,7 +167,7 @@ public abstract class BaseTask {
doExecute();
}
public abstract void doExecute() throws SQLException;
protected abstract void doExecute() throws SQLException;
public void setFailureAllowed(boolean theFailureAllowed) {
myFailureAllowed = theFailureAllowed;

View File

@ -34,253 +34,34 @@ import org.springframework.jdbc.core.RowCallbackHandler;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
public class CalculateHashesTask extends BaseTableColumnTask {
private static final Logger ourLog = LoggerFactory.getLogger(CalculateHashesTask.class);
private int myBatchSize = 10000;
private Map<String, Function<MandatoryKeyMap<String, Object>, Long>> myCalculators = new HashMap<>();
private ThreadPoolExecutor myExecutor;
public class CalculateHashesTask extends BaseColumnCalculatorTask {
/**
* Constructor
*/
public CalculateHashesTask(VersionEnum theRelease, String theVersion) {
super(theRelease.toString(), theVersion);
super(theRelease, theVersion);
setDescription("Calculate resource search parameter index hashes");
}
public void setBatchSize(int theBatchSize) {
myBatchSize = theBatchSize;
}
@Override
public CalculateHashesTask setColumnName(String theColumnName) {
super.setColumnName(theColumnName);
return this;
}
@Override
public synchronized void doExecute() throws SQLException {
if (isDryRun()) {
return;
}
Set<String> tableNames = JdbcUtils.getTableNames(getConnectionProperties());
// This table was added shortly after hash indexes were added, so it is a reasonable indicator for whether this
// migration has already been run
if (tableNames.contains("HFJ_RES_REINDEX_JOB")) {
logInfo(ourLog, "The table HFJ_RES_REINDEX_JOB already exists. Skipping calculate hashes task.");
return;
}
initializeExecutor();
protected boolean shouldSkipTask() {
try {
while (true) {
MyRowCallbackHandler rch = new MyRowCallbackHandler();
getTxTemplate().execute(t -> {
JdbcTemplate jdbcTemplate = newJdbcTemplate();
jdbcTemplate.setMaxRows(100000);
String sql = "SELECT * FROM " + getTableName() + " WHERE " + getColumnName() + " IS NULL";
logInfo(ourLog, "Finding up to {} rows in {} that requires hashes", myBatchSize, getTableName());
jdbcTemplate.query(sql, rch);
rch.done();
return null;
});
rch.submitNext();
List<Future<?>> futures = rch.getFutures();
if (futures.isEmpty()) {
break;
}
logInfo(ourLog, "Waiting for {} tasks to complete", futures.size());
for (Future<?> next : futures) {
try {
next.get();
} catch (Exception e) {
throw new SQLException(e);
}
}
Set<String> tableNames = JdbcUtils.getTableNames(getConnectionProperties());
boolean shouldSkip = tableNames.contains("HFJ_RES_REINDEX_JOB");
// This table was added shortly after hash indexes were added, so it is a reasonable indicator for whether this
// migration has already been run
if (shouldSkip) {
logInfo(ourLog, "The table HFJ_RES_REINDEX_JOB already exists. Skipping calculate hashes task.");
}
} finally {
destroyExecutor();
}
}
private void destroyExecutor() {
myExecutor.shutdownNow();
}
private void initializeExecutor() {
int maximumPoolSize = Runtime.getRuntime().availableProcessors();
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(maximumPoolSize);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("worker-" + "-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
logInfo(ourLog, "Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size());
StopWatch sw = new StopWatch();
try {
executorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
logInfo(ourLog, "Slot become available after {}ms", sw.getMillis());
}
};
myExecutor = new ThreadPoolExecutor(
1,
maximumPoolSize,
0L,
TimeUnit.MILLISECONDS,
executorQueue,
threadFactory,
rejectedExecutionHandler);
}
private Future<?> updateRows(List<Map<String, Object>> theRows) {
Runnable task = () -> {
StopWatch sw = new StopWatch();
getTxTemplate().execute(t -> {
// Loop through rows
assert theRows != null;
for (Map<String, Object> nextRow : theRows) {
Map<String, Long> newValues = new HashMap<>();
MandatoryKeyMap<String, Object> nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow);
// Apply calculators
for (Map.Entry<String, Function<MandatoryKeyMap<String, Object>, Long>> nextCalculatorEntry : myCalculators.entrySet()) {
String nextColumn = nextCalculatorEntry.getKey();
Function<MandatoryKeyMap<String, Object>, Long> nextCalculator = nextCalculatorEntry.getValue();
Long value = nextCalculator.apply(nextRowMandatoryKeyMap);
newValues.put(nextColumn, value);
}
// Generate update SQL
StringBuilder sqlBuilder = new StringBuilder();
List<Number> arguments = new ArrayList<>();
sqlBuilder.append("UPDATE ");
sqlBuilder.append(getTableName());
sqlBuilder.append(" SET ");
for (Map.Entry<String, Long> nextNewValueEntry : newValues.entrySet()) {
if (arguments.size() > 0) {
sqlBuilder.append(", ");
}
sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?");
arguments.add(nextNewValueEntry.getValue());
}
sqlBuilder.append(" WHERE SP_ID = ?");
arguments.add((Number) nextRow.get("SP_ID"));
// Apply update SQL
newJdbcTemplate().update(sqlBuilder.toString(), arguments.toArray());
}
return theRows.size();
});
logInfo(ourLog, "Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString());
};
return myExecutor.submit(task);
}
public CalculateHashesTask addCalculator(String theColumnName, Function<MandatoryKeyMap<String, Object>, Long> theConsumer) {
Validate.isTrue(myCalculators.containsKey(theColumnName) == false);
myCalculators.put(theColumnName, theConsumer);
return this;
}
private class MyRowCallbackHandler implements RowCallbackHandler {
private List<Map<String, Object>> myRows = new ArrayList<>();
private List<Future<?>> myFutures = new ArrayList<>();
@Override
public void processRow(ResultSet rs) throws SQLException {
Map<String, Object> row = new ColumnMapRowMapper().mapRow(rs, 0);
myRows.add(row);
if (myRows.size() >= myBatchSize) {
submitNext();
}
}
private void submitNext() {
if (myRows.size() > 0) {
myFutures.add(updateRows(myRows));
myRows = new ArrayList<>();
}
}
public List<Future<?>> getFutures() {
return myFutures;
}
public void done() {
if (myRows.size() > 0) {
submitNext();
}
}
}
public static class MandatoryKeyMap<K, V> extends ForwardingMap<K, V> {
private final Map<K, V> myWrap;
public MandatoryKeyMap(Map<K, V> theWrap) {
myWrap = theWrap;
}
@Override
public V get(Object theKey) {
if (!containsKey(theKey)) {
throw new IllegalArgumentException("No key: " + theKey);
}
return super.get(theKey);
}
public String getString(String theKey) {
return (String) get(theKey);
}
@Override
protected Map<K, V> delegate() {
return myWrap;
}
public String getResourceType() {
return getString("RES_TYPE");
}
public String getParamName() {
return getString("SP_NAME");
return shouldSkip;
} catch (SQLException e) {
logInfo(ourLog, "Error retrieving table names, skipping task");
return true;
}
}
}

View File

@ -0,0 +1,37 @@
package ca.uhn.fhir.jpa.migrate.taskdef;
/*-
* #%L
* HAPI FHIR JPA Server - Migration
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.util.VersionEnum;
public class CalculateOrdinalDatesTask extends BaseColumnCalculatorTask {
public CalculateOrdinalDatesTask(VersionEnum theRelease, String theVersion) {
super(theRelease, theVersion);
setDescription("Calculate SP_LOW_VALUE_DATE_ORDINAL and SP_HIGH_VALUE_DATE_ORDINAL based on existing SP_VALUE_LOW and SP_VALUE_HIGH date values in Date Search Params");
setWhereClause("(SP_VALUE_LOW_DATE_ORDINAL IS NULL AND SP_VALUE_LOW IS NOT NULL) OR (SP_VALUE_HIGH_DATE_ORDINAL IS NULL AND SP_VALUE_HIGH IS NOT NULL)");
}
@Override
protected boolean shouldSkipTask() {
return false; // TODO Is there a case where we should just not do this?
}
}

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.migrate.taskdef.AddColumnTask;
import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask;
import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableColumnTypeTask;
import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask;
import ca.uhn.fhir.jpa.migrate.taskdef.CalculateOrdinalDatesTask;
import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks;
import ca.uhn.fhir.jpa.migrate.tasks.api.Builder;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
@ -69,6 +70,19 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
version.onTable("HFJ_RES_VER").dropColumn("20200218.2", "FORCED_ID_PID");
version.onTable("HFJ_RES_VER").addForeignKey("20200218.3", "FK_RESOURCE_HISTORY_RESOURCE").toColumn("RES_ID").references("HFJ_RESOURCE", "RES_ID");
version.onTable("HFJ_RES_VER").modifyColumn("20200220.1", "RES_ID").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG);
//
// Add support for integer comparisons during day-precision date search.
Builder.BuilderWithTableName spidxDate = version.onTable("HFJ_SPIDX_DATE");
spidxDate.addColumn("20200225.1", "SP_VALUE_LOW_DATE_ORDINAL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT);
spidxDate.addColumn("20200225.2", "SP_VALUE_HIGH_DATE_ORDINAL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT);
spidxDate.addTask(new CalculateOrdinalDatesTask(VersionEnum.V4_3_0, "20200225.3")
.addCalculator("SP_VALUE_LOW_DATE_ORDINAL", t -> ResourceIndexedSearchParamDate.calculateOrdinalValue(t.getDate("SP_VALUE_LOW")))
.addCalculator("SP_VALUE_HIGH_DATE_ORDINAL", t -> ResourceIndexedSearchParamDate.calculateOrdinalValue(t.getDate("SP_VALUE_HIGH")))
.setColumnName("SP_VALUE_LOW_DATE_ORDINAL") //It doesn't matter which of the two we choose as they will both be null.
);
//
// Drop unused column
version.onTable("HFJ_RESOURCE").dropIndex("20200419.1", "IDX_RES_PROFILE");
@ -527,8 +541,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.withColumns("HASH_IDENTITY", "SP_LATITUDE", "SP_LONGITUDE");
spidxCoords
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.5")
.setColumnName("HASH_IDENTITY")
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
.setColumnName("HASH_IDENTITY")
);
}
@ -550,8 +564,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.dropIndex("20180903.9", "IDX_SP_DATE");
spidxDate
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.10")
.setColumnName("HASH_IDENTITY")
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
.setColumnName("HASH_IDENTITY")
);
}
@ -571,8 +585,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.withColumns("HASH_IDENTITY", "SP_VALUE");
spidxNumber
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.14")
.setColumnName("HASH_IDENTITY")
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
.setColumnName("HASH_IDENTITY")
);
}
@ -608,10 +622,10 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
.withColumns("HASH_IDENTITY_SYS_UNITS", "SP_VALUE");
spidxQuantity
.addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.22")
.setColumnName("HASH_IDENTITY")
.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME")))
.addCalculator("HASH_IDENTITY_AND_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_UNITS")))
.addCalculator("HASH_IDENTITY_SYS_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_SYSTEM"), t.getString("SP_UNITS")))
.setColumnName("HASH_IDENTITY")
);
}

View File

@ -154,7 +154,7 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex {
public abstract IQueryParameterType toQueryParameterType();
public boolean matches(IQueryParameterType theParam) {
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
throw new UnsupportedOperationException("No parameter matcher for " + theParam);
}

View File

@ -61,6 +61,10 @@ public class ModelConfig {
private Set<Subscription.SubscriptionChannelType> mySupportedSubscriptionTypes = new HashSet<>();
private String myEmailFromAddress = "noreply@unknown.com";
private String myWebsocketContextPath = DEFAULT_WEBSOCKET_CONTEXT_PATH;
/**
* Update setter javadoc if default changes.
*/
private boolean myUseOrdinalDatesForDayPrecisionSearches = true;
/**
* Constructor
@ -258,7 +262,6 @@ public class ModelConfig {
myTreatReferencesAsLogical = new HashSet<>();
}
myTreatReferencesAsLogical.add(theTreatReferencesAsLogical);
}
/**
@ -368,6 +371,43 @@ public class ModelConfig {
myWebsocketContextPath = theWebsocketContextPath;
}
/**
* <p>
* Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in
* {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using
* precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}.
*
* For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an
* ordinal {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()}
* and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()}
* </p>
* Default is {@literal true} beginning in HAPI FHIR 4.3.
* </p>
*
* @since 4.3
*/
public void setUseOrdinalDatesForDayPrecisionSearches(boolean theUseOrdinalDates) {
myUseOrdinalDatesForDayPrecisionSearches = theUseOrdinalDates;
}
/**
* <p>
* Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in
* {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using
* precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}.
*
* For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an
* integer representing the ordinal date {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()}
* and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()}
* </p>
* Default is {@literal true} beginning in HAPI FHIR 4.3.
* </p>
*
* @since 4.3
*/
public boolean getUseOrdinalDatesForDayPrecisionSearches() {
return myUseOrdinalDatesForDayPrecisionSearches;
}
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
Validate.notBlank(theUrl, "Base URL must not be null or empty");

View File

@ -26,6 +26,8 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.util.DateUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
@ -55,6 +57,16 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
@Temporal(TemporalType.TIMESTAMP)
@Field
public Date myValueLow;
/**
* Field which stores an integer representation of YYYYMDD as calculated by Calendar
* e.g. 2019-01-20 -> 20190120
*/
@Column(name="SP_VALUE_LOW_DATE_ORDINAL")
public Integer myValueLowDateOrdinal;
@Column(name="SP_VALUE_HIGH_DATE_ORDINAL")
public Integer myValueHighDateOrdinal;
@Transient
private transient String myOriginalValue;
@Id
@ -78,21 +90,58 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
/**
* Constructor
*/
public ResourceIndexedSearchParamDate(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, Date theLow, Date theHigh, String theOriginalValue) {
public ResourceIndexedSearchParamDate(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, Date theLow, String theLowString, Date theHigh, String theHighString, String theOriginalValue) {
setPartitionSettings(thePartitionSettings);
setResourceType(theResourceType);
setParamName(theParamName);
setValueLow(theLow);
setValueHigh(theHigh);
if (theHigh != null && theHighString == null) {
theHighString = DateUtils.convertDateToIso8601String(theHigh);
}
if (theLow != null && theLowString == null) {
theLowString = DateUtils.convertDateToIso8601String(theLow);
}
computeValueHighDateOrdinal(theHighString);
computeValueLowDateOrdinal(theLowString);
myOriginalValue = theOriginalValue;
}
private void computeValueHighDateOrdinal(String theHigh) {
if (!StringUtils.isBlank(theHigh)) {
this.myValueHighDateOrdinal = generateOrdinalDateInteger(theHigh);
}
}
private int generateOrdinalDateInteger(String theDateString){
if (theDateString.contains("T")) {
theDateString = theDateString.substring(0, theDateString.indexOf("T"));
}
theDateString = theDateString.replace("-", "");
return Integer.valueOf(theDateString);
}
private void computeValueLowDateOrdinal(String theLow) {
if (StringUtils.isNotBlank(theLow)) {
this.myValueLowDateOrdinal = generateOrdinalDateInteger(theLow);
}
}
public Integer getValueLowDateOrdinal() {
return myValueLowDateOrdinal;
}
public Integer getValueHighDateOrdinal() {
return myValueHighDateOrdinal;
}
@Override
public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
super.copyMutableValuesFrom(theSource);
ResourceIndexedSearchParamDate source = (ResourceIndexedSearchParamDate) theSource;
myValueHigh = source.myValueHigh;
myValueLow = source.myValueLow;
myValueHighDateOrdinal = source.myValueHighDateOrdinal;
myValueLowDateOrdinal = source.myValueLowDateOrdinal;
myHashIdentity = source.myHashIdentity;
}
@ -203,20 +252,34 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
@SuppressWarnings("ConstantConditions")
@Override
public boolean matches(IQueryParameterType theParam) {
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
if (!(theParam instanceof DateParam)) {
return false;
}
DateParam date = (DateParam) theParam;
DateRangeParam range = new DateRangeParam(date);
DateParam dateParam = (DateParam) theParam;
DateRangeParam range = new DateRangeParam(dateParam);
boolean result;
if (theUseOrdinalDatesForDayComparison) {
result = matchesOrdinalDateBounds(range);
result = matchesDateBounds(range);
} else {
result = matchesDateBounds(range);
}
return result;
}
private boolean matchesDateBounds(DateRangeParam range) {
Date lowerBound = range.getLowerBoundAsInstant();
Date upperBound = range.getUpperBoundAsInstant();
if (lowerBound == null && upperBound == null) {
// should never happen
return false;
}
boolean result = true;
if (lowerBound != null) {
result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound));
@ -229,4 +292,31 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
return result;
}
private boolean matchesOrdinalDateBounds(DateRangeParam range) {
boolean result = true;
Integer lowerBoundAsDateInteger = range.getLowerBoundAsDateInteger();
Integer upperBoundAsDateInteger = range.getUpperBoundAsDateInteger();
if (upperBoundAsDateInteger == null && lowerBoundAsDateInteger == null) {
return false;
}
if (lowerBoundAsDateInteger != null) {
//TODO as we run into equality issues
result &= (myValueLowDateOrdinal.equals(lowerBoundAsDateInteger) || myValueLowDateOrdinal > lowerBoundAsDateInteger);
result &= (myValueHighDateOrdinal.equals(lowerBoundAsDateInteger) || myValueHighDateOrdinal > lowerBoundAsDateInteger);
}
if (upperBoundAsDateInteger != null) {
result &= (myValueHighDateOrdinal.equals(upperBoundAsDateInteger) || myValueHighDateOrdinal < upperBoundAsDateInteger);
result &= (myValueLowDateOrdinal.equals(upperBoundAsDateInteger) || myValueLowDateOrdinal < upperBoundAsDateInteger);
}
return result;
}
public static Long calculateOrdinalValue(Date theDate) {
if (theDate == null) {
return null;
}
return (long) DateUtils.convertDatetoDayInteger(theDate);
}
}

View File

@ -164,7 +164,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP
}
@Override
public boolean matches(IQueryParameterType theParam) {
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
if (!(theParam instanceof NumberParam)) {
return false;
}

View File

@ -253,7 +253,7 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc
}
@Override
public boolean matches(IQueryParameterType theParam) {
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
if (!(theParam instanceof QuantityParam)) {
return false;
}

View File

@ -270,7 +270,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP
}
@Override
public boolean matches(IQueryParameterType theParam) {
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
if (!(theParam instanceof StringParam)) {
return false;
}

View File

@ -256,7 +256,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa
}
@Override
public boolean matches(IQueryParameterType theParam) {
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
if (!(theParam instanceof TokenParam)) {
return false;
}

View File

@ -201,7 +201,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
}
@Override
public boolean matches(IQueryParameterType theParam) {
public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
if (!(theParam instanceof UriParam)) {
return false;
}

View File

@ -5,6 +5,7 @@ import org.junit.Before;
import org.junit.Test;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Calendar;
import java.util.Date;
@ -36,8 +37,8 @@ public class ResourceIndexedSearchParamDateTest {
@Test
public void equalsIsTrueForMatchingNullDates() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, null, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", null, null, null, null, "SomeValue");
assertTrue(param.equals(param2));
assertTrue(param2.equals(param));
@ -46,8 +47,8 @@ public class ResourceIndexedSearchParamDateTest {
@Test
public void equalsIsTrueForMatchingDates() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1B, date2B, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1B, null, date2B, null, "SomeValue");
assertTrue(param.equals(param2));
assertTrue(param2.equals(param));
@ -56,8 +57,8 @@ public class ResourceIndexedSearchParamDateTest {
@Test
public void equalsIsTrueForMatchingTimeStampsThatMatch() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1B, timestamp2B, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1B, null, timestamp2B, null, "SomeValue");
assertTrue(param.equals(param2));
assertTrue(param2.equals(param));
@ -68,8 +69,8 @@ public class ResourceIndexedSearchParamDateTest {
// other will be equivalent but will be a java.sql.Timestamp. Equals should work in both directions.
@Test
public void equalsIsTrueForMixedTimestampsAndDates() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue");
assertTrue(param.equals(param2));
assertTrue(param2.equals(param));
@ -78,8 +79,8 @@ public class ResourceIndexedSearchParamDateTest {
@Test
public void equalsIsFalseForNonMatchingDates() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date2A, date1A, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date2A, null, date1A, null, "SomeValue");
assertFalse(param.equals(param2));
assertFalse(param2.equals(param));
@ -88,8 +89,8 @@ public class ResourceIndexedSearchParamDateTest {
@Test
public void equalsIsFalseForNonMatchingDatesNullCase() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", null, null, null, null, "SomeValue");
assertFalse(param.equals(param2));
assertFalse(param2.equals(param));
@ -98,8 +99,8 @@ public class ResourceIndexedSearchParamDateTest {
@Test
public void equalsIsFalseForNonMatchingTimeStamps() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp2A, timestamp1A, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp2A, null, timestamp1A, null, "SomeValue");
assertFalse(param.equals(param2));
assertFalse(param2.equals(param));
@ -108,8 +109,8 @@ public class ResourceIndexedSearchParamDateTest {
@Test
public void equalsIsFalseForMixedTimestampsAndDatesThatDoNotMatch() {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp2A, timestamp1A, "SomeValue");
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue");
ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp2A, null, timestamp1A, null, "SomeValue");
assertFalse(param.equals(param2));
assertFalse(param2.equals(param));

View File

@ -584,9 +584,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
Date start = extractValueAsDate(myPeriodStartValueChild, theValue);
String startAsString = extractValueAsString(myPeriodStartValueChild, theValue);
Date end = extractValueAsDate(myPeriodEndValueChild, theValue);
String endAsString = extractValueAsString(myPeriodEndValueChild, theValue);
if (start != null || end != null) {
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), start, end, startAsString);
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings ,theResourceType, theSearchParam.getName(), start, startAsString, end, endAsString, startAsString);
theParams.add(nextEntity);
}
}
@ -595,13 +596,16 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
List<IPrimitiveType<Date>> values = extractValuesAsFhirDates(myTimingEventValueChild, theValue);
TreeSet<Date> dates = new TreeSet<>();
TreeSet<String> dateStrings = new TreeSet<>();
String firstValue = null;
String finalValue = null;
for (IPrimitiveType<Date> nextEvent : values) {
if (nextEvent.getValue() != null) {
dates.add(nextEvent.getValue());
if (firstValue == null) {
firstValue = nextEvent.getValueAsString();
}
finalValue = nextEvent.getValueAsString();
}
}
@ -613,14 +617,20 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
if ("Period".equals(boundsType)) {
Date start = extractValueAsDate(myPeriodStartValueChild, bounds.get());
Date end = extractValueAsDate(myPeriodEndValueChild, bounds.get());
String endString = extractValueAsString(myPeriodEndValueChild, bounds.get());
dates.add(start);
dates.add(end);
//TODO Check if this logic is valid. Does the start of the first period indicate a lower bound??
if (firstValue == null) {
firstValue = extractValueAsString(myPeriodStartValueChild, bounds.get());
}
finalValue = endString;
}
}
}
if (!dates.isEmpty()) {
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), dates.first(), dates.last(), firstValue);
ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), dates.first(), firstValue, dates.last(), finalValue, firstValue);
theParams.add(nextEntity);
}
}
@ -811,7 +821,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
private void addDateTimeTypes(String theResourceType, Set<ResourceIndexedSearchParamDate> theParams, RuntimeSearchParam theSearchParam, IBase theValue) {
IPrimitiveType<Date> nextBaseDateTime = (IPrimitiveType<Date>) theValue;
if (nextBaseDateTime.getValue() != null) {
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), nextBaseDateTime.getValue(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString());
ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(myPartitionSettings ,theResourceType, theSearchParam.getName(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString(), nextBaseDateTime.getValueAsString());
theParams.add(param);
}
}

View File

@ -140,7 +140,7 @@ public final class ResourceIndexedSearchParams {
return myPopulatedResourceLinkParameters;
}
public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) {
if (theParamDef == null) {
return false;
}
@ -177,7 +177,7 @@ public final class ResourceIndexedSearchParams {
}
Predicate<BaseResourceIndexedSearchParam> namedParamPredicate = param ->
param.getParamName().equalsIgnoreCase(theParamName) &&
param.matches(theParam);
param.matches(theParam, theUseOrdinalDatesForDayComparison);
return resourceParams.stream().anyMatch(namedParamPredicate);
}

View File

@ -199,7 +199,7 @@ public class InMemoryResourceMatcher {
if (theSearchParams == null) {
return InMemoryMatchResult.successfulMatch();
} else {
return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theModelConfig, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theModelConfig, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams, myModelConfig.getUseOrdinalDatesForDayPrecisionSearches())));
}
case COMPOSITE:
case HAS:
@ -216,8 +216,8 @@ public class InMemoryResourceMatcher {
}
}
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams) {
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token));
private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams,boolean theUseOrdinalDatesForDayComparison) {
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token, theUseOrdinalDatesForDayComparison));
}
private boolean hasChain(List<List<IQueryParameterType>> theAndOrParams) {

View File

@ -149,8 +149,8 @@ public class InMemoryResourceMatcherR5Test {
@Test
public void testDateSupportedOps() {
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, true, true, false);
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN, true, false, false);
testDateSupportedOp(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, true, true, false);
testDateSupportedOp(ParamPrefixEnum.EQUAL, false, true, false);
testDateSupportedOp(ParamPrefixEnum.LESSTHAN_OR_EQUALS, false, true, true);
testDateSupportedOp(ParamPrefixEnum.LESSTHAN, false, false, true);
@ -166,7 +166,7 @@ public class InMemoryResourceMatcherR5Test {
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + OBSERVATION_DATE, myObservation, mySearchParams);
assertTrue(result.getUnsupportedReason(), result.supported());
assertEquals(result.matched(), theSame);
assertEquals(theSame, result.matched());
}
{
InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + LATE_DATE, myObservation, mySearchParams);
@ -209,7 +209,7 @@ public class InMemoryResourceMatcherR5Test {
private ResourceIndexedSearchParams extractDateSearchParam(Observation theObservation) {
ResourceIndexedSearchParams retval = new ResourceIndexedSearchParams();
BaseDateTimeType dateValue = (BaseDateTimeType) theObservation.getEffective();
ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValue(), dateValue.getValueAsString());
ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValueAsString());
retval.myDateParams.add(dateParam);
return retval;
}