diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/ReindexResourceDAO.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/ReindexResourceDAO.java index 3d6df64c5f6..5e0c156d783 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/ReindexResourceDAO.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/ReindexResourceDAO.java @@ -49,7 +49,7 @@ public class ReindexResourceDAO extends ResourceDAOImpl { private final ParameterDAO parameterDao; private static final String PICK_SINGLE_LOGICAL_RESOURCE = "" - + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid " + + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid, lr.parameter_hash " + " FROM logical_resources lr " + " WHERE lr.logical_resource_id = ? " + " AND lr.is_deleted = 'N' " @@ -57,7 +57,7 @@ public class ReindexResourceDAO extends ResourceDAOImpl { ; private static final String PICK_SINGLE_RESOURCE = "" - + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid " + + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid, lr.parameter_hash " + " FROM logical_resources lr " + " WHERE lr.resource_type_id = ? " + " AND lr.logical_id = ? " @@ -66,7 +66,7 @@ public class ReindexResourceDAO extends ResourceDAOImpl { ; private static final String PICK_SINGLE_RESOURCE_TYPE = "" - + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid " + + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid, lr.parameter_hash " + " FROM logical_resources lr " + " WHERE lr.resource_type_id = ? " + " AND lr.is_deleted = 'N' " @@ -75,7 +75,7 @@ public class ReindexResourceDAO extends ResourceDAOImpl { ; private static final String PICK_ANY_RESOURCE = "" - + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid " + + " SELECT lr.logical_resource_id, lr.resource_type_id, lr.logical_id, lr.reindex_txid, lr.parameter_hash " + " FROM logical_resources lr " + " WHERE lr.is_deleted = 'N' " + " AND lr.reindex_tstamp < ? " @@ -152,7 +152,7 @@ protected ResourceIndexRecord getResource(Instant reindexTstamp, Long logicalRes } ResultSet rs = stmt.executeQuery(); if (rs.next()) { - result = new ResourceIndexRecord(rs.getLong(1), rs.getInt(2), rs.getString(3), rs.getLong(4)); + result = new ResourceIndexRecord(rs.getLong(1), rs.getInt(2), rs.getString(3), rs.getLong(4), rs.getString(5)); } } catch (SQLException x) { logger.log(Level.SEVERE, select, x); @@ -219,7 +219,7 @@ protected ResourceIndexRecord getNextResource(SecureRandom random, Instant reind } ResultSet rs = stmt.executeQuery(); if (rs.next()) { - result = new ResourceIndexRecord(rs.getLong(1), rs.getInt(2), rs.getString(3), rs.getLong(4)); + result = new ResourceIndexRecord(rs.getLong(1), rs.getInt(2), rs.getString(3), rs.getLong(4), rs.getString(5)); } } catch (SQLException x) { logger.log(Level.SEVERE, select, x); @@ -327,12 +327,13 @@ public ResourceIndexRecord getResourceToReindex(Instant reindexTstamp, Long logi /** * Reindex the resource by deleting existing parameters and replacing them with those passed in. * @param tablePrefix the table prefix - * @param parameters the extracted parameters + * @param parameters A collection of search parameters to be persisted along with the passed Resource + * @param parameterHashB64 the Base64 encoded SHA-256 hash of parameters * @param logicalId the logical id * @param logicalResourceId the logical resource id * @throws Exception */ - public void updateParameters(String tablePrefix, List parameters, String logicalId, long logicalResourceId) throws Exception { + public void updateParameters(String tablePrefix, List parameters, String parameterHashB64, String logicalId, long logicalResourceId) throws Exception { final String METHODNAME = "updateParameters() for " + tablePrefix + "/" + logicalId; logger.entering(CLASSNAME, METHODNAME); @@ -355,6 +356,35 @@ identityCache, getResourceReferenceDAO(), getTransactionData())) { throw translator.translate(x); } } + + // Update the parameter hash in the LOGICAL_RESOURCES table + updateParameterHash(connection, logicalResourceId, parameterHashB64); + logger.exiting(CLASSNAME, METHODNAME); } + + /** + * Updates the parameter hash in the LOGICAL_RESOURCES table. + * @param conn the connection + * @param logicalResourceId the logical resource ID + * @param parameterHashB64 the Base64 encoded SHA-256 hash of parameters + * @throws SQLException + */ + protected void updateParameterHash(Connection conn, long logicalResourceId, String parameterHashB64) throws SQLException { + final String SQL = "UPDATE logical_resources SET parameter_hash = ? WHERE logical_resource_id = ?"; + try (PreparedStatement stmt = conn.prepareStatement(SQL)) { + // bind parameters + stmt.setString(1, parameterHashB64); + stmt.setLong(2, logicalResourceId); + long dbCallStartTime = System.nanoTime(); + stmt.executeUpdate(); + double dbCallDuration = (System.nanoTime() - dbCallStartTime) / 1e6; + if (logger.isLoggable(Level.FINEST)) { + logger.finest("Update parameter_hash '" + parameterHashB64 + "' for logicalResourceId '" + logicalResourceId + "' [took " + dbCallDuration + " ms]"); + } + } catch (SQLException x) { + logger.log(Level.SEVERE, SQL, x); + throw translator.translate(x); + } + } } diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/api/ResourceIndexRecord.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/api/ResourceIndexRecord.java index 5b54d7dc95a..0f61707e38d 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/api/ResourceIndexRecord.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dao/api/ResourceIndexRecord.java @@ -30,11 +30,15 @@ public class ResourceIndexRecord { // Deletion flag for the resource. Set when we read the resource private boolean deleted; - public ResourceIndexRecord(long logicalResourceId, int resourceTypeId, String logicalId, long transactionId) { + // Base64-encoded SHA-256 hash of the search parameters + private String parameterHash; + + public ResourceIndexRecord(long logicalResourceId, int resourceTypeId, String logicalId, long transactionId, String parameterHash) { this.logicalResourceId = logicalResourceId; this.resourceTypeId = resourceTypeId; this.logicalId = logicalId; this.transactionId = transactionId; + this.parameterHash = parameterHash; } /** @@ -92,4 +96,20 @@ public boolean isDeleted() { public void setDeleted(boolean deleted) { this.deleted = deleted; } + + /** + * Gets the Base64-encoded SHA-256 hash of the search parameters. + * @return the Base64-encoded SHA-256 hash + */ + public String getParameterHash() { + return parameterHash; + } + + /** + * Gets the Base64-encoded SHA-256 hash of the search parameters. + * @param parameterHash the Base64-encoded SHA-256 hash + */ + public void setParameterHash(String parameterHash) { + this.parameterHash = parameterHash; + } } diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/CompositeParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/CompositeParmVal.java index 549ddfb90cc..9736930da9a 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/CompositeParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/CompositeParmVal.java @@ -25,13 +25,6 @@ public CompositeParmVal() { component = new ArrayList<>(2); } - /** - * We know our type, so we can call the correct method on the visitor - */ - public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { - visitor.visit(this); - } - /** * @return get the list of components in this composite parameter */ @@ -54,4 +47,42 @@ public void addComponent(ExtractedParameterValue... component) { this.component.add(value); } } + + /** + * We know our type, so we can call the correct method on the visitor + */ + @Override + public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { + visitor.visit(this); + } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + CompositeParmVal other = (CompositeParmVal) o; + int retVal; + + List thisComponent = this.getComponent(); + List otherComponent = other.getComponent(); + if (thisComponent != null || otherComponent != null) { + if (thisComponent == null) { + return -1; + } else if (otherComponent == null) { + return 1; + } + Integer thisSize = thisComponent.size(); + Integer otherSize = otherComponent.size(); + for (int i=0; i { // The name (code) of this parameter private String name; @@ -22,8 +22,9 @@ public abstract class ExtractedParameterValue { // The resource type associated with this parameter private String resourceType; - // The base resource name - private String base; + // URL and version of search parameter + private String url; + private String version; /** * Protected constructor @@ -52,20 +53,6 @@ public void setResourceType(String resourceType) { */ public abstract void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException; - /** - * @return the base - */ - public String getBase() { - return this.base; - } - - /** - * @param base the base to set - */ - public void setBase(String base) { - this.base = base; - } - /** * @return the wholeSystem */ @@ -93,4 +80,99 @@ public String getName() { public void setName(String name) { this.name = name; } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * @param version the version to set + */ + public void setVersion(String version) { + this.version = version; + } + + @Override + public int compareTo(ExtractedParameterValue o) { + int retVal; + String thisName = this.getName(); + String otherName = o.getName(); + if (thisName != null || otherName != null) { + if (thisName == null) { + return -1; + } else if (otherName == null) { + return 1; + } + retVal = thisName.compareTo(otherName); + if (retVal != 0) { + return retVal; + } + } + String thisUrl = this.getUrl(); + String otherUrl = o.getUrl(); + if (thisUrl != null || otherUrl != null) { + if (thisUrl == null) { + return -1; + } else if (otherUrl == null) { + return 1; + } + retVal = thisUrl.compareTo(otherUrl); + if (retVal != 0) { + return retVal; + } + } + String thisVersion = this.getVersion(); + String otherVersion = o.getVersion(); + if (thisVersion != null || otherVersion != null) { + if (thisVersion == null) { + return -1; + } else if (otherVersion == null) { + return 1; + } + retVal = thisVersion.compareTo(otherVersion); + if (retVal != 0) { + return retVal; + } + } + String thisClass = this.getClass().getName(); + String otherClass = o.getClass().getName(); + if (thisClass != null || otherClass != null) { + if (thisClass == null) { + return -1; + } else if (otherClass == null) { + return 1; + } + retVal = thisClass.compareTo(otherClass); + if (retVal != 0) { + return retVal; + } + } + return compareToInner(o); + } + + /** + * Additional extracted parameter value comparisions when the same class. + * @param o an extracted parameter value to compare to + * @return a negative integer, zero, or a positive integer as this extracted parameter value + * is less than, equal to, or greater than the specified extracted parameter value. + */ + protected abstract int compareToInner(ExtractedParameterValue o); + } \ No newline at end of file diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/LocationParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/LocationParmVal.java index da67655aedc..0614113b4a4 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/LocationParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/LocationParmVal.java @@ -13,8 +13,8 @@ */ public class LocationParmVal extends ExtractedParameterValue { - private Double valueLongitude; private Double valueLatitude; + private Double valueLongitude; /** * Public constructor @@ -23,14 +23,6 @@ public LocationParmVal() { super(); } - public Double getValueLongitude() { - return valueLongitude; - } - - public void setValueLongitude(Double valueLongitude) { - this.valueLongitude = valueLongitude; - } - public Double getValueLatitude() { return valueLatitude; } @@ -39,10 +31,55 @@ public void setValueLatitude(Double valueLatitude) { this.valueLatitude = valueLatitude; } + public Double getValueLongitude() { + return valueLongitude; + } + + public void setValueLongitude(Double valueLongitude) { + this.valueLongitude = valueLongitude; + } + /** * We know our type, so we can call the correct method on the visitor */ + @Override public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { visitor.visit(this); } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + LocationParmVal other = (LocationParmVal) o; + int retVal; + + Double thisValueLatitude = this.getValueLatitude(); + Double otherValueLatitude = other.getValueLatitude(); + if (thisValueLatitude != null || otherValueLatitude != null) { + if (thisValueLatitude == null) { + return -1; + } else if (otherValueLatitude == null) { + return 1; + } + retVal = thisValueLatitude.compareTo(otherValueLatitude); + if (retVal != 0) { + return retVal; + } + } + + Double thisValueLongitude = this.getValueLongitude(); + Double otherValueLongitude = other.getValueLongitude(); + if (thisValueLongitude != null || otherValueLongitude != null) { + if (thisValueLongitude == null) { + return -1; + } else if (otherValueLongitude == null) { + return 1; + } + retVal = thisValueLongitude.compareTo(otherValueLongitude); + if (retVal != 0) { + return retVal; + } + } + + return 0; + } } \ No newline at end of file diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/NumberParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/NumberParmVal.java index 4897bb48f3e..7b20b811fae 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/NumberParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/NumberParmVal.java @@ -19,15 +19,13 @@ public class NumberParmVal extends ExtractedParameterValue { private BigDecimal valueNumberLow; private BigDecimal valueNumberHigh; - // The SearchParameter base type. If "Resource", then this is a Resource-level attribute - private String base; - /** * Public constructor */ public NumberParmVal() { super(); } + public BigDecimal getValueNumber() { return valueNumber; } @@ -55,7 +53,58 @@ public void setValueNumberHigh(BigDecimal valueNumberHigh) { /** * We know our type, so we can call the correct method on the visitor */ + @Override public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { visitor.visit(this); } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + NumberParmVal other = (NumberParmVal) o; + int retVal; + + BigDecimal thisValueNumber = this.getValueNumber(); + BigDecimal otherValueNumber = other.getValueNumber(); + if (thisValueNumber != null || otherValueNumber != null) { + if (thisValueNumber == null) { + return -1; + } else if (otherValueNumber == null) { + return 1; + } + retVal = thisValueNumber.compareTo(otherValueNumber); + if (retVal != 0) { + return retVal; + } + } + + BigDecimal thisValueNumberLow = this.getValueNumberLow(); + BigDecimal otherValueNumberLow = other.getValueNumberLow(); + if (thisValueNumberLow != null || otherValueNumberLow != null) { + if (thisValueNumberLow == null) { + return -1; + } else if (otherValueNumberLow == null) { + return 1; + } + retVal = thisValueNumberLow.compareTo(otherValueNumberLow); + if (retVal != 0) { + return retVal; + } + } + + BigDecimal thisValueNumberHigh = this.getValueNumberHigh(); + BigDecimal otherValueNumberHigh = other.getValueNumberHigh(); + if (thisValueNumberHigh != null || otherValueNumberHigh != null) { + if (thisValueNumberHigh == null) { + return -1; + } else if (otherValueNumberHigh == null) { + return 1; + } + retVal = thisValueNumberHigh.compareTo(otherValueNumberHigh); + if (retVal != 0) { + return retVal; + } + } + + return 0; + } } \ No newline at end of file diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/QuantityParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/QuantityParmVal.java index 8098760e231..45651f75139 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/QuantityParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/QuantityParmVal.java @@ -83,4 +83,81 @@ public void setValueNumberHigh(BigDecimal valueNumberHigh) { public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { visitor.visit(this); } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + QuantityParmVal other = (QuantityParmVal) o; + int retVal; + + BigDecimal thisValueNumber = this.getValueNumber(); + BigDecimal otherValueNumber = other.getValueNumber(); + if (thisValueNumber != null || otherValueNumber != null) { + if (thisValueNumber == null) { + return -1; + } else if (otherValueNumber == null) { + return 1; + } + retVal = thisValueNumber.compareTo(otherValueNumber); + if (retVal != 0) { + return retVal; + } + } + + BigDecimal thisValueNumberLow = this.getValueNumberLow(); + BigDecimal otherValueNumberLow = other.getValueNumberLow(); + if (thisValueNumberLow != null || otherValueNumberLow != null) { + if (thisValueNumberLow == null) { + return -1; + } else if (otherValueNumberLow == null) { + return 1; + } + retVal = thisValueNumberLow.compareTo(otherValueNumberLow); + if (retVal != 0) { + return retVal; + } + } + + BigDecimal thisValueNumberHigh = this.getValueNumberHigh(); + BigDecimal otherValueNumberHigh = other.getValueNumberHigh(); + if (thisValueNumberHigh != null || otherValueNumberHigh != null) { + if (thisValueNumberHigh == null) { + return -1; + } else if (otherValueNumberHigh == null) { + return 1; + } + retVal = thisValueNumberHigh.compareTo(otherValueNumberHigh); + if (retVal != 0) { + return retVal; + } + } + + String thisValueSystem = this.getValueSystem(); + String otherValueSystem = other.getValueSystem(); + if (thisValueSystem != null || otherValueSystem != null) { + if (thisValueSystem == null) { + return -1; + } else if (otherValueSystem == null) { + return 1; + } + retVal = thisValueSystem.compareTo(otherValueSystem); + if (retVal != 0) { + return retVal; + } + } + String thisValueCode = this.getValueCode(); + String otherValueCode = other.getValueCode(); + if (thisValueCode != null || otherValueCode != null) { + if (thisValueCode == null) { + return -1; + } else if (otherValueCode == null) { + return 1; + } + retVal = thisValueCode.compareTo(otherValueCode); + if (retVal != 0) { + return retVal; + } + } + + return 0; + } } \ No newline at end of file diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/ReferenceParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/ReferenceParmVal.java index 41a2d7a3e70..b6f2fb697d2 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/ReferenceParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/ReferenceParmVal.java @@ -9,6 +9,7 @@ import com.ibm.fhir.persistence.exception.FHIRPersistenceException; import com.ibm.fhir.search.SearchConstants.Type; import com.ibm.fhir.search.util.ReferenceValue; +import com.ibm.fhir.search.util.ReferenceValue.ReferenceType; /** * DTO representing external and local reference parameters @@ -48,7 +49,82 @@ public Type getType() { /** * We know our type, so we can call the correct method on the visitor */ + @Override public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { visitor.visit(this); } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + ReferenceParmVal other = (ReferenceParmVal) o; + int retVal; + + ReferenceValue thisRefValue = this.getRefValue(); + ReferenceValue otherRefValue = other.getRefValue(); + if (thisRefValue != null || otherRefValue != null) { + if (thisRefValue == null) { + return -1; + } else if (otherRefValue == null) { + return 1; + } + + String thisTargetResourceType = thisRefValue.getTargetResourceType(); + String otherTargetResourceType = otherRefValue.getTargetResourceType(); + if (thisTargetResourceType != null || otherTargetResourceType != null) { + if (thisTargetResourceType == null) { + return -1; + } else if (otherTargetResourceType == null) { + return 1; + } + retVal = thisTargetResourceType.compareTo(otherTargetResourceType); + if (retVal != 0) { + return retVal; + } + } + + String thisValue = thisRefValue.getValue(); + String otherValue = otherRefValue.getValue(); + if (thisValue != null || otherValue != null) { + if (thisValue == null) { + return -1; + } else if (otherValue == null) { + return 1; + } + retVal = thisValue.compareTo(otherValue); + if (retVal != 0) { + return retVal; + } + } + + ReferenceType thisType = thisRefValue.getType(); + ReferenceType otherType = otherRefValue.getType(); + if (thisType != null || otherType != null) { + if (thisType == null) { + return -1; + } else if (otherType == null) { + return 1; + } + retVal = thisType.compareTo(otherType); + if (retVal != 0) { + return retVal; + } + } + + Integer thisVersion = thisRefValue.getVersion(); + Integer otherVersion = otherRefValue.getVersion(); + if (thisVersion != null || otherVersion != null) { + if (thisVersion == null) { + return -1; + } else if (otherVersion == null) { + return 1; + } + retVal = thisVersion.compareTo(otherVersion); + if (retVal != 0) { + return retVal; + } + } + } + + return 0; + } } \ No newline at end of file diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/StringParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/StringParmVal.java index db3ebebbaea..65f40731263 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/StringParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/StringParmVal.java @@ -34,7 +34,30 @@ public void setValueString(String valueString) { /** * We know our type, so we can call the correct method on the visitor */ + @Override public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { visitor.visit(this); } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + StringParmVal other = (StringParmVal) o; + int retVal; + + String thisValueString = this.getValueString(); + String otherValueString = other.getValueString(); + if (thisValueString != null || otherValueString != null) { + if (thisValueString == null) { + return -1; + } else if (otherValueString == null) { + return 1; + } + retVal = thisValueString.compareTo(otherValueString); + if (retVal != 0) { + return retVal; + } + } + + return 0; + } } \ No newline at end of file diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/TokenParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/TokenParmVal.java index 0d41b046e03..fa2c589667a 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/TokenParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/TokenParmVal.java @@ -56,4 +56,39 @@ public void setValueCode(String valueCode) { public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { visitor.visit(this); } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + TokenParmVal other = (TokenParmVal) o; + int retVal; + + String thisValueSystem = this.getValueSystem(); + String otherValueSystem = other.getValueSystem(); + if (thisValueSystem != null || otherValueSystem != null) { + if (thisValueSystem == null) { + return -1; + } else if (otherValueSystem == null) { + return 1; + } + retVal = thisValueSystem.compareTo(otherValueSystem); + if (retVal != 0) { + return retVal; + } + } + String thisValueCode = this.getValueCode(); + String otherValueCode = other.getValueCode(); + if (thisValueCode != null || otherValueCode != null) { + if (thisValueCode == null) { + return -1; + } else if (otherValueCode == null) { + return 1; + } + retVal = thisValueCode.compareTo(otherValueCode); + if (retVal != 0) { + return retVal; + } + } + + return 0; + } } diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/UriParmVal.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/UriParmVal.java index fce102c7ed2..7a9ea6989ab 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/UriParmVal.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/dto/UriParmVal.java @@ -34,7 +34,14 @@ public void setValueString(String valueString) { /** * We know our type, so we can call the correct method on the visitor */ + @Override public void accept(ExtractedParameterValueVisitor visitor) throws FHIRPersistenceException { // visitor.visit(this); } + + @Override + protected int compareToInner(ExtractedParameterValue o) { + // Not used, so don't implement any real logic + return 0; + } } \ No newline at end of file diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java index 5484b7361ef..3821deb4345 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java @@ -143,9 +143,11 @@ import com.ibm.fhir.persistence.jdbc.exception.FHIRPersistenceDataAccessException; import com.ibm.fhir.persistence.jdbc.exception.FHIRPersistenceFKVException; import com.ibm.fhir.persistence.jdbc.util.CodeSystemsCache; +import com.ibm.fhir.persistence.jdbc.util.ExtractedSearchParameters; import com.ibm.fhir.persistence.jdbc.util.JDBCParameterBuildingVisitor; import com.ibm.fhir.persistence.jdbc.util.JDBCQueryBuilder; import com.ibm.fhir.persistence.jdbc.util.NewQueryBuilder; +import com.ibm.fhir.persistence.jdbc.util.ParameterHashVisitor; import com.ibm.fhir.persistence.jdbc.util.ParameterNamesCache; import com.ibm.fhir.persistence.jdbc.util.ResourceTypesCache; import com.ibm.fhir.persistence.jdbc.util.SqlQueryData; @@ -387,9 +389,9 @@ public SingleResourceResult create(FHIRPersistenceContex ParameterDAO parameterDao = makeParameterDAO(connection); // Persist the Resource DTO. - final String parameterHashB64 = ""; resourceDao.setPersistenceContext(context); - resourceDao.insert(resourceDTO, this.extractSearchParameters(updatedResource, resourceDTO), parameterHashB64, parameterDao); + ExtractedSearchParameters searchParameters = this.extractSearchParameters(updatedResource, resourceDTO); + resourceDao.insert(resourceDTO, searchParameters.getParameters(), searchParameters.getParameterHashB64(), parameterDao); if (log.isLoggable(Level.FINE)) { log.fine("Persisted FHIR Resource '" + resourceDTO.getResourceType() + "/" + resourceDTO.getLogicalId() + "' id=" + resourceDTO.getId() + ", version=" + resourceDTO.getVersionId()); @@ -611,9 +613,9 @@ public SingleResourceResult update(FHIRPersistenceContex createResourceDTO(logicalId, newVersionNumber, lastUpdated, updatedResource); // Persist the Resource DTO. - final String parameterHashB64 = ""; resourceDao.setPersistenceContext(context); - resourceDao.insert(resourceDTO, this.extractSearchParameters(updatedResource, resourceDTO), parameterHashB64, parameterDao); + ExtractedSearchParameters searchParameters = this.extractSearchParameters(updatedResource, resourceDTO); + resourceDao.insert(resourceDTO, searchParameters.getParameters(), searchParameters.getParameterHashB64(), parameterDao); if (log.isLoggable(Level.FINE)) { log.fine("Persisted FHIR Resource '" + resourceDTO.getResourceType() + "/" + resourceDTO.getLogicalId() + "' id=" + resourceDTO.getId() + ", version=" + resourceDTO.getVersionId()); @@ -1417,9 +1419,8 @@ public SingleResourceResult delete(FHIRPersistenceContex resourceDTO.setDeleted(true); // Persist the logically deleted Resource DTO. - final String parameterHashB64 = ""; resourceDao.setPersistenceContext(context); - resourceDao.insert(resourceDTO, null, parameterHashB64, null); + resourceDao.insert(resourceDTO, null, null, null); if (log.isLoggable(Level.FINE)) { log.fine("Persisted FHIR Resource '" + resourceDTO.getResourceType() + "/" + resourceDTO.getLogicalId() + "' id=" + resourceDTO.getId() @@ -1878,135 +1879,199 @@ private TransactionSynchronizationRegistry getTrxSynchRegistry() throws FHIRPers * @return list of extracted search parameters * @throws Exception */ - private List extractSearchParameters(Resource fhirResource, com.ibm.fhir.persistence.jdbc.dto.Resource resourceDTOx) + private ExtractedSearchParameters extractSearchParameters(Resource fhirResource, com.ibm.fhir.persistence.jdbc.dto.Resource resourceDTOx) throws Exception { final String METHODNAME = "extractSearchParameters"; log.entering(CLASSNAME, METHODNAME); Map> map; String code; + String url; + String version; String type; String expression; List allParameters = new ArrayList<>(); + String parameterHashB64 = null; try { - if (fhirResource == null) { - return allParameters; - } - map = SearchUtil.extractParameterValues(fhirResource); + if (fhirResource != null) { - for (Entry> entry : map.entrySet()) { - SearchParameter sp = entry.getKey(); - code = sp.getCode().getValue(); - final boolean wholeSystemParam = isWholeSystem(sp); + map = SearchUtil.extractParameterValues(fhirResource); - // As not to inject any other special handling logic, this is a simple inline check to see if - // _id or _lastUpdated are used, and ignore those extracted values. - if (SPECIAL_HANDLING.contains(code)) { - continue; - } - type = sp.getType().getValue(); - expression = sp.getExpression().getValue(); + for (Entry> entry : map.entrySet()) { + SearchParameter sp = entry.getKey(); + code = sp.getCode().getValue(); + url = sp.getUrl().getValue(); + version = sp.getVersion() != null ? sp.getVersion().getValue(): null; + final boolean wholeSystemParam = isWholeSystem(sp); - if (log.isLoggable(Level.FINE)) { - log.fine("Processing SearchParameter resource: " + fhirResource.getClass().getSimpleName() + ", code: " + code + ", type: " + type + ", expression: " + expression); - } + // As not to inject any other special handling logic, this is a simple inline check to see if + // _id or _lastUpdated are used, and ignore those extracted values. + if (SPECIAL_HANDLING.contains(code)) { + continue; + } + type = sp.getType().getValue(); + expression = sp.getExpression().getValue(); - List values = entry.getValue(); - - if (SearchParamType.COMPOSITE.equals(sp.getType())) { - List components = sp.getComponent(); - FHIRPathEvaluator evaluator = FHIRPathEvaluator.evaluator(); - - for (FHIRPathNode value : values) { - Visitable fhirNode; - EvaluationContext context; - if (value.isResourceNode()) { - fhirNode = value.asResourceNode().resource(); - context = new EvaluationContext((Resource) fhirNode); - } else if (value.isElementNode()) { - fhirNode = value.asElementNode().element(); - context = new EvaluationContext((Element) fhirNode); - } else { - throw new IllegalStateException("Composite parameter expression must select one or more FHIR elements"); - } + if (log.isLoggable(Level.FINE)) { + log.fine("Processing SearchParameter resource: " + fhirResource.getClass().getSimpleName() + ", code: " + code + ", type: " + type + ", expression: " + expression); + } - CompositeParmVal p = new CompositeParmVal(); - p.setName(code); - p.setResourceType(fhirResource.getClass().getSimpleName()); - - for (int i = 0; i < components.size(); i++) { - Component component = components.get(i); - Collection nodes = evaluator.evaluate(context, component.getExpression().getValue()); - if (nodes.isEmpty()){ - if (log.isLoggable(Level.FINER)) { - log.finer("Component expression '" + component.getExpression().getValue() + "' resulted in 0 nodes; " - + "skipping composite parameter '" + code + "'."); - } - continue; + List values = entry.getValue(); + + if (SearchParamType.COMPOSITE.equals(sp.getType())) { + List components = sp.getComponent(); + FHIRPathEvaluator evaluator = FHIRPathEvaluator.evaluator(); + + for (FHIRPathNode value : values) { + Visitable fhirNode; + EvaluationContext context; + if (value.isResourceNode()) { + fhirNode = value.asResourceNode().resource(); + context = new EvaluationContext((Resource) fhirNode); + } else if (value.isElementNode()) { + fhirNode = value.asElementNode().element(); + context = new EvaluationContext((Element) fhirNode); + } else { + throw new IllegalStateException("Composite parameter expression must select one or more FHIR elements"); } - // Alternative: consider pulling the search parameter from the FHIRRegistry instead so we can use versioned references. - // Of course, that would require adding extension-search-params to the Registry which would require the Registry to be tenant-aware. -// SearchParameter compSP = FHIRRegistry.getInstance().getResource(component.getDefinition().getValue(), SearchParameter.class); - SearchParameter compSP = SearchUtil.getSearchParameter(p.getResourceType(), component.getDefinition()); - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(p.getResourceType(), compSP); - FHIRPathNode node = nodes.iterator().next(); - if (nodes.size() > 1 ) { - // TODO: support component expressions that result in multiple nodes - // On the current schema, this means creating a different CompositeParmValue for each ordered set of component values. - // For example, if a composite has two components and each one's expression results in two nodes - // ([Code1,Code2] and [Quantity1,Quantity2]) and each node results in a single ExtractedParameterValue, - // then we need to generate CompositeParmVal objects for [Code1,Quantity1], [Code1,Quantity2], - // [Code2,Quantity1], and [Code2,Quantity2]. - // Assumption: this should be rare. - if (log.isLoggable(Level.FINE)) { - log.fine("Component expression '" + component.getExpression().getValue() + "' resulted in multiple nodes; " - + "proceeding with randomly chosen node '" + node.path() + "' for search parameter '" + code + "'."); + CompositeParmVal p = new CompositeParmVal(); + p.setName(code); + p.setUrl(url); + p.setVersion(version); + p.setResourceType(fhirResource.getClass().getSimpleName()); + + for (int i = 0; i < components.size(); i++) { + Component component = components.get(i); + Collection nodes = evaluator.evaluate(context, component.getExpression().getValue()); + if (nodes.isEmpty()){ + if (log.isLoggable(Level.FINER)) { + log.finer("Component expression '" + component.getExpression().getValue() + "' resulted in 0 nodes; " + + "skipping composite parameter '" + code + "'."); + } + continue; } - } - try { - if (node.isElementNode()) { - // parameterBuilder aggregates the results for later retrieval - node.asElementNode().element().accept(parameterBuilder); - // retrieve the list of parameters built from all the FHIRPathElementNode values - List parameters = parameterBuilder.getResult(); - if (parameters.isEmpty()){ - if (log.isLoggable(Level.FINE)) { - log.fine("Selected element '" + node.path() + "' resulted in 0 extracted parameter values; " - + "skipping composite parameter '" + code + "'."); - } - continue; + // Alternative: consider pulling the search parameter from the FHIRRegistry instead so we can use versioned references. + // Of course, that would require adding extension-search-params to the Registry which would require the Registry to be tenant-aware. + // SearchParameter compSP = FHIRRegistry.getInstance().getResource(component.getDefinition().getValue(), SearchParameter.class); + SearchParameter compSP = SearchUtil.getSearchParameter(p.getResourceType(), component.getDefinition()); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(p.getResourceType(), compSP); + FHIRPathNode node = nodes.iterator().next(); + if (nodes.size() > 1 ) { + // TODO: support component expressions that result in multiple nodes + // On the current schema, this means creating a different CompositeParmValue for each ordered set of component values. + // For example, if a composite has two components and each one's expression results in two nodes + // ([Code1,Code2] and [Quantity1,Quantity2]) and each node results in a single ExtractedParameterValue, + // then we need to generate CompositeParmVal objects for [Code1,Quantity1], [Code1,Quantity2], + // [Code2,Quantity1], and [Code2,Quantity2]. + // Assumption: this should be rare. + if (log.isLoggable(Level.FINE)) { + log.fine("Component expression '" + component.getExpression().getValue() + "' resulted in multiple nodes; " + + "proceeding with randomly chosen node '" + node.path() + "' for search parameter '" + code + "'."); } + } + + try { + if (node.isElementNode()) { + // parameterBuilder aggregates the results for later retrieval + node.asElementNode().element().accept(parameterBuilder); + // retrieve the list of parameters built from all the FHIRPathElementNode values + List parameters = parameterBuilder.getResult(); + if (parameters.isEmpty()){ + if (log.isLoggable(Level.FINE)) { + log.fine("Selected element '" + node.path() + "' resulted in 0 extracted parameter values; " + + "skipping composite parameter '" + code + "'."); + } + continue; + } - if (parameters.size() > 1) { - // TODO: support component searchParms that lead to multiple ExtractedParameterValues - // On the current schema, this means creating a different CompositeParmValue for each ordered set of component values. - // For example: - // If a composite has two components and each results in two extracted parameters ([A,B] and [1,2] respectively) - // then we need to generate CompositeParmVal objects for [A,1], [A,2], [B,1], and [B,2] - // Assumption: this should only be common for Quantity search parameters with both a coded unit and a display unit, - // and in these cases, the coded unit is almost always the preferred value for search. + if (parameters.size() > 1) { + // TODO: support component searchParms that lead to multiple ExtractedParameterValues + // On the current schema, this means creating a different CompositeParmValue for each ordered set of component values. + // For example: + // If a composite has two components and each results in two extracted parameters ([A,B] and [1,2] respectively) + // then we need to generate CompositeParmVal objects for [A,1], [A,2], [B,1], and [B,2] + // Assumption: this should only be common for Quantity search parameters with both a coded unit and a display unit, + // and in these cases, the coded unit is almost always the preferred value for search. + if (log.isLoggable(Level.FINE)) { + log.fine("Selected element '" + node.path() + "' resulted in multiple extracted parameter values; " + + "proceeding with the first extracted value for composite parameter '" + code + "'."); + } + } + ExtractedParameterValue componentParam = parameters.get(0); + // override the component parameter name with the composite parameter name + componentParam.setName(SearchUtil.makeCompositeSubCode(code, componentParam.getName())); + componentParam.setUrl(url); + componentParam.setVersion(version); + p.addComponent(componentParam); + } else if (node.isSystemValue()){ + ExtractedParameterValue primitiveParam = processPrimitiveValue(node.asSystemValue()); + primitiveParam.setName(code); + primitiveParam.setUrl(url); + primitiveParam.setVersion(version); + primitiveParam.setResourceType(fhirResource.getClass().getSimpleName()); + + if (log.isLoggable(Level.FINE)) { + log.fine("Extracted Parameter '" + p.getName() + "' from Resource."); + } + p.addComponent(primitiveParam); + } else { + // log and continue + String msg = "Unable to extract value from '" + value.path() + + "'; search parameter value extraction can only be performed on Elements and primitive values."; if (log.isLoggable(Level.FINE)) { - log.fine("Selected element '" + node.path() + "' resulted in multiple extracted parameter values; " - + "proceeding with the first extracted value for composite parameter '" + code + "'."); + log.fine(msg); } + addWarning(IssueType.INVALID, msg); + continue; + } + } catch (IllegalArgumentException e) { + // log and continue with the other parameters + StringBuilder msg = new StringBuilder("Skipped search parameter '" + code + "'"); + if (sp.getId() != null) { + msg.append(" with id '" + sp.getId() + "'"); } - ExtractedParameterValue componentParam = parameters.get(0); - // override the component parameter name with the composite parameter name - componentParam.setName(SearchUtil.makeCompositeSubCode(code, componentParam.getName())); - p.addComponent(componentParam); - } else if (node.isSystemValue()){ - ExtractedParameterValue primitiveParam = processPrimitiveValue(node.asSystemValue()); - primitiveParam.setName(code); - primitiveParam.setResourceType(fhirResource.getClass().getSimpleName()); + msg.append(" for resource type " + fhirResource.getClass().getSimpleName()); + // just use the message...no need for the whole stack trace + msg.append(" due to \n" + e.getMessage()); + if (log.isLoggable(Level.FINE)) { + log.fine(msg.toString()); + } + addWarning(IssueType.INVALID, msg.toString()); + } + } + if (components.size() == p.getComponent().size()) { + // only add the parameter if all of the components are present and accounted for + allParameters.add(p); + } + } + } else { // ! SearchParamType.COMPOSITE.equals(sp.getType()) + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(fhirResource.getClass().getSimpleName(), sp); + + for (FHIRPathNode value : values) { + + try { + if (value.isElementNode()) { + // parameterBuilder aggregates the results for later retrieval + value.asElementNode().element().accept(parameterBuilder); + } else if (value.isSystemValue()){ + ExtractedParameterValue p = processPrimitiveValue(value.asSystemValue()); + p.setName(code); + p.setUrl(url); + p.setVersion(version); + p.setResourceType(fhirResource.getClass().getSimpleName()); + + if (wholeSystemParam) { + p.setWholeSystem(true); + } + allParameters.add(p); if (log.isLoggable(Level.FINE)) { log.fine("Extracted Parameter '" + p.getName() + "' from Resource."); } - p.addComponent(primitiveParam); } else { // log and continue String msg = "Unable to extract value from '" + value.path() + @@ -2019,7 +2084,7 @@ private List extractSearchParameters(Resource fhirResou } } catch (IllegalArgumentException e) { // log and continue with the other parameters - StringBuilder msg = new StringBuilder("Skipped search parameter '" + code + "'"); + StringBuilder msg = new StringBuilder("Skipping search parameter '" + code + "'"); if (sp.getId() != null) { msg.append(" with id '" + sp.getId() + "'"); } @@ -2032,79 +2097,58 @@ private List extractSearchParameters(Resource fhirResou addWarning(IssueType.INVALID, msg.toString()); } } - if (components.size() == p.getComponent().size()) { - // only add the parameter if all of the components are present and accounted for - allParameters.add(p); - } - } - } else { // ! SearchParamType.COMPOSITE.equals(sp.getType()) - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(fhirResource.getClass().getSimpleName(), sp); - - for (FHIRPathNode value : values) { - - try { - if (value.isElementNode()) { - // parameterBuilder aggregates the results for later retrieval - value.asElementNode().element().accept(parameterBuilder); - } else if (value.isSystemValue()){ - ExtractedParameterValue p = processPrimitiveValue(value.asSystemValue()); - p.setName(code); - p.setResourceType(fhirResource.getClass().getSimpleName()); - - if (wholeSystemParam) { - p.setWholeSystem(true); - } - allParameters.add(p); - if (log.isLoggable(Level.FINE)) { - log.fine("Extracted Parameter '" + p.getName() + "' from Resource."); - } - } else { - // log and continue - String msg = "Unable to extract value from '" + value.path() + - "'; search parameter value extraction can only be performed on Elements and primitive values."; - if (log.isLoggable(Level.FINE)) { - log.fine(msg); - } - addWarning(IssueType.INVALID, msg); - continue; - } - } catch (IllegalArgumentException e) { - // log and continue with the other parameters - StringBuilder msg = new StringBuilder("Skipping search parameter '" + code + "'"); - if (sp.getId() != null) { - msg.append(" with id '" + sp.getId() + "'"); + // retrieve the list of parameters built from all the FHIRPathElementNode values + List parameters = parameterBuilder.getResult(); + for (ExtractedParameterValue p : parameters) { + if (wholeSystemParam) { + p.setWholeSystem(true); } - msg.append(" for resource type " + fhirResource.getClass().getSimpleName()); - // just use the message...no need for the whole stack trace - msg.append(" due to \n" + e.getMessage()); + allParameters.add(p); if (log.isLoggable(Level.FINE)) { - log.fine(msg.toString()); + log.fine("Extracted Parameter '" + p.getName() + "' from Resource."); } - addWarning(IssueType.INVALID, msg.toString()); - } - } - // retrieve the list of parameters built from all the FHIRPathElementNode values - List parameters = parameterBuilder.getResult(); - for (ExtractedParameterValue p : parameters) { - if (wholeSystemParam) { - p.setWholeSystem(true); - } - allParameters.add(p); - if (log.isLoggable(Level.FINE)) { - log.fine("Extracted Parameter '" + p.getName() + "' from Resource."); } } } + + // Augment the extracted parameter list with special values we use to represent compartment relationships. + // These references are stored as tokens and are used by the search query builder + // for compartment-based searches + addCompartmentParams(allParameters, fhirResource); } - // Augment the extracted parameter list with special values we use to represent compartment relationships. - // These references are stored as tokens and are used by the search query builder - // for compartment-based searches - addCompartmentParams(allParameters, fhirResource); + + // Generate the hash which is used to quickly determine whether the extracted parameters + // are different than the extracted parameters that currently exist in the database. + // Sort extracted parameter values in natural order first, to ensure the hash generated by + // this visitor is deterministic. + sortExtractedParameterValues(allParameters); + ParameterHashVisitor phv = new ParameterHashVisitor(); + for (ExtractedParameterValue p: allParameters) { + p.accept(phv); + } + parameterHashB64 = phv.getBase64Hash(); + } finally { log.exiting(CLASSNAME, METHODNAME); } - return allParameters; + return new ExtractedSearchParameters(allParameters, parameterHashB64); + } + + /** + * Sorts the extracted parameter values in natural order. If the list contains any composite parameter values, + * those are sorted before the list itself is sorted. Since composite parameters cannot themselves contain composites, + * doing this with a recursive call is ok. + * @param extractedParameterValues the extracted parameter values + */ + private void sortExtractedParameterValues(List extractedParameterValues) { + for (ExtractedParameterValue extractedParameterValue : extractedParameterValues) { + if (extractedParameterValue instanceof CompositeParmVal) { + CompositeParmVal compositeParmVal = (CompositeParmVal) extractedParameterValue; + sortExtractedParameterValues(compositeParmVal.getComponent()); + } + } + Collections.sort(extractedParameterValues); } /** @@ -2529,7 +2573,7 @@ public int reindex(FHIRPersistenceContext context, OperationOutcome.Builder oper } else { // Skip this particular resource because it has been deleted if (log.isLoggable(Level.FINE)) { - log.info("Skipping reindex for deleted FHIR Resource '" + rir.getResourceType() + "/" + rir.getLogicalId() + "'"); + log.fine("Skipping reindex for deleted FHIR Resource '" + rir.getResourceType() + "/" + rir.getLogicalId() + "'"); } rir.setDeleted(true); } @@ -2571,11 +2615,11 @@ public int reindex(FHIRPersistenceContext context, OperationOutcome.Builder oper /** * Update the parameters for the resource described by the given DTO * @param - * @param rir - * @param resourceTypeClass - * @param existingResourceDTO - * @param reindexDAO - * @param operationOutcomeResult + * @param rir the resource index record + * @param resourceTypeClass the resource type class + * @param existingResourceDTO the existing resource DTO + * @param reindexDAO the reindex resource DAO + * @param operationOutcomeResult the operation outcome result * @throws Exception */ public void updateParameters(ResourceIndexRecord rir, Class resourceTypeClass, com.ibm.fhir.persistence.jdbc.dto.Resource existingResourceDTO, @@ -2583,9 +2627,17 @@ public void updateParameters(ResourceIndexRecord rir, Class if (existingResourceDTO != null && !existingResourceDTO.isDeleted()) { T existingResource = this.convertResourceDTO(existingResourceDTO, resourceTypeClass, null); - // Extract parameters from the resource payload we just read and store them, replacing - // the existing set - reindexDAO.updateParameters(rir.getResourceType(), this.extractSearchParameters(existingResource, existingResourceDTO), rir.getLogicalId(), rir.getLogicalResourceId()); + // Extract parameters from the resource payload. + ExtractedSearchParameters searchParameters = this.extractSearchParameters(existingResource, existingResourceDTO); + + // Compare the hash of the extracted parameters with the hash in the index record. + // If hash in the index record is not null and it matches the hash of the extracted parameters, then no need to replace the + // extracted search parameters in the database tables for this resource, which helps with performance during reindex. + if (rir.getParameterHash() == null || !rir.getParameterHash().equals(searchParameters.getParameterHashB64())) { + reindexDAO.updateParameters(rir.getResourceType(), searchParameters.getParameters(), searchParameters.getParameterHashB64(), rir.getLogicalId(), rir.getLogicalResourceId()); + } else { + log.fine(() -> "Skipping update of unchanged parameters for FHIR Resource '" + rir.getResourceType() + "/" + rir.getLogicalId() + "'"); + } // Use an OperationOutcome Issue to let the caller know that some work was performed final String diag = "Processed " + rir.getResourceType() + "/" + rir.getLogicalId(); @@ -2595,7 +2647,6 @@ public void updateParameters(ResourceIndexRecord rir, Class final String diag = "Failed to read resource: " + rir.getResourceType() + "/" + rir.getLogicalId(); operationOutcomeResult.issue(Issue.builder().code(IssueType.NOT_FOUND).severity(IssueSeverity.WARNING).diagnostics(string(diag)).build()); } - } @Override diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/postgres/PostgresReindexResourceDAO.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/postgres/PostgresReindexResourceDAO.java index 96eff1ce80d..8c18cac9252 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/postgres/PostgresReindexResourceDAO.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/postgres/PostgresReindexResourceDAO.java @@ -46,7 +46,7 @@ public class PostgresReindexResourceDAO extends ReindexResourceDAO { + " AND lr.reindex_tstamp < ? " + " ORDER BY lr.reindex_tstamp " + " FOR UPDATE SKIP LOCKED LIMIT 1) " - + "RETURNING logical_resource_id, resource_type_id, logical_id, reindex_txid " + + "RETURNING logical_resource_id, resource_type_id, logical_id, reindex_txid, parameter_hash " ; private static final String PICK_SINGLE_RESOURCE_TYPE = "" @@ -61,7 +61,7 @@ public class PostgresReindexResourceDAO extends ReindexResourceDAO { + " AND lr.reindex_tstamp < ? " + " ORDER BY lr.reindex_tstamp " + " FOR UPDATE SKIP LOCKED LIMIT 1) " - + "RETURNING logical_resource_id, resource_type_id, logical_id, reindex_txid " + + "RETURNING logical_resource_id, resource_type_id, logical_id, reindex_txid, parameter_hash " ; private static final String PICK_ANY_RESOURCE = "" @@ -75,7 +75,7 @@ public class PostgresReindexResourceDAO extends ReindexResourceDAO { + " AND lr.reindex_tstamp < ? " + " ORDER BY lr.reindex_tstamp " + " FOR UPDATE SKIP LOCKED LIMIT 1) " - + "RETURNING logical_resource_id, resource_type_id, logical_id, reindex_txid " + + "RETURNING logical_resource_id, resource_type_id, logical_id, reindex_txid, parameter_hash " ; /** @@ -157,7 +157,7 @@ public ResourceIndexRecord getNextResource(SecureRandom random, Instant reindexT stmt.execute(); ResultSet rs = stmt.getResultSet(); if (rs.next()) { - result = new ResourceIndexRecord(rs.getLong(1), rs.getInt(2), rs.getString(3), rs.getLong(4)); + result = new ResourceIndexRecord(rs.getLong(1), rs.getInt(2), rs.getString(3), rs.getLong(4), rs.getString(5)); } } catch (SQLException x) { logger.log(Level.SEVERE, update, x); diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/ExtractedSearchParameters.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/ExtractedSearchParameters.java new file mode 100644 index 00000000000..2fa3734db35 --- /dev/null +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/ExtractedSearchParameters.java @@ -0,0 +1,43 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.persistence.jdbc.util; + + +import java.util.Collections; +import java.util.List; + +import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValue; + +/** + * Contains a list of extracted search parameters and a Base64-encoded hash. + */ +public class ExtractedSearchParameters { + + private final List parameters; + private final String parameterHashB64; + + public ExtractedSearchParameters(List parameters, String parameterHashB64) { + this.parameters = Collections.unmodifiableList(parameters); + this.parameterHashB64 = parameterHashB64; + } + + /** + * Gets the parameters. + * @return the parameters + */ + public List getParameters() { + return parameters; + } + + /** + * Gets the Base64-encoded hash of the parameters. + * @return the Base64-encoded hash + */ + public String getParameterHashB64() { + return parameterHashB64; + } +} diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java index 3fdb199680c..2e975948d75 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java @@ -95,9 +95,11 @@ public class JDBCParameterBuildingVisitor extends DefaultVisitor { ZonedDateTime.parse("9999-12-31T23:59:59.999999Z").toInstant()); private final String resourceType; - // We only need the SearchParameter type and code, so just store those directly as members + // We only need the SearchParameter code, type, url, and version, so just store those directly as members private final String searchParamCode; private final SearchParamType searchParamType; + private final String searchParamUrl; + private final String searchParamVersion; /** * The result of the visit(s) @@ -106,15 +108,17 @@ public class JDBCParameterBuildingVisitor extends DefaultVisitor { /** * Public constructor - * @param resourceType - * @param searchParameter + * @param resourceType the resource type + * @param searchParameter the search parameter */ public JDBCParameterBuildingVisitor(String resourceType, SearchParameter searchParameter) { super(false); this.resourceType = resourceType; this.searchParamCode = searchParameter.getCode().getValue(); this.searchParamType = searchParameter.getType(); - this.result = new ArrayList<>(); + this.searchParamUrl = searchParameter.getUrl().getValue(); + this.searchParamVersion = searchParameter.getVersion() != null ? searchParameter.getVersion().getValue(): null; + this.result = new ArrayList<>(); } /** @@ -147,6 +151,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueSystem("http://terminology.hl7.org/CodeSystem/special-values"); if (_boolean.getValue()) { p.setValueCode("true"); @@ -167,6 +173,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi StringParmVal p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(canonical.getValue()); result.add(p); } @@ -182,6 +190,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); String system = ModelSupport.getSystem(code); setTokenValues(p, system != null ? Uri.of(system) : null, code.getValue()); result.add(p); @@ -198,6 +208,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi DateParmVal p = new DateParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); setDateValues(p, date); result.add(p); } @@ -213,6 +225,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi DateParmVal p = new DateParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); setDateValues(p, dateTime); result.add(p); } @@ -228,6 +242,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi NumberParmVal p = new NumberParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); BigDecimal value = decimal.getValue(); p.setValueNumber(value); p.setValueNumberLow(NewNumberParmBehaviorUtil.generateLowerBound(value)); @@ -246,6 +262,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueCode(id.getValue()); result.add(p); } @@ -261,6 +279,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi DateParmVal p = new DateParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); Timestamp t = generateTimestamp(instant.getValue().toInstant()); p.setValueDateStart(t); p.setValueDateEnd(t); @@ -278,6 +298,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi NumberParmVal p = new NumberParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); BigDecimal value = new BigDecimal(integer.getValue()); p.setValueNumber(value); p.setValueNumberLow(value); @@ -295,12 +317,16 @@ public boolean visit(String elementName, int elementIndex, com.ibm.fhir.model.ty p.setResourceType(resourceType); p.setValueString(value.getValue()); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); result.add(p); } else if (TOKEN.equals(searchParamType)) { TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setValueCode(SearchUtil.normalizeForSearch(value.getValue())); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); result.add(p); } else { throw invalidComboException(searchParamType, value); @@ -322,12 +348,16 @@ public boolean visit(String elementName, int elementIndex, Uri uri) { TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueCode(uri.getValue()); result.add(p); } else { StringParmVal p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(uri.getValue()); result.add(p); } @@ -351,6 +381,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(aLine.getValue()); result.add(p); } @@ -358,6 +390,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(address.getCity().getValue()); result.add(p); } @@ -365,6 +399,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(address.getDistrict().getValue()); result.add(p); } @@ -372,6 +408,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(address.getState().getValue()); result.add(p); } @@ -379,6 +417,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(address.getCountry().getValue()); result.add(p); } @@ -386,6 +426,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(address.getPostalCode().getValue()); result.add(p); } @@ -393,6 +435,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(address.getText().getValue()); result.add(p); } @@ -412,6 +456,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, CodeableCon TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode + SearchConstants.TEXT_MODIFIER_SUFFIX); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueCode(SearchUtil.normalizeForSearch(codeableConcept.getText().getValue())); result.add(p); } @@ -427,6 +473,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Coding codi TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); setTokenValues(p, coding.getSystem(), coding.getCode().getValue()); result.add(p); if (coding.getDisplay() != null && coding.getDisplay().hasValue()) { @@ -434,6 +482,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Coding codi p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode + SearchConstants.TEXT_MODIFIER_SUFFIX); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueCode(SearchUtil.normalizeForSearch(coding.getDisplay().getValue())); result.add(p); } @@ -450,6 +500,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, ContactPoin TokenParmVal telecom = new TokenParmVal(); telecom.setResourceType(resourceType); telecom.setName(searchParamCode); + telecom.setUrl(searchParamUrl); + telecom.setVersion(searchParamVersion); telecom.setValueCode(contactPoint.getValue().getValue()); result.add(telecom); } @@ -467,6 +519,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, HumanName h p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(humanName.getFamily().getValue()); result.add(p); } @@ -474,6 +528,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, HumanName h p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(given.getValue()); result.add(p); } @@ -481,6 +537,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, HumanName h p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(prefix.getValue()); result.add(p); } @@ -488,6 +546,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, HumanName h p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(suffix.getValue()); result.add(p); } @@ -495,6 +555,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, HumanName h p = new StringParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueString(humanName.getText().getValue()); result.add(p); } @@ -510,6 +572,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Money money QuantityParmVal p = new QuantityParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueNumber(money.getValue().getValue()); p.setValueNumberLow(NewNumberParmBehaviorUtil.generateLowerBound(money.getValue().getValue())); p.setValueNumberHigh(NewNumberParmBehaviorUtil.generateUpperBound(money.getValue().getValue())); @@ -533,6 +597,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Period peri DateParmVal p = new DateParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); if (period.getStart() == null || period.getStart().getValue() == null) { p.setValueDateStart(SMALLEST_TIMESTAMP); } else { @@ -588,6 +654,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Quantity qu QuantityParmVal p = new QuantityParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueNumber(value); p.setValueNumberLow(valueLow); p.setValueNumberHigh(valueHigh); @@ -605,6 +673,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Quantity qu QuantityParmVal p = new QuantityParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueNumber(value); p.setValueNumberLow(valueLow); p.setValueNumberHigh(valueHigh); @@ -618,6 +688,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Quantity qu QuantityParmVal p = new QuantityParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); p.setValueNumber(value); p.setValueNumberLow(valueLow); p.setValueNumberHigh(valueHigh); @@ -636,6 +708,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Range range QuantityParmVal p = new QuantityParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); if (range.getLow() != null && range.getLow().getValue() != null && range.getLow().getValue().getValue() != null) { if (range.getLow().getSystem() != null) { @@ -680,6 +754,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Identifier TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); setTokenValues(p, identifier.getSystem(), identifier.getValue().getValue()); result.add(p); if (identifier.getType() != null) { @@ -689,11 +765,15 @@ public boolean visit(java.lang.String elementName, int elementIndex, Identifier CompositeParmVal cp = new CompositeParmVal(); cp.setResourceType(resourceType); cp.setName(searchParamCode + SearchConstants.OF_TYPE_MODIFIER_SUFFIX); + cp.setUrl(searchParamUrl); + cp.setVersion(searchParamVersion); // type p = new TokenParmVal(); p.setResourceType(cp.getResourceType()); p.setName(SearchUtil.makeCompositeSubCode(cp.getName(), SearchConstants.OF_TYPE_MODIFIER_COMPONENT_TYPE)); + p.setUrl(cp.getUrl()); + p.setVersion(cp.getVersion()); setTokenValues(p, typeCoding.getSystem(), typeCoding.getCode().getValue()); cp.addComponent(p); @@ -701,6 +781,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Identifier p = new TokenParmVal(); p.setResourceType(cp.getResourceType()); p.setName(SearchUtil.makeCompositeSubCode(cp.getName(), SearchConstants.OF_TYPE_MODIFIER_COMPONENT_VALUE)); + p.setUrl(cp.getUrl()); + p.setVersion(cp.getVersion()); p.setValueCode(identifier.getValue().getValue()); cp.addComponent(p); @@ -729,6 +811,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Reference r p.setResourceType(resourceType); p.setRefValue(refValue); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); result.add(p); } Identifier identifier = reference.getIdentifier(); @@ -736,6 +820,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Reference r TokenParmVal p = new TokenParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode + SearchConstants.IDENTIFIER_MODIFIER_SUFFIX); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); setTokenValues(p, identifier.getSystem(), identifier.getValue().getValue()); result.add(p); } @@ -776,6 +862,8 @@ public boolean visit(java.lang.String elementName, int elementIndex, Location.Po LocationParmVal p = new LocationParmVal(); p.setResourceType(resourceType); p.setName(searchParamCode); + p.setUrl(searchParamUrl); + p.setVersion(searchParamVersion); // The following code ensures that the lat/lon is only added when there is a pair. boolean add = false; diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/ParameterHashVisitor.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/ParameterHashVisitor.java new file mode 100644 index 00000000000..f2cb740f6ba --- /dev/null +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/ParameterHashVisitor.java @@ -0,0 +1,218 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.persistence.jdbc.util; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.Timestamp; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.List; + +import com.ibm.fhir.persistence.exception.FHIRPersistenceException; +import com.ibm.fhir.persistence.jdbc.dto.CompositeParmVal; +import com.ibm.fhir.persistence.jdbc.dto.DateParmVal; +import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValue; +import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValueVisitor; +import com.ibm.fhir.persistence.jdbc.dto.LocationParmVal; +import com.ibm.fhir.persistence.jdbc.dto.NumberParmVal; +import com.ibm.fhir.persistence.jdbc.dto.QuantityParmVal; +import com.ibm.fhir.persistence.jdbc.dto.ReferenceParmVal; +import com.ibm.fhir.persistence.jdbc.dto.StringParmVal; +import com.ibm.fhir.persistence.jdbc.dto.TokenParmVal; +import com.ibm.fhir.schema.control.FhirSchemaVersion; +import com.ibm.fhir.search.util.ReferenceValue; + +/** + * Compute a cryptographic hash of the visited parameters. + */ +public class ParameterHashVisitor implements ExtractedParameterValueVisitor { + private static final String SHA_256 = "SHA-256"; + // Since the digest is not updated when values are null, use a delimiter to ensure that + // different combinations subfields being null do not end up generating the same hash + private static final byte[] DELIMITER = "|".getBytes(StandardCharsets.UTF_8); + + // The digest being accumulated as the parameters are visited + private final MessageDigest digest; + private final Encoder encoder; + + /** + * Public constructor. + */ + public ParameterHashVisitor() { + try { + digest = MessageDigest.getInstance(SHA_256); + // Start digest with latest FHIR schema version (with parameter storage update) + digest.update(FhirSchemaVersion.getLatestParameterStorageUpdate().toString().getBytes(StandardCharsets.UTF_8)); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("MessageDigest not found: " + SHA_256, e); + } + encoder = Base64.getEncoder(); + } + + @Override + public void visit(StringParmVal param) throws FHIRPersistenceException { + updateDigestWithParmValKey(param); + updateDigestWithString(param.getValueString()); + } + + @Override + public void visit(NumberParmVal param) throws FHIRPersistenceException { + updateDigestWithParmValKey(param); + updateDigestWithBigDecimal(param.getValueNumber()); + updateDigestWithBigDecimal(param.getValueNumberLow()); + updateDigestWithBigDecimal(param.getValueNumberHigh()); + } + + @Override + public void visit(DateParmVal param) throws FHIRPersistenceException { + updateDigestWithParmValKey(param); + updateDigestWithTimestamp(param.getValueDateStart()); + updateDigestWithTimestamp(param.getValueDateEnd()); + } + + @Override + public void visit(TokenParmVal param) throws FHIRPersistenceException { + updateDigestWithParmValKey(param); + updateDigestWithString(param.getValueSystem()); + updateDigestWithString(param.getValueCode()); + } + + @Override + public void visit(QuantityParmVal param) throws FHIRPersistenceException { + updateDigestWithParmValKey(param); + updateDigestWithBigDecimal(param.getValueNumber()); + updateDigestWithBigDecimal(param.getValueNumberLow()); + updateDigestWithBigDecimal(param.getValueNumberHigh()); + updateDigestWithString(param.getValueSystem()); + updateDigestWithString(param.getValueCode()); + } + + @Override + public void visit(LocationParmVal param) throws FHIRPersistenceException { + updateDigestWithParmValKey(param); + updateDigestWithDouble(param.getValueLongitude()); + updateDigestWithDouble(param.getValueLatitude()); + } + + @Override + public void visit(ReferenceParmVal param) throws FHIRPersistenceException { + updateDigestWithParmValKey(param); + updateDigestWithReferenceValue(param.getRefValue()); + } + + @Override + public void visit(CompositeParmVal compositeParameter) throws FHIRPersistenceException { + updateDigestWithParmValKey(compositeParameter); + // This composite is a collection of multiple parameters + List component = compositeParameter.getComponent(); + for (ExtractedParameterValue val : component) { + val.accept(this); + } + } + + /** + * Compute and return the hash. + * @return the hash string as Base64 + */ + public String getBase64Hash() { + byte[] hash = digest.digest(); + return encoder.encodeToString(hash); + } + + /** + * Updates the digest with the key of the ExtractedParameterValue. + * @param epv the ExtractedParameterValue + */ + private void updateDigestWithParmValKey(ExtractedParameterValue epv) { + updateDigestWithString(epv.getClass().getName()); + updateDigestWithString(epv.getName()); + updateDigestWithString(epv.getUrl()); + updateDigestWithString(epv.getVersion()); + } + + /** + * Updates the digest with the String. + * @param string the String + */ + private void updateDigestWithString(String value) { + if (value != null) { + digest.update(value.getBytes(StandardCharsets.UTF_8)); + } + digest.update(DELIMITER); + } + + /** + * Updates the digest with the BigDecimal. + * @param value the BigDecimal + */ + private void updateDigestWithBigDecimal(BigDecimal value) { + if (value != null) { + ByteBuffer bb = ByteBuffer.allocate(Double.SIZE); + bb.putDouble(value.doubleValue()); + digest.update(bb); + } + digest.update(DELIMITER); + } + + /** + * Updates the digest with the Integer. + * @param value the Integer + */ + private void updateDigestWithInteger(Integer value) { + if (value != null) { + ByteBuffer bb = ByteBuffer.allocate(Integer.SIZE); + bb.putInt(value.intValue()); + digest.update(bb); + } + digest.update(DELIMITER); + } + + /** + * Updates the digest with the Double. + * @param value the Double + */ + private void updateDigestWithDouble(Double value) { + if (value != null) { + ByteBuffer bb = ByteBuffer.allocate(Double.SIZE); + bb.putDouble(value.doubleValue()); + digest.update(bb); + } + digest.update(DELIMITER); + } + + /** + * Updates the digest with the Timestamp. + * @param value the Timestamp + */ + private void updateDigestWithTimestamp(Timestamp value) { + if (value != null) { + ByteBuffer bb = ByteBuffer.allocate(Long.SIZE); + bb.putLong(value.getTime()); + digest.update(bb); + } + digest.update(DELIMITER); + } + + /** + * Updates the digest with the ReferenceValue. + * @param value the ReferenceValue + */ + private void updateDigestWithReferenceValue(ReferenceValue value) { + if (value != null) { + updateDigestWithString(value.getTargetResourceType()); + updateDigestWithString(value.getValue()); + updateDigestWithString(value.getType() != null ? value.getType().toString() : null); + updateDigestWithInteger(value.getVersion()); + } else { + digest.update(DELIMITER); + } + } +} diff --git a/fhir-persistence-jdbc/src/test/java/com/ibm/fhir/persistence/jdbc/test/util/ParameterHashTest.java b/fhir-persistence-jdbc/src/test/java/com/ibm/fhir/persistence/jdbc/test/util/ParameterHashTest.java new file mode 100644 index 00000000000..4d06464690b --- /dev/null +++ b/fhir-persistence-jdbc/src/test/java/com/ibm/fhir/persistence/jdbc/test/util/ParameterHashTest.java @@ -0,0 +1,372 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.persistence.jdbc.test.util; + + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotNull; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.testng.annotations.Test; + +import com.ibm.fhir.persistence.jdbc.dto.CompositeParmVal; +import com.ibm.fhir.persistence.jdbc.dto.DateParmVal; +import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValue; +import com.ibm.fhir.persistence.jdbc.dto.LocationParmVal; +import com.ibm.fhir.persistence.jdbc.dto.NumberParmVal; +import com.ibm.fhir.persistence.jdbc.dto.QuantityParmVal; +import com.ibm.fhir.persistence.jdbc.dto.ReferenceParmVal; +import com.ibm.fhir.persistence.jdbc.dto.StringParmVal; +import com.ibm.fhir.persistence.jdbc.dto.TokenParmVal; +import com.ibm.fhir.persistence.jdbc.util.ParameterHashVisitor; +import com.ibm.fhir.persistence.jdbc.util.type.NumberParmBehaviorUtil; +import com.ibm.fhir.search.date.DateTimeHandler; +import com.ibm.fhir.search.util.ReferenceValue; +import com.ibm.fhir.search.util.ReferenceValue.ReferenceType; + +/** + * Utility to do testing of the parameter hash utility. + */ +public class ParameterHashTest { + + @Test + public void testNoExtractedParameters() throws Exception { + List parameters1 = new ArrayList<>(); + List parameters2 = new ArrayList<>(); + + // Sort parameters to ensure hash is deterministic + sortExtractedParameterValues(parameters1); + sortExtractedParameterValues(parameters2); + + // Visit parameters + ParameterHashVisitor phv1 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters1) { + p.accept(phv1); + } + String hash1 = phv1.getBase64Hash(); + assertNotNull(hash1); + + ParameterHashVisitor phv2 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters2) { + p.accept(phv2); + } + String hash2 = phv2.getBase64Hash(); + assertNotNull(hash2); + + // Check hashes + assertEquals(hash1, hash2); + } + + @Test + public void testNullValues() throws Exception { + + // Create two number param values, one with null low, and one with null high + BigDecimal value = new BigDecimal(5); + + NumberParmVal num1 = new NumberParmVal(); + num1.setResourceType("Patient"); + num1.setName("code5"); + num1.setUrl("url5"); + num1.setVersion("version5"); + num1.setValueNumber(value); + num1.setValueNumberLow(NumberParmBehaviorUtil.generateLowerBound(value)); + + NumberParmVal num2 = new NumberParmVal(); + num2.setResourceType("Patient"); + num2.setName("code5"); + num2.setUrl("url5"); + num2.setVersion("version5"); + num2.setValueNumber(value); + num2.setValueNumberHigh(NumberParmBehaviorUtil.generateUpperBound(value)); + + List parameters1 = Arrays.asList(num1); + List parameters2 = Arrays.asList(num2); + + // Sort parameters to ensure hash is deterministic + sortExtractedParameterValues(parameters1); + sortExtractedParameterValues(parameters2); + + // Visit parameters + ParameterHashVisitor phv1 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters1) { + p.accept(phv1); + } + String hash1 = phv1.getBase64Hash(); + assertNotNull(hash1); + + ParameterHashVisitor phv2 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters2) { + p.accept(phv2); + } + String hash2 = phv2.getBase64Hash(); + assertNotNull(hash2); + + // Check hashes + assertNotEquals(hash1, hash2); + } + + @Test + public void testExtractedParametersDifferentOrders() throws Exception { + Instant instant = Instant.now(); + + // Define some search parameter values + StringParmVal p1 = new StringParmVal(); + p1.setResourceType("Patient"); + p1.setName("code1"); + p1.setUrl("url1"); + p1.setVersion("version1"); + p1.setValueString("valueString1"); + + TokenParmVal p2 = new TokenParmVal(); + p2.setResourceType("Patient"); + p2.setName("code2"); + p2.setUrl("url2"); + p2.setVersion("version2"); + p2.setValueSystem("valueSystem2"); + p2.setValueCode("valueCode2"); + + ReferenceParmVal p3 = new ReferenceParmVal(); + p3.setResourceType("Patient"); + p3.setName("code3"); + p3.setUrl("url3"); + p3.setVersion("version3"); + p3.setRefValue(new ReferenceValue("Patient", "value3", ReferenceType.LOGICAL, 1)); + + QuantityParmVal p4 = new QuantityParmVal(); + p4.setResourceType("Patient"); + p4.setName("code4"); + p4.setUrl("url4"); + p4.setVersion("version4"); + p4.setValueNumber(new BigDecimal(4)); + p4.setValueNumberLow(new BigDecimal(3.9)); + p4.setValueNumberHigh(new BigDecimal(4.1)); + p4.setValueCode("valueCode4"); + p4.setValueSystem("valueSystem4"); + + NumberParmVal p5 = new NumberParmVal(); + p5.setResourceType("Patient"); + p5.setName("code5"); + p5.setUrl("url5"); + p5.setVersion("version5"); + BigDecimal value5 = new BigDecimal(5); + p5.setValueNumber(value5); + p5.setValueNumberLow(NumberParmBehaviorUtil.generateLowerBound(value5)); + p5.setValueNumberHigh(NumberParmBehaviorUtil.generateUpperBound(value5)); + + LocationParmVal p6 = new LocationParmVal(); + p6.setResourceType("Patient"); + p6.setName("code6"); + p6.setUrl("url6"); + p6.setVersion("version6"); + p6.setValueLatitude(6.6); + p6.setValueLongitude(60.6); + + DateParmVal p7 = new DateParmVal(); + p7.setResourceType("Patient"); + p7.setName("code7"); + p7.setUrl("url7"); + p7.setVersion("version7"); + p7.setValueDateStart(DateTimeHandler.generateTimestamp(instant)); + p7.setValueDateEnd(DateTimeHandler.generateTimestamp(instant.plusSeconds(1))); + + CompositeParmVal p8a = new CompositeParmVal(); + p8a.setResourceType("Patient"); + p8a.setName("code8"); + p8a.setUrl("url8"); + p8a.setVersion("version8"); + p8a.addComponent(p1); + p8a.addComponent(p5); + + CompositeParmVal p8b = new CompositeParmVal(); + p8b.setResourceType("Patient"); + p8b.setName("code8"); + p8b.setUrl("url8"); + p8b.setVersion("version8"); + p8b.addComponent(p5); + p8b.addComponent(p1); + + CompositeParmVal p8diff = new CompositeParmVal(); + p8diff.setResourceType("Patient"); + p8diff.setName("code8"); + p8diff.setUrl("url8"); + p8diff.setVersion("version8"); + p8diff.addComponent(p4); + p8diff.addComponent(p6); + + // Add the search parameters in different orders for esp1 and esp2, which still results in same hash, + // but for esp3 and esp4, the search parameters are different, so they should not match the others + List parameters1 = Arrays.asList(p1, p2, p3, p4, p5, p6, p7, p8a); + List parameters2 = Arrays.asList(p8b, p6, p4, p2, p1, p3, p5, p7); + List parameters3 = Arrays.asList(p1, p2, p3, p4, p5, p6, p7, p8diff); + List parameters4 = Collections.emptyList(); + + // Visit parameters + ParameterHashVisitor phv1PreSort = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters1) { + p.accept(phv1PreSort); + } + String hash1PreSort = phv1PreSort.getBase64Hash(); + assertNotNull(hash1PreSort); + + ParameterHashVisitor phv2PreSort = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters2) { + p.accept(phv2PreSort); + } + String hash2PreSort = phv2PreSort.getBase64Hash(); + assertNotNull(hash2PreSort); + + // Check hashes (without sorting first) + // They should not match, which shows that the sorting is needed to generate deterministic hashes + assertNotEquals(hash1PreSort, hash2PreSort); + + // Sort parameters to ensure hash is deterministic + sortExtractedParameterValues(parameters1); + sortExtractedParameterValues(parameters2); + sortExtractedParameterValues(parameters3); + sortExtractedParameterValues(parameters4); + + // Visit parameters + ParameterHashVisitor phv1 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters1) { + p.accept(phv1); + } + String hash1 = phv1.getBase64Hash(); + assertNotNull(hash1); + + ParameterHashVisitor phv2 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters2) { + p.accept(phv2); + } + String hash2 = phv2.getBase64Hash(); + assertNotNull(hash2); + + ParameterHashVisitor phv3 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters3) { + p.accept(phv3); + } + String hash3 = phv3.getBase64Hash(); + assertNotNull(hash3); + + ParameterHashVisitor phv4 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parameters4) { + p.accept(phv4); + } + String hash4 = phv4.getBase64Hash(); + assertNotNull(hash4); + + // Check hashes + assertEquals(hash1, hash2); + assertNotEquals(hash1, hash3); + assertNotEquals(hash1, hash4); + assertNotEquals(hash3, hash4); + } + + @Test + public void testExtractedParametersSwappedValues() throws Exception { + + // Define some search parameter values + StringParmVal p1a = new StringParmVal(); + p1a.setResourceType("Patient"); + p1a.setName("code1"); + p1a.setUrl("url1"); + p1a.setVersion("version1"); + p1a.setValueString("valueString1"); + + StringParmVal p2a = new StringParmVal(); + p2a.setResourceType("Patient"); + p2a.setName("code2"); + p2a.setUrl("url2"); + p2a.setVersion("version2"); + p2a.setValueString("valueString2"); + + // Define some search parameter values with the values swapped + StringParmVal p1b = new StringParmVal(); + p1b.setResourceType("Patient"); + p1b.setName("code1"); + p1b.setUrl("url1"); + p1b.setVersion("version1"); + p1b.setValueString("valueString2"); + + StringParmVal p2b = new StringParmVal(); + p2b.setResourceType("Patient"); + p2b.setName("code2"); + p2b.setUrl("url2"); + p2b.setVersion("version2"); + p2b.setValueString("valueString1"); + + // Add the search parameters in which the values are swapped (espA1<-->espB1, espA2<-->espB2), so they should not match each other, + // but if just the order of the parameters is swapped (espA1<-->espA2, espB1<-->espB2), then they do match + List parametersA1 = Arrays.asList(p1a, p2a); + List parametersA2 = Arrays.asList(p2a, p1a); + List parametersB1 = Arrays.asList(p1b, p2b); + List parametersB2 = Arrays.asList(p2b, p1b); + + // Sort parameters to ensure hash is deterministic + sortExtractedParameterValues(parametersA1); + sortExtractedParameterValues(parametersA2); + sortExtractedParameterValues(parametersB1); + sortExtractedParameterValues(parametersB2); + + // Visit parameters + ParameterHashVisitor phvA1 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parametersA1) { + p.accept(phvA1); + } + String hashA1 = phvA1.getBase64Hash(); + assertNotNull(hashA1); + + ParameterHashVisitor phvA2 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parametersA2) { + p.accept(phvA2); + } + String hashA2 = phvA2.getBase64Hash(); + assertNotNull(hashA2); + + ParameterHashVisitor phvB1 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parametersB1) { + p.accept(phvB1); + } + String hashB1 = phvB1.getBase64Hash(); + assertNotNull(hashB1); + + ParameterHashVisitor phvB2 = new ParameterHashVisitor(); + for (ExtractedParameterValue p: parametersB2) { + p.accept(phvB2); + } + String hashB2 = phvB2.getBase64Hash(); + assertNotNull(hashB2); + + // Check hashes + assertEquals(hashA1, hashA2); + assertNotEquals(hashA1, hashB1); + assertEquals(hashB1, hashB2); + assertNotEquals(hashA2, hashB2); + } + + /** + * Sorts the extracted parameter values in natural order. If the list contains any composite parameter values, + * those are sorted before the list itself is sorted. Since composite parameters cannot themselves contain composites, + * doing this with a recursive call is ok. + * @param extractedParameterValues the extracted parameter values + */ + private void sortExtractedParameterValues(List extractedParameterValues) { + for (ExtractedParameterValue extractedParameterValue : extractedParameterValues) { + if (extractedParameterValue instanceof CompositeParmVal) { + CompositeParmVal compositeParmVal = (CompositeParmVal) extractedParameterValue; + sortExtractedParameterValues(compositeParmVal.getComponent()); + } + } + Collections.sort(extractedParameterValues); + } +} diff --git a/fhir-persistence-schema/src/main/java/com/ibm/fhir/schema/control/FhirSchemaVersion.java b/fhir-persistence-schema/src/main/java/com/ibm/fhir/schema/control/FhirSchemaVersion.java index 8ff1644219b..c05ae1df28b 100644 --- a/fhir-persistence-schema/src/main/java/com/ibm/fhir/schema/control/FhirSchemaVersion.java +++ b/fhir-persistence-schema/src/main/java/com/ibm/fhir/schema/control/FhirSchemaVersion.java @@ -6,6 +6,10 @@ package com.ibm.fhir.schema.control; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; + /** * Tracks the incremental changes to the FHIR schema as it evolves. Incremental * changes to the schema should be recorded here to create a new version number @@ -15,22 +19,22 @@ public enum FhirSchemaVersion { // Make sure the vid values are unique...this cannot be done programmatically with an enum - V0001(1, "Initial version") - ,V0002(2, "Composite search value support") - ,V0003(3, "issue-1263 fhir_ref_sequence start with 20000") - ,V0004(4, "row_id sequence cache 20 to 1000") - ,V0005(5, "issue-1331 add index for resource.last_updated") - ,V0006(6, "issue-1366 normalized schema for storing resource references") - ,V0007(7, "issue-1273 add ref_version_id column to xxx_TOKEN_VALUES_V view") - ,V0008(8, "issue-1929 expose common_token_value_id in xxx_TOKEN_VALUES_V view") - ,V0009(9, "issue-1683 refactor composite values") - ,V0010(10, "issue-1958 add IS_DELETED flag to each xxx_LOGICAL_RESOURCES table") - ,V0011(11, "issue-2011 add LAST_UPDATED to each xxx_LOGICAL_RESOURCES table") - ,V0012(12, "issue-2109 add VERSION_ID to each xxx_LOGICAL_RESOURCES table") - ,V0013(13, "Add $erase operation for hard delete scenarios") - ,V0014(14, "whole-system search and canonical references") - ,V0015(15, "issue-2155 add parameter hash to bypass update during reindex") - ,V0016(16, "issue-1921 add dedicated common_token_values mapping table for searching on security labels") + V0001(1, "Initial version", true) + ,V0002(2, "Composite search value support", true) + ,V0003(3, "issue-1263 fhir_ref_sequence start with 20000", true) + ,V0004(4, "row_id sequence cache 20 to 1000", true) + ,V0005(5, "issue-1331 add index for resource.last_updated", true) + ,V0006(6, "issue-1366 normalized schema for storing resource references", true) + ,V0007(7, "issue-1273 add ref_version_id column to xxx_TOKEN_VALUES_V view", true) + ,V0008(8, "issue-1929 expose common_token_value_id in xxx_TOKEN_VALUES_V view", true) + ,V0009(9, "issue-1683 refactor composite values", true) + ,V0010(10, "issue-1958 add IS_DELETED flag to each xxx_LOGICAL_RESOURCES table", true) + ,V0011(11, "issue-2011 add LAST_UPDATED to each xxx_LOGICAL_RESOURCES table", true) + ,V0012(12, "issue-2109 add VERSION_ID to each xxx_LOGICAL_RESOURCES table", true) + ,V0013(13, "Add $erase operation for hard delete scenarios", true) + ,V0014(14, "whole-system search and canonical references", true) + ,V0015(15, "issue-2155 add parameter hash to bypass update during reindex", true) + ,V0016(16, "issue-1921 add dedicated common_token_values mapping table for security", true) ; // The version number recorded in the VERSION_HISTORY @@ -39,14 +43,20 @@ public enum FhirSchemaVersion { // A meaningful description of the schema change private final String description; + // Version change affects parameter storage, which would require reindex of all resources + // even if the search parameters and extracted values for those resources did not change + private final boolean parameterStorageUpdated; + /** * Constructor for the enum - * @param vn - * @param description + * @param vn the version number + * @param description the description + * @param parameterStorageUpdated true if version change affects parameter storage, otherwise false */ - private FhirSchemaVersion(int vn, String description) { + private FhirSchemaVersion(int vn, String description, boolean parameterStorageUpdated) { this.vid = vn; this.description = description; + this.parameterStorageUpdated = parameterStorageUpdated; } /** @@ -65,4 +75,26 @@ public int vid() { public String getDescription() { return this.description; } + + /** + * Determines if the version change affects parameter storage + * @return + */ + public boolean isParameterStorageUpdated() { + return this.parameterStorageUpdated; + } + + /** + * Gets the latest version that included a parameter storage update, which + * would require all resources to reindex all search parameters, even if the + * search parameters and extracted values did not change. + * @return latest version that included a parameter storage update + */ + public static FhirSchemaVersion getLatestParameterStorageUpdate() { + Optional version = Arrays.stream(FhirSchemaVersion.values()) + .filter(k -> k.isParameterStorageUpdated()) + .sorted(Comparator.comparing(FhirSchemaVersion::vid).reversed()) + .findFirst(); + return version.isPresent() ? version.get() : null; + } } diff --git a/fhir-persistence-schema/src/main/resources/db2/add_any_resource.sql b/fhir-persistence-schema/src/main/resources/db2/add_any_resource.sql index 2805083bb3f..6a2a97ace3c 100644 --- a/fhir-persistence-schema/src/main/resources/db2/add_any_resource.sql +++ b/fhir-persistence-schema/src/main/resources/db2/add_any_resource.sql @@ -99,6 +99,10 @@ BEGIN WHERE resource_type_id = v_resource_type_id AND logical_id = p_logical_id FOR UPDATE WITH RS ; + + -- Since the resource did not previously exist, set o_current_parameter_hash back to NULL + SET o_current_parameter_hash = NULL; + ELSE -- we created the logical resource and therefore we already own the lock. So now we can -- safely create the corresponding record in the resource-type-specific logical_resources table diff --git a/fhir-persistence-schema/src/main/resources/postgres/add_any_resource.sql b/fhir-persistence-schema/src/main/resources/postgres/add_any_resource.sql index 22fc8bb7d0c..6fed20689db 100644 --- a/fhir-persistence-schema/src/main/resources/postgres/add_any_resource.sql +++ b/fhir-persistence-schema/src/main/resources/postgres/add_any_resource.sql @@ -82,6 +82,9 @@ BEGIN FETCH lock_cur INTO t_logical_resource_id, o_current_parameter_hash; CLOSE lock_cur; + -- Since the resource did not previously exist, set o_current_parameter_hash back to NULL + o_current_parameter_hash := NULL; + IF v_logical_resource_id = t_logical_resource_id THEN -- we created the logical resource and therefore we already own the lock. So now we can